Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
323 lines
14 KiB
Rust
323 lines
14 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use super::utils::{format_size, type_badge_class};
|
|
use crate::client::{CollectionResponse, MediaResponse};
|
|
|
|
#[component]
|
|
pub fn Collections(
|
|
collections: Vec<CollectionResponse>,
|
|
collection_members: Vec<MediaResponse>,
|
|
viewing_collection: Option<String>,
|
|
on_create: EventHandler<(String, String, Option<String>, Option<String>)>,
|
|
on_delete: EventHandler<String>,
|
|
on_view_members: EventHandler<String>,
|
|
on_back_to_list: EventHandler<()>,
|
|
on_remove_member: EventHandler<(String, String)>,
|
|
on_select: EventHandler<String>,
|
|
on_add_member: EventHandler<(String, String)>,
|
|
all_media: Vec<MediaResponse>,
|
|
) -> Element {
|
|
let mut new_name = use_signal(String::new);
|
|
let mut new_kind = use_signal(|| String::from("manual"));
|
|
let mut new_description = use_signal(String::new);
|
|
let mut new_filter_query = use_signal(String::new);
|
|
let mut confirm_delete: Signal<Option<String>> = use_signal(|| None);
|
|
let mut show_add_modal = use_signal(|| false);
|
|
|
|
// Detail view: viewing a specific collection's members
|
|
if let Some(ref col_id) = viewing_collection {
|
|
let col_name = collections
|
|
.iter()
|
|
.find(|c| &c.id == col_id)
|
|
.map(|c| c.name.clone())
|
|
.unwrap_or_else(|| col_id.clone());
|
|
|
|
let back_click = move |_| on_back_to_list.call(());
|
|
|
|
// Collect IDs of current members to filter available media
|
|
let member_ids: Vec<String> =
|
|
collection_members.iter().map(|m| m.id.clone()).collect();
|
|
let available_media: Vec<&MediaResponse> = all_media
|
|
.iter()
|
|
.filter(|m| !member_ids.contains(&m.id))
|
|
.collect();
|
|
|
|
let modal_col_id = col_id.clone();
|
|
|
|
return rsx! {
|
|
button { class: "btn btn-ghost mb-16", onclick: back_click, "\u{2190} Back to Collections" }
|
|
|
|
h3 { class: "mb-16", "{col_name}" }
|
|
|
|
div { class: "form-row mb-16",
|
|
button {
|
|
class: "btn btn-primary",
|
|
onclick: move |_| show_add_modal.set(true),
|
|
"Add Media"
|
|
}
|
|
}
|
|
|
|
if collection_members.is_empty() {
|
|
div { class: "empty-state",
|
|
p { class: "empty-subtitle", "This collection has no members." }
|
|
}
|
|
} else {
|
|
table { class: "data-table",
|
|
thead {
|
|
tr {
|
|
th { "Name" }
|
|
th { "Type" }
|
|
th { "Artist" }
|
|
th { "Size" }
|
|
th { "" }
|
|
}
|
|
}
|
|
tbody {
|
|
for item in collection_members.iter() {
|
|
{
|
|
let artist = item.artist.clone().unwrap_or_default();
|
|
let size = format_size(item.file_size);
|
|
let badge_class = type_badge_class(&item.media_type);
|
|
let remove_cid = col_id.clone();
|
|
let remove_mid = item.id.clone();
|
|
let row_click = {
|
|
let mid = item.id.clone();
|
|
move |_| on_select.call(mid.clone())
|
|
};
|
|
rsx! {
|
|
tr { key: "{item.id}", class: "clickable-row", onclick: row_click,
|
|
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 |e: Event<MouseData>| {
|
|
e.stop_propagation();
|
|
on_remove_member.call((remove_cid.clone(), remove_mid.clone()));
|
|
},
|
|
"Remove"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Media modal
|
|
if *show_add_modal.read() {
|
|
div {
|
|
class: "modal-overlay",
|
|
onclick: move |_| show_add_modal.set(false),
|
|
div {
|
|
class: "modal",
|
|
onclick: move |e: Event<MouseData>| e.stop_propagation(),
|
|
div { class: "modal-header",
|
|
h3 { "Add Media to Collection" }
|
|
button {
|
|
class: "btn btn-ghost",
|
|
onclick: move |_| show_add_modal.set(false),
|
|
"\u{2715}"
|
|
}
|
|
}
|
|
div { class: "modal-body",
|
|
if available_media.is_empty() {
|
|
p { "No media available to add." }
|
|
} else {
|
|
table { class: "data-table",
|
|
thead {
|
|
tr {
|
|
th { "Name" }
|
|
th { "Type" }
|
|
th { "Artist" }
|
|
}
|
|
}
|
|
tbody {
|
|
for media in available_media.iter() {
|
|
{
|
|
let artist = media.artist.clone().unwrap_or_default();
|
|
let badge_class = type_badge_class(&media.media_type);
|
|
let add_click = {
|
|
let cid = modal_col_id.clone();
|
|
let mid = media.id.clone();
|
|
move |_| {
|
|
on_add_member.call((cid.clone(), mid.clone()));
|
|
show_add_modal.set(false);
|
|
}
|
|
};
|
|
rsx! {
|
|
tr { key: "{media.id}", class: "clickable-row", onclick: add_click,
|
|
td { "{media.file_name}" }
|
|
td {
|
|
span { class: "type-badge {badge_class}", "{media.media_type}" }
|
|
}
|
|
td { "{artist}" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// List view: show all collections with create form
|
|
let is_virtual = *new_kind.read() == "virtual";
|
|
|
|
let create_click = move |_| {
|
|
let name = new_name.read().clone();
|
|
if name.is_empty() {
|
|
return;
|
|
}
|
|
let kind = new_kind.read().clone();
|
|
let desc = {
|
|
let d = new_description.read().clone();
|
|
if d.is_empty() { None } else { Some(d) }
|
|
};
|
|
let filter = {
|
|
let f = new_filter_query.read().clone();
|
|
if f.is_empty() { None } else { Some(f) }
|
|
};
|
|
on_create.call((name, kind, desc, filter));
|
|
new_name.set(String::new());
|
|
new_kind.set(String::from("manual"));
|
|
new_description.set(String::new());
|
|
new_filter_query.set(String::new());
|
|
};
|
|
|
|
rsx! {
|
|
div { class: "card",
|
|
div { class: "card-header",
|
|
h3 { class: "card-title", "Collections" }
|
|
}
|
|
|
|
div { class: "form-row mb-16",
|
|
input {
|
|
r#type: "text",
|
|
placeholder: "Collection name...",
|
|
value: "{new_name}",
|
|
oninput: move |e| new_name.set(e.value()),
|
|
}
|
|
select {
|
|
value: "{new_kind}",
|
|
onchange: move |e| new_kind.set(e.value()),
|
|
option { value: "manual", "Manual" }
|
|
option { value: "virtual", "Virtual" }
|
|
}
|
|
input {
|
|
r#type: "text",
|
|
placeholder: "Description (optional)...",
|
|
value: "{new_description}",
|
|
oninput: move |e| new_description.set(e.value()),
|
|
}
|
|
}
|
|
|
|
if is_virtual {
|
|
div { class: "form-row mb-16",
|
|
input {
|
|
r#type: "text",
|
|
placeholder: "Filter query for virtual collection...",
|
|
value: "{new_filter_query}",
|
|
oninput: move |e| new_filter_query.set(e.value()),
|
|
}
|
|
}
|
|
}
|
|
|
|
div { class: "form-row mb-16",
|
|
button { class: "btn btn-primary", onclick: create_click, "Create" }
|
|
}
|
|
|
|
if collections.is_empty() {
|
|
div { class: "empty-state",
|
|
p { class: "empty-subtitle", "No collections yet. Create one above." }
|
|
}
|
|
} else {
|
|
table { class: "data-table",
|
|
thead {
|
|
tr {
|
|
th { "Name" }
|
|
th { "Kind" }
|
|
th { "Description" }
|
|
th { "" }
|
|
th { "" }
|
|
}
|
|
}
|
|
tbody {
|
|
for col in collections.iter() {
|
|
{
|
|
let desc = col.description.clone().unwrap_or_default();
|
|
let kind_class = if col.kind == "virtual" {
|
|
"type-document"
|
|
} else {
|
|
"type-other"
|
|
};
|
|
let view_click = {
|
|
let id = col.id.clone();
|
|
move |_| on_view_members.call(id.clone())
|
|
};
|
|
let col_id_for_delete = col.id.clone();
|
|
let is_confirming = confirm_delete
|
|
.read()
|
|
.as_ref()
|
|
.map(|id| id == &col.id)
|
|
.unwrap_or(false);
|
|
rsx! {
|
|
tr { key: "{col.id}",
|
|
td { "{col.name}" }
|
|
td {
|
|
span { class: "type-badge {kind_class}", "{col.kind}" }
|
|
}
|
|
td { "{desc}" }
|
|
td {
|
|
button { class: "btn btn-sm btn-secondary", onclick: view_click, "View" }
|
|
}
|
|
td {
|
|
if is_confirming {
|
|
button {
|
|
class: "btn btn-danger btn-sm",
|
|
onclick: {
|
|
let id = col_id_for_delete.clone();
|
|
move |_| {
|
|
on_delete.call(id.clone());
|
|
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 = col_id_for_delete.clone();
|
|
move |_| confirm_delete.set(Some(id.clone()))
|
|
},
|
|
"Delete"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|