use dioxus::prelude::*; use super::utils::{format_size, type_badge_class}; use crate::client::{ApiClient, MediaResponse, PlaylistResponse}; #[component] pub fn Playlists(client: Signal) -> Element { let mut playlists: Signal> = use_signal(Vec::new); let mut items: Signal> = use_signal(Vec::new); let mut selected_id: Signal> = use_signal(|| None); let mut loading = use_signal(|| false); let mut error: Signal> = use_signal(|| None); let mut new_name = use_signal(String::new); let mut new_desc = use_signal(String::new); let mut confirm_delete: Signal> = use_signal(|| None); let mut renaming_id: Signal> = use_signal(|| None); let mut rename_input = use_signal(String::new); let mut add_media_id = use_signal(String::new); // Load playlists on mount use_effect(move || { spawn(async move { loading.set(true); match client.read().list_playlists().await { Ok(list) => playlists.set(list), Err(e) => error.set(Some(format!("Failed to load playlists: {e}"))), } loading.set(false); }); }); let load_items = move |pid: String| { spawn(async move { match client.read().get_playlist_items(&pid).await { Ok(list) => items.set(list), Err(e) => error.set(Some(format!("Failed to load items: {e}"))), } }); }; let on_create = move |_| { let name = new_name.read().clone(); if name.is_empty() { return; } let desc = { let d = new_desc.read().clone(); if d.is_empty() { None } else { Some(d) } }; spawn(async move { match client.read().create_playlist(&name, desc.as_deref()).await { Ok(pl) => { playlists.write().push(pl); new_name.set(String::new()); new_desc.set(String::new()); }, Err(e) => error.set(Some(format!("Failed to create playlist: {e}"))), } }); }; let on_shuffle = move |pid: String| { spawn(async move { match client.read().shuffle_playlist(&pid).await { Ok(_) => { // Reload items if this is the selected playlist if selected_id.read().as_deref() == Some(&pid) { match client.read().get_playlist_items(&pid).await { Ok(list) => items.set(list), Err(e) => error.set(Some(format!("Failed to reload: {e}"))), } } }, Err(e) => error.set(Some(format!("Shuffle failed: {e}"))), } }); }; // Detail view: show items for selected playlist if let Some(ref pid) = selected_id.read().clone() { let pl_name = playlists .read() .iter() .find(|p| &p.id == pid) .map(|p| p.name.clone()) .unwrap_or_else(|| pid.clone()); let pid_for_shuffle = pid.clone(); let pid_for_back = pid.clone(); return rsx! { div { class: "form-row mb-16", button { class: "btn btn-ghost", onclick: move |_| { let _ = &pid_for_back; selected_id.set(None); items.set(Vec::new()); }, "\u{2190} Back to Playlists" } } h3 { class: "mb-16", "{pl_name}" } if let Some(ref err) = *error.read() { div { class: "error-banner", "{err}" } } div { class: "form-row mb-16", button { class: "btn btn-secondary btn-sm", onclick: move |_| on_shuffle(pid_for_shuffle.clone()), "\u{1f500} Shuffle" } } div { class: "form-row mb-16", input { r#type: "text", placeholder: "Media ID to add...", value: "{add_media_id}", oninput: move |e| add_media_id.set(e.value()), } button { class: "btn btn-primary btn-sm", onclick: { let pid = pid.clone(); move |_| { let mid = add_media_id.read().clone(); if mid.is_empty() { return; } let pid = pid.clone(); spawn(async move { match client.read().add_to_playlist(&pid, &mid).await { Ok(_) => { add_media_id.set(String::new()); match client.read().get_playlist_items(&pid).await { Ok(list) => items.set(list), Err(e) => error.set(Some(format!("Reload failed: {e}"))), } } Err(e) => error.set(Some(format!("Add failed: {e}"))), } }); } }, "Add Media" } } if *loading.read() { div { class: "loading-overlay", div { class: "spinner" } "Loading..." } } else if items.read().is_empty() { div { class: "empty-state", p { class: "empty-subtitle", "No items in this playlist." } } } else { table { class: "data-table", thead { tr { th { "Name" } th { "Type" } th { "Artist" } th { "Size" } th { "" } } } tbody { for item in items.read().clone() { { let pid_rm = pid.clone(); let mid = item.id.clone(); let artist = item.artist.clone().unwrap_or_default(); let size = format_size(item.file_size); let badge_class = type_badge_class(&item.media_type); rsx! { tr { key: "{mid}", td { "{item.file_name}" } td { span { class: "type-badge {badge_class}", "{item.media_type}" } } td { "{artist}" } td { "{size}" } td { button { class: "btn btn-danger btn-sm", onclick: move |_| { let pid_rm = pid_rm.clone(); let mid = mid.clone(); spawn(async move { match client.read().remove_from_playlist(&pid_rm, &mid).await { Ok(_) => { match client.read().get_playlist_items(&pid_rm).await { Ok(list) => items.set(list), Err(e) => error.set(Some(format!("Reload failed: {e}"))), } } Err(e) => error.set(Some(format!("Remove failed: {e}"))), } }); }, "Remove" } } } } } } } } } }; } // List view rsx! { div { class: "card", div { class: "card-header", h3 { class: "card-title", "Playlists" } } if let Some(ref err) = *error.read() { div { class: "error-banner mb-16", "{err}" } } div { class: "form-row mb-16", input { r#type: "text", placeholder: "Playlist name...", value: "{new_name}", oninput: move |e| new_name.set(e.value()), } input { r#type: "text", placeholder: "Description (optional)...", value: "{new_desc}", oninput: move |e| new_desc.set(e.value()), } button { class: "btn btn-primary", onclick: on_create, "Create" } } if *loading.read() { div { class: "loading-overlay", div { class: "spinner" } "Loading..." } } else if playlists.read().is_empty() { div { class: "empty-state", p { class: "empty-subtitle", "No playlists yet. Create one above." } } } else { table { class: "data-table", thead { tr { th { "Name" } th { "Description" } th { "Items" } th { "" } th { "" } th { "" } } } tbody { for pl in playlists.read().clone() { { let pl_id = pl.id.clone(); let pl_id_open = pl.id.clone(); let pl_id_rename = pl.id.clone(); let desc = pl.description.clone().unwrap_or_default(); let count = pl.item_count.map(|c| c.to_string()).unwrap_or_default(); let is_confirming = confirm_delete .read() .as_ref() .map(|id| id == &pl.id) .unwrap_or(false); let is_renaming = renaming_id .read() .as_ref() .map(|id| id == &pl.id) .unwrap_or(false); rsx! { tr { key: "{pl_id}", td { if is_renaming { div { class: "form-row", input { r#type: "text", value: "{rename_input}", oninput: move |e| rename_input.set(e.value()), } button { class: "btn btn-primary btn-sm", onclick: { let rid = pl_id_rename.clone(); move |_| { let rid = rid.clone(); let new_name_val = rename_input.read().clone(); if new_name_val.is_empty() { return; } spawn(async move { match client.read().update_playlist(&rid, &new_name_val).await { Ok(updated) => { let mut list = playlists.write(); if let Some(p) = list.iter_mut().find(|p| p.id == rid) { *p = updated; } renaming_id.set(None); } Err(e) => error.set(Some(format!("Rename failed: {e}"))), } }); } }, "Save" } button { class: "btn btn-ghost btn-sm", onclick: move |_| renaming_id.set(None), "Cancel" } } } else { "{pl.name}" } } td { "{desc}" } td { "{count}" } td { button { class: "btn btn-sm btn-secondary", onclick: move |_| { let pid = pl_id_open.clone(); selected_id.set(Some(pid.clone())); load_items(pid); }, "Open" } } td { if !is_renaming { button { class: "btn btn-sm btn-ghost", onclick: { let rid = pl_id.clone(); let rname = pl.name.clone(); move |_| { rename_input.set(rname.clone()); renaming_id.set(Some(rid.clone())); } }, "Rename" } } } td { if is_confirming { button { class: "btn btn-danger btn-sm", onclick: { let id = pl_id.clone(); move |_| { let id = id.clone(); spawn(async move { match client.read().delete_playlist(&id).await { Ok(_) => { playlists.write().retain(|p| p.id != id); confirm_delete.set(None); } Err(e) => { error.set(Some(format!("Delete failed: {e}"))); confirm_delete.set(None); } } }); } }, "Confirm" } button { class: "btn btn-ghost btn-sm", onclick: move |_| confirm_delete.set(None), "Cancel" } } else { button { class: "btn btn-danger btn-sm", onclick: { let id = pl_id.clone(); move |_| confirm_delete.set(Some(id.clone())) }, "Delete" } } } } } } } } } } } } }