various: bump dependencies; wire up dead code
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I12432bc956453cc4b0a2db82dce1b4976a6a6964
This commit is contained in:
parent
4f878b5abe
commit
875bdf5ebc
6 changed files with 764 additions and 427 deletions
828
Cargo.lock
generated
828
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
23
Cargo.toml
23
Cargo.toml
|
|
@ -15,6 +15,11 @@ license = "MIT"
|
||||||
readme = true
|
readme = true
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
# Internal Dependencies
|
||||||
|
pinakes-core = {path = "./crates/pinakes-core"}
|
||||||
|
pinakes-server = {path = "./crates/pinakes-server"}
|
||||||
|
pinakes-plugin-api = {path = "./crates/plugin/api"}
|
||||||
|
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
tokio-util = { version = "0.7.18", features = ["rt"] }
|
tokio-util = { version = "0.7.18", features = ["rt"] }
|
||||||
|
|
@ -25,7 +30,7 @@ serde_json = "1.0.149"
|
||||||
toml = "0.9.11"
|
toml = "0.9.11"
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
clap = { version = "4.5.56", features = ["derive", "env"] }
|
clap = { version = "4.5.57", features = ["derive", "env"] }
|
||||||
|
|
||||||
# Date/time
|
# Date/time
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
|
|
@ -80,8 +85,8 @@ winnow = "0.7.14"
|
||||||
axum = { version = "0.8.8", features = ["macros", "multipart"] }
|
axum = { version = "0.8.8", features = ["macros", "multipart"] }
|
||||||
tower = "0.5.3"
|
tower = "0.5.3"
|
||||||
tower-http = { version = "0.6.8", features = ["cors", "trace", "set-header"] }
|
tower-http = { version = "0.6.8", features = ["cors", "trace", "set-header"] }
|
||||||
governor = "0.8.1"
|
governor = "0.10.4"
|
||||||
tower_governor = "0.6.0"
|
tower_governor = "0.8.0"
|
||||||
|
|
||||||
# HTTP client
|
# HTTP client
|
||||||
reqwest = { version = "0.13.1", features = ["json", "query", "blocking"] }
|
reqwest = { version = "0.13.1", features = ["json", "query", "blocking"] }
|
||||||
|
|
@ -94,10 +99,10 @@ crossterm = "0.29.0"
|
||||||
dioxus = { version = "0.7.3", features = ["desktop", "router"] }
|
dioxus = { version = "0.7.3", features = ["desktop", "router"] }
|
||||||
|
|
||||||
# Async trait (dyn-compatible async methods)
|
# Async trait (dyn-compatible async methods)
|
||||||
async-trait = "0.1"
|
async-trait = "0.1.89"
|
||||||
|
|
||||||
# Async utilities
|
# Async utilities
|
||||||
futures = "0.3"
|
futures = "0.3.31"
|
||||||
|
|
||||||
# Image processing (thumbnails)
|
# Image processing (thumbnails)
|
||||||
image = { version = "0.25.9", default-features = false, features = [
|
image = { version = "0.25.9", default-features = false, features = [
|
||||||
|
|
@ -110,15 +115,15 @@ image = { version = "0.25.9", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# Markdown rendering
|
# Markdown rendering
|
||||||
pulldown-cmark = "0.12.2"
|
pulldown-cmark = "0.13.0"
|
||||||
|
|
||||||
# Password hashing
|
# Password hashing
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
regex = "1.11"
|
regex = "1.12.3"
|
||||||
|
|
||||||
# WASM runtime for plugins
|
# WASM runtime for plugins
|
||||||
wasmtime = { version = "30.0.2", features = ["component-model"] }
|
wasmtime = { version = "41.0.3", features = ["component-model"] }
|
||||||
wit-bindgen = "0.39.0"
|
wit-bindgen = "0.52.0"
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,7 @@ pub fn create_router_with_tls(
|
||||||
// Login route with strict rate limiting
|
// Login route with strict rate limiting
|
||||||
let login_route = Router::new()
|
let login_route = Router::new()
|
||||||
.route("/auth/login", post(routes::auth::login))
|
.route("/auth/login", post(routes::auth::login))
|
||||||
.layer(GovernorLayer {
|
.layer(GovernorLayer::new(login_governor));
|
||||||
config: login_governor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Public routes (no auth required)
|
// Public routes (no auth required)
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
|
|
@ -82,16 +80,12 @@ pub fn create_router_with_tls(
|
||||||
let search_routes = Router::new()
|
let search_routes = Router::new()
|
||||||
.route("/search", get(routes::search::search))
|
.route("/search", get(routes::search::search))
|
||||||
.route("/search", post(routes::search::search_post))
|
.route("/search", post(routes::search::search_post))
|
||||||
.layer(GovernorLayer {
|
.layer(GovernorLayer::new(search_governor));
|
||||||
config: search_governor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Streaming routes with enhanced rate limiting (5 concurrent)
|
// Streaming routes with enhanced rate limiting (5 concurrent)
|
||||||
let streaming_routes = Router::new()
|
let streaming_routes = Router::new()
|
||||||
.route("/media/{id}/stream", get(routes::media::stream_media))
|
.route("/media/{id}/stream", get(routes::media::stream_media))
|
||||||
.layer(GovernorLayer {
|
.layer(GovernorLayer::new(stream_governor));
|
||||||
config: stream_governor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Read-only routes: any authenticated user (Viewer+)
|
// Read-only routes: any authenticated user (Viewer+)
|
||||||
let viewer_routes = Router::new()
|
let viewer_routes = Router::new()
|
||||||
|
|
@ -561,9 +555,7 @@ pub fn create_router_with_tls(
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.nest("/api/v1", full_api)
|
.nest("/api/v1", full_api)
|
||||||
.layer(DefaultBodyLimit::max(10 * 1024 * 1024))
|
.layer(DefaultBodyLimit::max(10 * 1024 * 1024))
|
||||||
.layer(GovernorLayer {
|
.layer(GovernorLayer::new(global_governor))
|
||||||
config: global_governor,
|
|
||||||
})
|
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.layer(security_headers);
|
.layer(security_headers);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ use futures::future::join_all;
|
||||||
|
|
||||||
use crate::client::*;
|
use crate::client::*;
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
audit, collections, database, detail, duplicates, import, library, search, settings, tags,
|
audit, collections, database, detail, duplicates, import, library,
|
||||||
|
media_player::PlayQueue,
|
||||||
|
search, settings, statistics, tags, tasks,
|
||||||
};
|
};
|
||||||
// Login component available via crate::components::login when auth gating is needed
|
// Login component available via crate::components::login when auth gating is needed
|
||||||
use crate::styles;
|
use crate::styles;
|
||||||
|
|
@ -23,6 +25,8 @@ enum View {
|
||||||
Audit,
|
Audit,
|
||||||
Import,
|
Import,
|
||||||
Duplicates,
|
Duplicates,
|
||||||
|
Statistics,
|
||||||
|
Tasks,
|
||||||
Settings,
|
Settings,
|
||||||
Database,
|
Database,
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +42,8 @@ impl View {
|
||||||
Self::Audit => "Audit Log",
|
Self::Audit => "Audit Log",
|
||||||
Self::Import => "Import",
|
Self::Import => "Import",
|
||||||
Self::Duplicates => "Duplicates",
|
Self::Duplicates => "Duplicates",
|
||||||
|
Self::Statistics => "Statistics",
|
||||||
|
Self::Tasks => "Tasks",
|
||||||
Self::Settings => "Settings",
|
Self::Settings => "Settings",
|
||||||
Self::Database => "Database",
|
Self::Database => "Database",
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +74,8 @@ pub fn App() -> Element {
|
||||||
let mut audit_list = use_signal(Vec::<AuditEntryResponse>::new);
|
let mut audit_list = use_signal(Vec::<AuditEntryResponse>::new);
|
||||||
let mut config_data = use_signal(|| Option::<ConfigResponse>::None);
|
let mut config_data = use_signal(|| Option::<ConfigResponse>::None);
|
||||||
let mut db_stats = use_signal(|| Option::<crate::client::DatabaseStatsResponse>::None);
|
let mut db_stats = use_signal(|| Option::<crate::client::DatabaseStatsResponse>::None);
|
||||||
|
let mut library_stats = use_signal(|| Option::<crate::client::LibraryStatisticsResponse>::None);
|
||||||
|
let mut scheduled_tasks = use_signal(Vec::<crate::client::ScheduledTaskResponse>::new);
|
||||||
let mut duplicate_groups = use_signal(Vec::<crate::client::DuplicateGroupResponse>::new);
|
let mut duplicate_groups = use_signal(Vec::<crate::client::DuplicateGroupResponse>::new);
|
||||||
let mut preview_files = use_signal(Vec::<DirectoryPreviewFile>::new);
|
let mut preview_files = use_signal(Vec::<DirectoryPreviewFile>::new);
|
||||||
let mut preview_total_size = use_signal(|| 0u64);
|
let mut preview_total_size = use_signal(|| 0u64);
|
||||||
|
|
@ -111,6 +119,7 @@ pub fn App() -> Element {
|
||||||
let mut login_error = use_signal(|| Option::<String>::None);
|
let mut login_error = use_signal(|| Option::<String>::None);
|
||||||
let mut login_loading = use_signal(|| false);
|
let mut login_loading = use_signal(|| false);
|
||||||
let mut auto_play_media = use_signal(|| false);
|
let mut auto_play_media = use_signal(|| false);
|
||||||
|
let mut play_queue = use_signal(PlayQueue::default);
|
||||||
|
|
||||||
// Theme state (Phase 3.3)
|
// Theme state (Phase 3.3)
|
||||||
let mut current_theme = use_signal(|| "dark".to_string());
|
let mut current_theme = use_signal(|| "dark".to_string());
|
||||||
|
|
@ -538,6 +547,40 @@ pub fn App() -> Element {
|
||||||
span { class: "nav-icon", "\u{2261}" }
|
span { class: "nav-icon", "\u{2261}" }
|
||||||
span { class: "nav-item-text", "Duplicates" }
|
span { class: "nav-item-text", "Duplicates" }
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
class: if *current_view.read() == View::Statistics { "nav-item active" } else { "nav-item" },
|
||||||
|
onclick: {
|
||||||
|
let client = client.read().clone();
|
||||||
|
move |_| {
|
||||||
|
current_view.set(View::Statistics);
|
||||||
|
let client = client.clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(stats) = client.library_statistics().await {
|
||||||
|
library_stats.set(Some(stats));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
span { class: "nav-icon", "\u{1f4ca}" }
|
||||||
|
span { class: "nav-item-text", "Statistics" }
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: if *current_view.read() == View::Tasks { "nav-item active" } else { "nav-item" },
|
||||||
|
onclick: {
|
||||||
|
let client = client.read().clone();
|
||||||
|
move |_| {
|
||||||
|
current_view.set(View::Tasks);
|
||||||
|
let client = client.clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(tasks_data) = client.list_scheduled_tasks().await {
|
||||||
|
scheduled_tasks.set(tasks_data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
span { class: "nav-icon", "\u{23f0}" }
|
||||||
|
span { class: "nav-item-text", "Tasks" }
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
class: if *current_view.read() == View::Settings { "nav-item active" } else { "nav-item" },
|
class: if *current_view.read() == View::Settings { "nav-item active" } else { "nav-item" },
|
||||||
onclick: {
|
onclick: {
|
||||||
|
|
@ -1175,6 +1218,98 @@ pub fn App() -> Element {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
play_queue: if play_queue.read().is_empty() { None } else { Some(play_queue.read().clone()) },
|
||||||
|
on_queue_select: {
|
||||||
|
move |idx: usize| {
|
||||||
|
let mut q = play_queue.write();
|
||||||
|
q.current_index = idx;
|
||||||
|
// Update selected_media to the item at this index
|
||||||
|
if let Some(item) = q.items.get(idx) {
|
||||||
|
let media_id = item.media_id.clone();
|
||||||
|
let client = client.read().clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(media) = client.get_media(&media_id).await {
|
||||||
|
selected_media.set(Some(media));
|
||||||
|
auto_play_media.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_queue_remove: {
|
||||||
|
move |idx: usize| {
|
||||||
|
play_queue.write().remove(idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_queue_clear: {
|
||||||
|
move |_| {
|
||||||
|
play_queue.write().clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_queue_toggle_repeat: {
|
||||||
|
move |_| {
|
||||||
|
play_queue.write().toggle_repeat();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_queue_toggle_shuffle: {
|
||||||
|
move |_| {
|
||||||
|
play_queue.write().toggle_shuffle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_queue_next: {
|
||||||
|
move |_| {
|
||||||
|
let mut q = play_queue.write();
|
||||||
|
if let Some(item) = q.next() {
|
||||||
|
let media_id = item.media_id.clone();
|
||||||
|
drop(q);
|
||||||
|
let client = client.read().clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(media) = client.get_media(&media_id).await {
|
||||||
|
selected_media.set(Some(media));
|
||||||
|
auto_play_media.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_queue_previous: {
|
||||||
|
move |_| {
|
||||||
|
let mut q = play_queue.write();
|
||||||
|
if let Some(item) = q.previous() {
|
||||||
|
let media_id = item.media_id.clone();
|
||||||
|
drop(q);
|
||||||
|
let client = client.read().clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(media) = client.get_media(&media_id).await {
|
||||||
|
selected_media.set(Some(media));
|
||||||
|
auto_play_media.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_track_ended: {
|
||||||
|
move |_| {
|
||||||
|
let mut q = play_queue.write();
|
||||||
|
if let Some(item) = q.next() {
|
||||||
|
let media_id = item.media_id.clone();
|
||||||
|
drop(q);
|
||||||
|
let client = client.read().clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(media) = client.get_media(&media_id).await {
|
||||||
|
selected_media.set(Some(media));
|
||||||
|
auto_play_media.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_add_to_queue: {
|
||||||
|
move |item: crate::components::media_player::QueueItem| {
|
||||||
|
play_queue.write().add(item);
|
||||||
|
show_toast("Added to queue".into(), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => rsx! {
|
None => rsx! {
|
||||||
|
|
@ -1979,6 +2114,86 @@ pub fn App() -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
View::Statistics => {
|
||||||
|
let refresh_stats = {
|
||||||
|
let client = client.read().clone();
|
||||||
|
move || {
|
||||||
|
let client = client.clone();
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rsx! {
|
||||||
|
statistics::Statistics {
|
||||||
|
stats: library_stats.read().clone(),
|
||||||
|
on_refresh: {
|
||||||
|
let refresh_stats = refresh_stats.clone();
|
||||||
|
move |_| refresh_stats()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
View::Tasks => {
|
||||||
|
let refresh_tasks = {
|
||||||
|
let client = client.read().clone();
|
||||||
|
move || {
|
||||||
|
let client = client.clone();
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rsx! {
|
||||||
|
tasks::Tasks {
|
||||||
|
tasks: scheduled_tasks.read().clone(),
|
||||||
|
on_refresh: {
|
||||||
|
let refresh_tasks = refresh_tasks.clone();
|
||||||
|
move |_| refresh_tasks()
|
||||||
|
},
|
||||||
|
on_toggle: {
|
||||||
|
let client = client.read().clone();
|
||||||
|
let refresh_tasks = refresh_tasks.clone();
|
||||||
|
move |task_id: String| {
|
||||||
|
let client = client.clone();
|
||||||
|
let refresh_tasks = refresh_tasks.clone();
|
||||||
|
spawn(async move {
|
||||||
|
match client.toggle_scheduled_task(&task_id).await {
|
||||||
|
Ok(_) => {
|
||||||
|
show_toast("Task toggled".into(), false);
|
||||||
|
refresh_tasks();
|
||||||
|
}
|
||||||
|
Err(e) => show_toast(format!("Toggle failed: {e}"), true),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_run_now: {
|
||||||
|
let client = client.read().clone();
|
||||||
|
let refresh_tasks = refresh_tasks.clone();
|
||||||
|
move |task_id: String| {
|
||||||
|
let client = client.clone();
|
||||||
|
let refresh_tasks = refresh_tasks.clone();
|
||||||
|
spawn(async move {
|
||||||
|
match client.run_scheduled_task_now(&task_id).await {
|
||||||
|
Ok(_) => {
|
||||||
|
show_toast("Task started".into(), false);
|
||||||
|
refresh_tasks();
|
||||||
|
}
|
||||||
|
Err(e) => show_toast(format!("Run failed: {e}"), true),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
View::Settings => {
|
View::Settings => {
|
||||||
let cfg_ref = config_data.read();
|
let cfg_ref = config_data.read();
|
||||||
match cfg_ref.as_ref() {
|
match cfg_ref.as_ref() {
|
||||||
|
|
|
||||||
|
|
@ -482,34 +482,6 @@ impl ApiClient {
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn batch_import(
|
|
||||||
&self,
|
|
||||||
paths: &[String],
|
|
||||||
tag_ids: &[String],
|
|
||||||
new_tags: &[String],
|
|
||||||
collection_id: Option<&str>,
|
|
||||||
) -> Result<BatchImportResponse> {
|
|
||||||
let mut body = serde_json::json!({"paths": paths});
|
|
||||||
if !tag_ids.is_empty() {
|
|
||||||
body["tag_ids"] = serde_json::json!(tag_ids);
|
|
||||||
}
|
|
||||||
if !new_tags.is_empty() {
|
|
||||||
body["new_tags"] = serde_json::json!(new_tags);
|
|
||||||
}
|
|
||||||
if let Some(cid) = collection_id {
|
|
||||||
body["collection_id"] = serde_json::json!(cid);
|
|
||||||
}
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.post(self.url("/media/import/batch"))
|
|
||||||
.json(&body)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json()
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn import_directory(
|
pub async fn import_directory(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
|
@ -967,17 +939,6 @@ impl ApiClient {
|
||||||
|
|
||||||
// ── UI Config ──
|
// ── UI Config ──
|
||||||
|
|
||||||
pub async fn get_ui_config(&self) -> Result<UiConfigResponse> {
|
|
||||||
Ok(self
|
|
||||||
.client
|
|
||||||
.get(self.url("/config/ui"))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json()
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_ui_config(&self, updates: serde_json::Value) -> Result<UiConfigResponse> {
|
pub async fn update_ui_config(&self, updates: serde_json::Value) -> Result<UiConfigResponse> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.client
|
.client
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use dioxus::prelude::*;
|
||||||
|
|
||||||
use super::image_viewer::ImageViewer;
|
use super::image_viewer::ImageViewer;
|
||||||
use super::markdown_viewer::MarkdownViewer;
|
use super::markdown_viewer::MarkdownViewer;
|
||||||
use super::media_player::MediaPlayer;
|
use super::media_player::{MediaPlayer, PlayQueue, QueueItem, QueuePanel};
|
||||||
use super::pdf_viewer::PdfViewer;
|
use super::pdf_viewer::PdfViewer;
|
||||||
use super::utils::{format_duration, format_size, media_category, type_badge_class};
|
use super::utils::{format_duration, format_size, media_category, type_badge_class};
|
||||||
use crate::client::{ApiClient, MediaResponse, MediaUpdateEvent, TagResponse};
|
use crate::client::{ApiClient, MediaResponse, MediaUpdateEvent, TagResponse};
|
||||||
|
|
@ -14,6 +14,7 @@ pub fn Detail(
|
||||||
all_tags: Vec<TagResponse>,
|
all_tags: Vec<TagResponse>,
|
||||||
server_url: String,
|
server_url: String,
|
||||||
#[props(default = false)] autoplay: bool,
|
#[props(default = false)] autoplay: bool,
|
||||||
|
#[props(default)] play_queue: Option<PlayQueue>,
|
||||||
on_back: EventHandler<()>,
|
on_back: EventHandler<()>,
|
||||||
on_open: EventHandler<String>,
|
on_open: EventHandler<String>,
|
||||||
on_update: EventHandler<MediaUpdateEvent>,
|
on_update: EventHandler<MediaUpdateEvent>,
|
||||||
|
|
@ -22,6 +23,15 @@ pub fn Detail(
|
||||||
on_set_custom_field: EventHandler<(String, String, String, String)>,
|
on_set_custom_field: EventHandler<(String, String, String, String)>,
|
||||||
on_delete_custom_field: EventHandler<(String, String)>,
|
on_delete_custom_field: EventHandler<(String, String)>,
|
||||||
on_delete: EventHandler<String>,
|
on_delete: EventHandler<String>,
|
||||||
|
#[props(default)] on_queue_select: Option<EventHandler<usize>>,
|
||||||
|
#[props(default)] on_queue_remove: Option<EventHandler<usize>>,
|
||||||
|
#[props(default)] on_queue_clear: Option<EventHandler<()>>,
|
||||||
|
#[props(default)] on_queue_toggle_repeat: Option<EventHandler<()>>,
|
||||||
|
#[props(default)] on_queue_toggle_shuffle: Option<EventHandler<()>>,
|
||||||
|
#[props(default)] on_queue_next: Option<EventHandler<()>>,
|
||||||
|
#[props(default)] on_queue_previous: Option<EventHandler<()>>,
|
||||||
|
#[props(default)] on_track_ended: Option<EventHandler<()>>,
|
||||||
|
#[props(default)] on_add_to_queue: Option<EventHandler<QueueItem>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let mut editing = use_signal(|| false);
|
let mut editing = use_signal(|| false);
|
||||||
let mut show_image_viewer = use_signal(|| false);
|
let mut show_image_viewer = use_signal(|| false);
|
||||||
|
|
@ -225,6 +235,9 @@ pub fn Detail(
|
||||||
let thumb_for_player = thumbnail_url.clone();
|
let thumb_for_player = thumbnail_url.clone();
|
||||||
let file_name_for_viewer = media.file_name.clone();
|
let file_name_for_viewer = media.file_name.clone();
|
||||||
|
|
||||||
|
// Clone queue handlers for use in the component
|
||||||
|
let has_queue = play_queue.is_some();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
// Media preview
|
// Media preview
|
||||||
div { class: "detail-preview",
|
div { class: "detail-preview",
|
||||||
|
|
@ -235,6 +248,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,
|
||||||
}
|
}
|
||||||
} else if category == "video" {
|
} else if category == "video" {
|
||||||
MediaPlayer {
|
MediaPlayer {
|
||||||
|
|
@ -242,6 +256,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,
|
||||||
}
|
}
|
||||||
} else if category == "image" {
|
} else if category == "image" {
|
||||||
if has_thumbnail {
|
if has_thumbnail {
|
||||||
|
|
@ -290,6 +305,71 @@ pub fn Detail(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play queue panel (only for audio/video with a queue)
|
||||||
|
if has_queue && (category == "audio" || category == "video") {
|
||||||
|
if let Some(ref queue) = play_queue {
|
||||||
|
QueuePanel {
|
||||||
|
queue: queue.clone(),
|
||||||
|
on_select: {
|
||||||
|
let handler = on_queue_select;
|
||||||
|
move |idx| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_remove: {
|
||||||
|
let handler = on_queue_remove;
|
||||||
|
move |idx| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_clear: {
|
||||||
|
let handler = on_queue_clear;
|
||||||
|
move |_| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_toggle_repeat: {
|
||||||
|
let handler = on_queue_toggle_repeat;
|
||||||
|
move |_| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_toggle_shuffle: {
|
||||||
|
let handler = on_queue_toggle_shuffle;
|
||||||
|
move |_| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_next: {
|
||||||
|
let handler = on_queue_next;
|
||||||
|
move |_| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_previous: {
|
||||||
|
let handler = on_queue_previous;
|
||||||
|
move |_| {
|
||||||
|
if let Some(ref h) = handler {
|
||||||
|
h.call(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Action bar
|
// Action bar
|
||||||
div { class: "detail-actions",
|
div { class: "detail-actions",
|
||||||
button {
|
button {
|
||||||
|
|
@ -305,6 +385,46 @@ pub fn Detail(
|
||||||
},
|
},
|
||||||
"Open"
|
"Open"
|
||||||
}
|
}
|
||||||
|
// Add to Queue button for audio/video content
|
||||||
|
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()
|
||||||
|
.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 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 on_add = on_add_to_queue;
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: if is_current { "btn btn-secondary disabled" } else { "btn btn-secondary" },
|
||||||
|
disabled: is_current,
|
||||||
|
title: if is_current { "Currently playing" } else { "Add to play queue" },
|
||||||
|
onclick: move |_| {
|
||||||
|
if let Some(ref handler) = on_add {
|
||||||
|
let item = QueueItem {
|
||||||
|
media_id: media_id_for_queue.clone(),
|
||||||
|
title: title_for_queue.clone(),
|
||||||
|
artist: artist_for_queue.clone(),
|
||||||
|
duration_secs: duration_for_queue,
|
||||||
|
media_type: media_type_for_queue.clone(),
|
||||||
|
stream_url: stream_url_for_queue.clone(),
|
||||||
|
thumbnail_url: thumbnail_for_queue.clone(),
|
||||||
|
};
|
||||||
|
handler.call(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
if is_current { "\u{266b} Playing" } else { "\u{2795} Queue" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if is_editing {
|
if is_editing {
|
||||||
button { class: "btn btn-primary", onclick: on_save_click, "Save" }
|
button { class: "btn btn-primary", onclick: on_save_click, "Save" }
|
||||||
button { class: "btn btn-ghost", onclick: on_cancel_click, "Cancel" }
|
button { class: "btn btn-ghost", onclick: on_cancel_click, "Cancel" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue