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
|
||||
|
||||
[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
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.18", features = ["rt"] }
|
||||
|
|
@ -25,7 +30,7 @@ serde_json = "1.0.149"
|
|||
toml = "0.9.11"
|
||||
|
||||
# CLI argument parsing
|
||||
clap = { version = "4.5.56", features = ["derive", "env"] }
|
||||
clap = { version = "4.5.57", features = ["derive", "env"] }
|
||||
|
||||
# Date/time
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
|
|
@ -80,8 +85,8 @@ winnow = "0.7.14"
|
|||
axum = { version = "0.8.8", features = ["macros", "multipart"] }
|
||||
tower = "0.5.3"
|
||||
tower-http = { version = "0.6.8", features = ["cors", "trace", "set-header"] }
|
||||
governor = "0.8.1"
|
||||
tower_governor = "0.6.0"
|
||||
governor = "0.10.4"
|
||||
tower_governor = "0.8.0"
|
||||
|
||||
# HTTP client
|
||||
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"] }
|
||||
|
||||
# Async trait (dyn-compatible async methods)
|
||||
async-trait = "0.1"
|
||||
async-trait = "0.1.89"
|
||||
|
||||
# Async utilities
|
||||
futures = "0.3"
|
||||
futures = "0.3.31"
|
||||
|
||||
# Image processing (thumbnails)
|
||||
image = { version = "0.25.9", default-features = false, features = [
|
||||
|
|
@ -110,15 +115,15 @@ image = { version = "0.25.9", default-features = false, features = [
|
|||
] }
|
||||
|
||||
# Markdown rendering
|
||||
pulldown-cmark = "0.12.2"
|
||||
pulldown-cmark = "0.13.0"
|
||||
|
||||
# Password hashing
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
|
||||
# Misc
|
||||
mime_guess = "2.0.5"
|
||||
regex = "1.11"
|
||||
regex = "1.12.3"
|
||||
|
||||
# WASM runtime for plugins
|
||||
wasmtime = { version = "30.0.2", features = ["component-model"] }
|
||||
wit-bindgen = "0.39.0"
|
||||
wasmtime = { version = "41.0.3", features = ["component-model"] }
|
||||
wit-bindgen = "0.52.0"
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@ pub fn create_router_with_tls(
|
|||
// Login route with strict rate limiting
|
||||
let login_route = Router::new()
|
||||
.route("/auth/login", post(routes::auth::login))
|
||||
.layer(GovernorLayer {
|
||||
config: login_governor,
|
||||
});
|
||||
.layer(GovernorLayer::new(login_governor));
|
||||
|
||||
// Public routes (no auth required)
|
||||
let public_routes = Router::new()
|
||||
|
|
@ -82,16 +80,12 @@ pub fn create_router_with_tls(
|
|||
let search_routes = Router::new()
|
||||
.route("/search", get(routes::search::search))
|
||||
.route("/search", post(routes::search::search_post))
|
||||
.layer(GovernorLayer {
|
||||
config: search_governor,
|
||||
});
|
||||
.layer(GovernorLayer::new(search_governor));
|
||||
|
||||
// Streaming routes with enhanced rate limiting (5 concurrent)
|
||||
let streaming_routes = Router::new()
|
||||
.route("/media/{id}/stream", get(routes::media::stream_media))
|
||||
.layer(GovernorLayer {
|
||||
config: stream_governor,
|
||||
});
|
||||
.layer(GovernorLayer::new(stream_governor));
|
||||
|
||||
// Read-only routes: any authenticated user (Viewer+)
|
||||
let viewer_routes = Router::new()
|
||||
|
|
@ -561,9 +555,7 @@ pub fn create_router_with_tls(
|
|||
let router = Router::new()
|
||||
.nest("/api/v1", full_api)
|
||||
.layer(DefaultBodyLimit::max(10 * 1024 * 1024))
|
||||
.layer(GovernorLayer {
|
||||
config: global_governor,
|
||||
})
|
||||
.layer(GovernorLayer::new(global_governor))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(cors)
|
||||
.layer(security_headers);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use futures::future::join_all;
|
|||
|
||||
use crate::client::*;
|
||||
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
|
||||
use crate::styles;
|
||||
|
|
@ -23,6 +25,8 @@ enum View {
|
|||
Audit,
|
||||
Import,
|
||||
Duplicates,
|
||||
Statistics,
|
||||
Tasks,
|
||||
Settings,
|
||||
Database,
|
||||
}
|
||||
|
|
@ -38,6 +42,8 @@ impl View {
|
|||
Self::Audit => "Audit Log",
|
||||
Self::Import => "Import",
|
||||
Self::Duplicates => "Duplicates",
|
||||
Self::Statistics => "Statistics",
|
||||
Self::Tasks => "Tasks",
|
||||
Self::Settings => "Settings",
|
||||
Self::Database => "Database",
|
||||
}
|
||||
|
|
@ -68,6 +74,8 @@ pub fn App() -> Element {
|
|||
let mut audit_list = use_signal(Vec::<AuditEntryResponse>::new);
|
||||
let mut config_data = use_signal(|| Option::<ConfigResponse>::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 preview_files = use_signal(Vec::<DirectoryPreviewFile>::new);
|
||||
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_loading = use_signal(|| false);
|
||||
let mut auto_play_media = use_signal(|| false);
|
||||
let mut play_queue = use_signal(PlayQueue::default);
|
||||
|
||||
// Theme state (Phase 3.3)
|
||||
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-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 {
|
||||
class: if *current_view.read() == View::Settings { "nav-item active" } else { "nav-item" },
|
||||
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! {
|
||||
|
|
@ -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 => {
|
||||
let cfg_ref = config_data.read();
|
||||
match cfg_ref.as_ref() {
|
||||
|
|
|
|||
|
|
@ -482,34 +482,6 @@ impl ApiClient {
|
|||
.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(
|
||||
&self,
|
||||
path: &str,
|
||||
|
|
@ -967,17 +939,6 @@ impl ApiClient {
|
|||
|
||||
// ── 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> {
|
||||
Ok(self
|
||||
.client
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use dioxus::prelude::*;
|
|||
|
||||
use super::image_viewer::ImageViewer;
|
||||
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::utils::{format_duration, format_size, media_category, type_badge_class};
|
||||
use crate::client::{ApiClient, MediaResponse, MediaUpdateEvent, TagResponse};
|
||||
|
|
@ -14,6 +14,7 @@ pub fn Detail(
|
|||
all_tags: Vec<TagResponse>,
|
||||
server_url: String,
|
||||
#[props(default = false)] autoplay: bool,
|
||||
#[props(default)] play_queue: Option<PlayQueue>,
|
||||
on_back: EventHandler<()>,
|
||||
on_open: EventHandler<String>,
|
||||
on_update: EventHandler<MediaUpdateEvent>,
|
||||
|
|
@ -22,6 +23,15 @@ pub fn Detail(
|
|||
on_set_custom_field: EventHandler<(String, String, String, String)>,
|
||||
on_delete_custom_field: EventHandler<(String, 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 {
|
||||
let mut editing = 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 file_name_for_viewer = media.file_name.clone();
|
||||
|
||||
// Clone queue handlers for use in the component
|
||||
let has_queue = play_queue.is_some();
|
||||
|
||||
rsx! {
|
||||
// Media preview
|
||||
div { class: "detail-preview",
|
||||
|
|
@ -235,6 +248,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,
|
||||
}
|
||||
} else if category == "video" {
|
||||
MediaPlayer {
|
||||
|
|
@ -242,6 +256,7 @@ pub fn Detail(
|
|||
media_type: "video".to_string(),
|
||||
title: media.title.clone(),
|
||||
autoplay,
|
||||
on_track_ended: on_track_ended,
|
||||
}
|
||||
} else if category == "image" {
|
||||
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
|
||||
div { class: "detail-actions",
|
||||
button {
|
||||
|
|
@ -305,6 +385,46 @@ pub fn Detail(
|
|||
},
|
||||
"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 {
|
||||
button { class: "btn btn-primary", onclick: on_save_click, "Save" }
|
||||
button { class: "btn btn-ghost", onclick: on_cancel_click, "Cancel" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue