pinakes-ui: integrate graph view; improve navigation via proper icons

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6d1d427f93b5293fc55cd5599ed02e696a6a6964
This commit is contained in:
raf 2026-02-09 13:15:15 +03:00
commit f396ce82af
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 129 additions and 76 deletions

View file

@ -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<T: IconShape + Clone + PartialEq + 'static>(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);
}