Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I1eb47cd79cd4145c56af966f6756fe1d6a6a6964
181 lines
9 KiB
Rust
181 lines
9 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use super::utils::{format_size, format_timestamp};
|
|
use crate::client::DuplicateGroupResponse;
|
|
|
|
#[component]
|
|
pub fn Duplicates(
|
|
groups: Vec<DuplicateGroupResponse>,
|
|
server_url: String,
|
|
on_delete: EventHandler<String>,
|
|
on_refresh: EventHandler<()>,
|
|
) -> Element {
|
|
let mut expanded_group = use_signal(|| Option::<String>::None);
|
|
let mut confirm_delete = use_signal(|| Option::<String>::None);
|
|
|
|
let total_groups = groups.len();
|
|
let total_duplicates: usize = groups.iter().map(|g| g.items.len().saturating_sub(1)).sum();
|
|
|
|
rsx! {
|
|
div { class: "duplicates-view",
|
|
div { class: "duplicates-header",
|
|
h3 { "Duplicates" }
|
|
div { class: "duplicates-summary",
|
|
span { class: "text-muted",
|
|
"{total_groups} group(s), {total_duplicates} duplicate(s)"
|
|
}
|
|
button {
|
|
class: "btn btn-sm btn-secondary",
|
|
onclick: move |_| on_refresh.call(()),
|
|
"Refresh"
|
|
}
|
|
}
|
|
}
|
|
|
|
if groups.is_empty() {
|
|
div { class: "empty-state",
|
|
p { class: "text-muted", "No duplicate files found." }
|
|
}
|
|
}
|
|
|
|
for group in groups.iter() {
|
|
{
|
|
let hash = group.content_hash.clone();
|
|
let is_expanded = expanded_group.read().as_ref() == Some(&hash);
|
|
let hash_for_toggle = hash.clone();
|
|
let item_count = group.items.len();
|
|
let first_name = group
|
|
.items
|
|
|
|
// Group header
|
|
|
|
// Expanded: show items
|
|
|
|
// Thumbnail
|
|
|
|
// Info
|
|
|
|
// Actions
|
|
|
|
.first()
|
|
.map(|i| i.file_name.clone())
|
|
.unwrap_or_default();
|
|
let total_size: u64 = group.items.iter().map(|i| i.file_size).sum();
|
|
let short_hash = if hash.len() > 12 {
|
|
format!("{}...", &hash[..12])
|
|
} else {
|
|
hash.clone()
|
|
};
|
|
rsx! {
|
|
div { class: "duplicate-group", key: "{hash}",
|
|
|
|
|
|
|
|
button {
|
|
class: "duplicate-group-header",
|
|
onclick: move |_| {
|
|
let current = expanded_group.read().clone();
|
|
if current.as_ref() == Some(&hash_for_toggle) {
|
|
expanded_group.set(None);
|
|
} else {
|
|
expanded_group.set(Some(hash_for_toggle.clone()));
|
|
}
|
|
},
|
|
span { class: "expand-icon",
|
|
if is_expanded {
|
|
"\u{25bc}"
|
|
} else {
|
|
"\u{25b6}"
|
|
}
|
|
}
|
|
span { class: "group-name", "{first_name}" }
|
|
span { class: "group-badge", "{item_count} files" }
|
|
span { class: "group-size text-muted", "{format_size(total_size)}" }
|
|
span { class: "group-hash mono text-muted", "{short_hash}" }
|
|
}
|
|
|
|
if is_expanded {
|
|
div { class: "duplicate-items",
|
|
for (idx , item) in group.items.iter().enumerate() {
|
|
{
|
|
let item_id = item.id.clone();
|
|
let is_first = idx == 0;
|
|
let is_confirming = confirm_delete.read().as_ref() == Some(&item_id);
|
|
let thumb_url = format!("{}/api/v1/media/{}/thumbnail", server_url, item.id);
|
|
let has_thumb = item.has_thumbnail;
|
|
|
|
rsx! {
|
|
div {
|
|
class: if is_first { "duplicate-item duplicate-item-keep" } else { "duplicate-item" },
|
|
key: "{item_id}",
|
|
|
|
|
|
|
|
div { class: "dup-thumb",
|
|
if has_thumb {
|
|
img {
|
|
src: "{thumb_url}",
|
|
alt: "{item.file_name}",
|
|
class: "dup-thumb-img",
|
|
}
|
|
} else {
|
|
div { class: "dup-thumb-placeholder", "\u{1f5bc}" }
|
|
}
|
|
}
|
|
|
|
div { class: "dup-info",
|
|
div { class: "dup-filename", "{item.file_name}" }
|
|
div { class: "dup-path mono text-muted", "{item.path}" }
|
|
div { class: "dup-meta",
|
|
span { "{format_size(item.file_size)}" }
|
|
span { class: "text-muted", " | " }
|
|
span { "{format_timestamp(&item.created_at)}" }
|
|
}
|
|
}
|
|
|
|
div { class: "dup-actions",
|
|
if is_first {
|
|
span { class: "keep-badge", "Keep" }
|
|
}
|
|
|
|
if is_confirming {
|
|
button {
|
|
class: "btn btn-sm btn-danger",
|
|
onclick: {
|
|
let id = item_id.clone();
|
|
move |_| {
|
|
confirm_delete.set(None);
|
|
on_delete.call(id.clone());
|
|
}
|
|
},
|
|
"Confirm"
|
|
}
|
|
button {
|
|
class: "btn btn-sm btn-ghost",
|
|
onclick: move |_| confirm_delete.set(None),
|
|
"Cancel"
|
|
}
|
|
} else if !is_first {
|
|
button {
|
|
class: "btn btn-sm btn-danger",
|
|
onclick: {
|
|
let id = item_id.clone();
|
|
move |_| confirm_delete.set(Some(id.clone()))
|
|
},
|
|
"Delete"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|