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 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue