initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
334
crates/pinakes-ui/src/components/collections.rs
Normal file
334
crates/pinakes-ui/src/components/collections.rs
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue