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 std::sync::atomic::{AtomicUsize, Ordering};
use dioxus::prelude::*; 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; 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::client::*;
use crate::components::{ use crate::components::{
audit, collections, database, detail, duplicates, graph_view, import, library, audit, collections, database, detail, duplicates, graph_view, import, library,
media_player::PlayQueue, media_player::PlayQueue, search, settings, statistics, tags, tasks,
search, settings, statistics, tags, tasks,
}; };
// Login component available via crate::components::login when auth gating is needed
use crate::styles; use crate::styles;
static TOAST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); static TOAST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
@ -454,15 +470,18 @@ pub fn App() -> Element {
refresh_media(); refresh_media();
} }
}, },
span { class: "nav-icon", "\u{25a6}" } span { class: "nav-icon",
NavIcon { icon: FaBook }
}
span { class: "nav-item-text", "Library" } span { class: "nav-item-text", "Library" }
// Phase 7.2: Badge
span { class: "nav-badge", "{media_total_count}" } span { class: "nav-badge", "{media_total_count}" }
} }
button { button {
class: if *current_view.read() == View::Search { "nav-item active" } else { "nav-item" }, class: if *current_view.read() == View::Search { "nav-item active" } else { "nav-item" },
onclick: move |_| current_view.set(View::Search), 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" } span { class: "nav-item-text", "Search" }
} }
button { button {
@ -479,9 +498,21 @@ pub fn App() -> Element {
refresh_collections(); refresh_collections();
} }
}, },
span { class: "nav-icon", "\u{2912}" } span { class: "nav-icon",
NavIcon { icon: FaUpload }
}
span { class: "nav-item-text", "Import" } 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", div { class: "nav-section",
@ -495,9 +526,10 @@ pub fn App() -> Element {
refresh_tags(); refresh_tags();
} }
}, },
span { class: "nav-icon", "\u{2605}" } span { class: "nav-icon",
NavIcon { icon: FaTags }
}
span { class: "nav-item-text", "Tags" } span { class: "nav-item-text", "Tags" }
// Phase 7.2: Badge
span { class: "nav-badge", "{tags_list.read().len()}" } span { class: "nav-badge", "{tags_list.read().len()}" }
} }
button { button {
@ -511,9 +543,10 @@ pub fn App() -> Element {
refresh_collections(); refresh_collections();
} }
}, },
span { class: "nav-icon", "\u{2630}" } span { class: "nav-icon",
NavIcon { icon: FaLayerGroup }
}
span { class: "nav-item-text", "Collections" } span { class: "nav-item-text", "Collections" }
// Phase 7.2: Badge
span { class: "nav-badge", "{collections_list.read().len()}" } span { class: "nav-badge", "{collections_list.read().len()}" }
} }
} }
@ -529,7 +562,9 @@ pub fn App() -> Element {
refresh_audit(); refresh_audit();
} }
}, },
span { class: "nav-icon", "\u{2637}" } span { class: "nav-icon",
NavIcon { icon: FaClockRotateLeft }
}
span { class: "nav-item-text", "Audit" } span { class: "nav-item-text", "Audit" }
} }
button { 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" } span { class: "nav-item-text", "Duplicates" }
} }
button { 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" } 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 { button {
class: if *current_view.read() == View::Tasks { "nav-item active" } else { "nav-item" }, class: if *current_view.read() == View::Tasks { "nav-item active" } else { "nav-item" },
onclick: { 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" } span { class: "nav-item-text", "Tasks" }
} }
button { 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" } span { class: "nav-item-text", "Settings" }
} }
button { 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" } span { class: "nav-item-text", "Database" }
} }
} }
@ -672,9 +709,9 @@ pub fn App() -> Element {
class: "sidebar-toggle", class: "sidebar-toggle",
onclick: move |_| sidebar_collapsed.toggle(), onclick: move |_| sidebar_collapsed.toggle(),
if *sidebar_collapsed.read() { if *sidebar_collapsed.read() {
"\u{25b6}" NavIcon { icon: FaChevronRight }
} else { } else {
"\u{25c0}" NavIcon { icon: FaChevronLeft }
} }
} }
@ -696,6 +733,7 @@ pub fn App() -> Element {
}); });
} }
}, },
NavIcon { icon: FaArrowRightFromBracket }
"Logout" "Logout"
} }
} }
@ -1334,7 +1372,46 @@ pub fn App() -> Element {
selected_media.set(Some(media)); selected_media.set(Some(media));
auto_play_media.set(false); 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_media = refresh_media.clone();
let refresh_tags = refresh_tags.clone(); let refresh_tags = refresh_tags.clone();
move |(path, tag_ids, new_tags, col_id): ImportEvent| { 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(); 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() { if *import_in_progress.read() {
import_queue.write().push(file_name); import_queue.write().push(file_name);
show_toast("Added to import queue".into(), false); show_toast("Added to import queue".into(), false);
return; return;
@ -2151,7 +2189,9 @@ pub fn App() -> Element {
spawn(async move { spawn(async move {
match client.library_statistics().await { match client.library_statistics().await {
Ok(stats) => library_stats.set(Some(stats)), 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 { spawn(async move {
match client.list_scheduled_tasks().await { match client.list_scheduled_tasks().await {
Ok(tasks_data) => scheduled_tasks.set(tasks_data), 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 { spawn(async move {
match client.get_media(&media_id).await { match client.get_media(&media_id).await {
Ok(media) => { Ok(media) => {
// Load tags for the media
if let Ok(mtags) = client.get_media_tags(&media_id).await { if let Ok(mtags) = client.get_media_tags(&media_id).await {
media_tags.set(mtags); media_tags.set(mtags);
} }

View file

@ -250,7 +250,7 @@ pub fn Detail(
title: media.title.clone(), title: media.title.clone(),
thumbnail_url: if has_thumbnail { Some(thumb_for_player.clone()) } else { None }, thumbnail_url: if has_thumbnail { Some(thumb_for_player.clone()) } else { None },
autoplay, autoplay,
on_track_ended: on_track_ended, on_track_ended,
} }
} else if category == "video" { } else if category == "video" {
MediaPlayer { MediaPlayer {
@ -258,7 +258,7 @@ pub fn Detail(
media_type: "video".to_string(), media_type: "video".to_string(),
title: media.title.clone(), title: media.title.clone(),
autoplay, autoplay,
on_track_ended: on_track_ended, on_track_ended,
} }
} else if category == "image" { } else if category == "image" {
if has_thumbnail { if has_thumbnail {
@ -391,17 +391,25 @@ pub fn Detail(
if (category == "audio" || category == "video") && on_add_to_queue.is_some() { if (category == "audio" || category == "video") && on_add_to_queue.is_some() {
{ {
// Check if this item is currently playing // 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()) .and_then(|q| q.current())
.map(|item| item.media_id == id) .map(|item| item.media_id == id)
.unwrap_or(false); .unwrap_or(false);
let media_id_for_queue = id.clone(); 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 artist_for_queue = media.artist.clone();
let duration_for_queue = media.duration_secs; let duration_for_queue = media.duration_secs;
let media_type_for_queue = category.to_string(); let media_type_for_queue = category.to_string();
let stream_url_for_queue = stream_url.clone(); 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; let on_add = on_add_to_queue;
rsx! { rsx! {
button { button {
@ -422,7 +430,11 @@ pub fn Detail(
handler.call(item); handler.call(item);
} }
}, },
if is_current { "\u{266b} Playing" } else { "\u{2795} Queue" } if is_current {
"\u{266b} Playing"
} else {
"\u{2795} Queue"
}
} }
} }
} }