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:
parent
3e1e8dea26
commit
f396ce82af
2 changed files with 129 additions and 76 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue