pinakes/crates/pinakes-ui/src/components/collections.rs
NotAShelf 3ccddce7fd
treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
2026-03-06 18:29:33 +03:00

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"
}
}
}
}
}
}
}
}
}
}
}
}
}