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,16 +600,10 @@ pub fn App() -> Element {
});
}
},
span { class: "nav-icon", "\u{1f4ca}" }
span { class: "nav-item-text", "Statistics" }
span { class: "nav-icon",
NavIcon { icon: FaChartBar }
}
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" }
span { class: "nav-item-text", "Statistics" }
}
button {
class: if *current_view.read() == View::Tasks { "nav-item active" } else { "nav-item" },
@ -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);
}

View file

@ -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"
}
}
}
}