diff --git a/crates/pinakes-ui/src/app.rs b/crates/pinakes-ui/src/app.rs index be3d657..96a8d51 100644 --- a/crates/pinakes-ui/src/app.rs +++ b/crates/pinakes-ui/src/app.rs @@ -2,15 +2,31 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use dioxus::prelude::*; +use dioxus_free_icons::icons::fa_solid_icons::{ + FaArrowRightFromBracket, FaBook, FaChartBar, FaChevronLeft, FaChevronRight, FaClock, + FaClockRotateLeft, FaCopy, FaDatabase, FaDiagramProject, FaGear, FaLayerGroup, + FaMagnifyingGlass, FaTags, FaUpload, +}; +use dioxus_free_icons::{Icon, IconShape}; use futures::future::join_all; +#[component] +fn NavIcon(icon: T) -> Element { + rsx! { + Icon { + width: 16, + height: 16, + fill: "currentColor", + icon, + } + } +} + use crate::client::*; use crate::components::{ audit, collections, database, detail, duplicates, graph_view, import, library, - media_player::PlayQueue, - search, settings, statistics, tags, tasks, + media_player::PlayQueue, search, settings, statistics, tags, tasks, }; -// Login component available via crate::components::login when auth gating is needed use crate::styles; static TOAST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -454,15 +470,18 @@ pub fn App() -> Element { refresh_media(); } }, - span { class: "nav-icon", "\u{25a6}" } + span { class: "nav-icon", + NavIcon { icon: FaBook } + } span { class: "nav-item-text", "Library" } - // Phase 7.2: Badge span { class: "nav-badge", "{media_total_count}" } } button { class: if *current_view.read() == View::Search { "nav-item active" } else { "nav-item" }, onclick: move |_| current_view.set(View::Search), - span { class: "nav-icon", "\u{2315}" } + span { class: "nav-icon", + NavIcon { icon: FaMagnifyingGlass } + } span { class: "nav-item-text", "Search" } } button { @@ -479,9 +498,21 @@ pub fn App() -> Element { refresh_collections(); } }, - span { class: "nav-icon", "\u{2912}" } + span { class: "nav-icon", + NavIcon { icon: FaUpload } + } span { class: "nav-item-text", "Import" } } + button { + class: if *current_view.read() == View::Graph { "nav-item active" } else { "nav-item" }, + onclick: move |_| { + current_view.set(View::Graph); + }, + span { class: "nav-icon", + NavIcon { icon: FaDiagramProject } + } + span { class: "nav-item-text", "Graph" } + } } div { class: "nav-section", @@ -495,9 +526,10 @@ pub fn App() -> Element { refresh_tags(); } }, - span { class: "nav-icon", "\u{2605}" } + span { class: "nav-icon", + NavIcon { icon: FaTags } + } span { class: "nav-item-text", "Tags" } - // Phase 7.2: Badge span { class: "nav-badge", "{tags_list.read().len()}" } } button { @@ -511,9 +543,10 @@ pub fn App() -> Element { refresh_collections(); } }, - span { class: "nav-icon", "\u{2630}" } + span { class: "nav-icon", + NavIcon { icon: FaLayerGroup } + } span { class: "nav-item-text", "Collections" } - // Phase 7.2: Badge span { class: "nav-badge", "{collections_list.read().len()}" } } } @@ -529,7 +562,9 @@ pub fn App() -> Element { refresh_audit(); } }, - span { class: "nav-icon", "\u{2637}" } + span { class: "nav-icon", + NavIcon { icon: FaClockRotateLeft } + } span { class: "nav-item-text", "Audit" } } button { @@ -546,7 +581,9 @@ pub fn App() -> Element { }); } }, - span { class: "nav-icon", "\u{2261}" } + span { class: "nav-icon", + NavIcon { icon: FaCopy } + } span { class: "nav-item-text", "Duplicates" } } button { @@ -563,17 +600,11 @@ pub fn App() -> Element { }); } }, - span { class: "nav-icon", "\u{1f4ca}" } + span { class: "nav-icon", + NavIcon { icon: FaChartBar } + } span { class: "nav-item-text", "Statistics" } } - button { - class: if *current_view.read() == View::Graph { "nav-item active" } else { "nav-item" }, - onclick: move |_| { - current_view.set(View::Graph); - }, - span { class: "nav-icon", "\u{1f578}" } - span { class: "nav-item-text", "Graph" } - } button { class: if *current_view.read() == View::Tasks { "nav-item active" } else { "nav-item" }, onclick: { @@ -588,7 +619,9 @@ pub fn App() -> Element { }); } }, - span { class: "nav-icon", "\u{23f0}" } + span { class: "nav-icon", + NavIcon { icon: FaClock } + } span { class: "nav-item-text", "Tasks" } } button { @@ -605,7 +638,9 @@ pub fn App() -> Element { }); } }, - span { class: "nav-icon", "\u{2699}" } + span { class: "nav-icon", + NavIcon { icon: FaGear } + } span { class: "nav-item-text", "Settings" } } button { @@ -622,7 +657,9 @@ pub fn App() -> Element { }); } }, - span { class: "nav-icon", "\u{2750}" } + span { class: "nav-icon", + NavIcon { icon: FaDatabase } + } span { class: "nav-item-text", "Database" } } } @@ -672,9 +709,9 @@ pub fn App() -> Element { class: "sidebar-toggle", onclick: move |_| sidebar_collapsed.toggle(), if *sidebar_collapsed.read() { - "\u{25b6}" + NavIcon { icon: FaChevronRight } } else { - "\u{25c0}" + NavIcon { icon: FaChevronLeft } } } @@ -696,6 +733,7 @@ pub fn App() -> Element { }); } }, + NavIcon { icon: FaArrowRightFromBracket } "Logout" } } @@ -1334,7 +1372,46 @@ pub fn App() -> Element { selected_media.set(Some(media)); auto_play_media.set(false); } - Err(e) => show_toast(format!("Failed to load linked note: {e}"), true), + // Extract file name from path + + // Check if already importing - if so, add to queue + + // Extract directory name from path + + // Check if already importing - if so, add to queue + + // Get preview files if available for per-file progress + + // Use parallel import with per-batch progress + + // Show first file in batch as current + + // Process batch in parallel + + // Update progress after batch + + // Fallback: use server-side directory import (no per-file progress) + // Check if already importing - if so, add to queue + + // Update progress from scan status + + // Check if already importing - if so, add to queue + + // Process files in parallel batches for better performance + + // Show first file in batch as current + + // Process batch in parallel + + // Update progress after batch + + // Extended import state + + Err(e) => { + + // Load tags for the media + show_toast(format!("Failed to load linked note: {e}"), true) + } } }); } @@ -1567,46 +1644,8 @@ pub fn App() -> Element { let refresh_media = refresh_media.clone(); let refresh_tags = refresh_tags.clone(); move |(path, tag_ids, new_tags, col_id): ImportEvent| { - // Extract file name from path let file_name = path.rsplit('/').next().unwrap_or(&path).to_string(); - // Check if already importing - if so, add to queue - - // Extract directory name from path - - // Check if already importing - if so, add to queue - - // Get preview files if available for per-file progress - - // Use parallel import with per-batch progress - - // Show first file in batch as current - - // Process batch in parallel - - // Update progress after batch - - // Fallback: use server-side directory import (no per-file progress) - // Check if already importing - if so, add to queue - - // Update progress from scan status - - // Check if already importing - if so, add to queue - - // Process files in parallel batches for better performance - - // Show first file in batch as current - - // Process batch in parallel - - // Update progress after batch - - // Extended import state - - - - - @@ -1625,7 +1664,6 @@ pub fn App() -> Element { if *import_in_progress.read() { - import_queue.write().push(file_name); show_toast("Added to import queue".into(), false); return; @@ -2151,7 +2189,9 @@ pub fn App() -> Element { spawn(async move { match client.library_statistics().await { Ok(stats) => library_stats.set(Some(stats)), - Err(e) => show_toast(format!("Failed to load statistics: {e}"), true), + Err(e) => { + show_toast(format!("Failed to load statistics: {e}"), true) + } } }); } @@ -2174,7 +2214,9 @@ pub fn App() -> Element { spawn(async move { match client.list_scheduled_tasks().await { Ok(tasks_data) => scheduled_tasks.set(tasks_data), - Err(e) => show_toast(format!("Failed to load tasks: {e}"), true), + Err(e) => { + show_toast(format!("Failed to load tasks: {e}"), true) + } } }); } @@ -2346,7 +2388,6 @@ pub fn App() -> Element { spawn(async move { match client.get_media(&media_id).await { Ok(media) => { - // Load tags for the media if let Ok(mtags) = client.get_media_tags(&media_id).await { media_tags.set(mtags); } diff --git a/crates/pinakes-ui/src/components/detail.rs b/crates/pinakes-ui/src/components/detail.rs index 021124f..b478f1a 100644 --- a/crates/pinakes-ui/src/components/detail.rs +++ b/crates/pinakes-ui/src/components/detail.rs @@ -250,7 +250,7 @@ pub fn Detail( title: media.title.clone(), thumbnail_url: if has_thumbnail { Some(thumb_for_player.clone()) } else { None }, autoplay, - on_track_ended: on_track_ended, + on_track_ended, } } else if category == "video" { MediaPlayer { @@ -258,7 +258,7 @@ pub fn Detail( media_type: "video".to_string(), title: media.title.clone(), autoplay, - on_track_ended: on_track_ended, + on_track_ended, } } else if category == "image" { if has_thumbnail { @@ -391,17 +391,25 @@ pub fn Detail( if (category == "audio" || category == "video") && on_add_to_queue.is_some() { { // Check if this item is currently playing - let is_current = play_queue.as_ref() + let is_current = play_queue + .as_ref() .and_then(|q| q.current()) .map(|item| item.media_id == id) .unwrap_or(false); let media_id_for_queue = id.clone(); - let title_for_queue = media.title.clone().unwrap_or_else(|| media.file_name.clone()); + let title_for_queue = media + .title + .clone() + .unwrap_or_else(|| media.file_name.clone()); let artist_for_queue = media.artist.clone(); let duration_for_queue = media.duration_secs; let media_type_for_queue = category.to_string(); let stream_url_for_queue = stream_url.clone(); - let thumbnail_for_queue = if has_thumbnail { Some(thumbnail_url.clone()) } else { None }; + let thumbnail_for_queue = if has_thumbnail { + Some(thumbnail_url.clone()) + } else { + None + }; let on_add = on_add_to_queue; rsx! { button { @@ -422,7 +430,11 @@ pub fn Detail( handler.call(item); } }, - if is_current { "\u{266b} Playing" } else { "\u{2795} Queue" } + if is_current { + "\u{266b} Playing" + } else { + "\u{2795} Queue" + } } } }