use dioxus::prelude::*; use super::utils::{format_size, format_timestamp}; use crate::client::DuplicateGroupResponse; #[component] pub fn Duplicates( groups: Vec, server_url: String, on_delete: EventHandler, on_refresh: EventHandler<()>, ) -> Element { let mut expanded_group = use_signal(|| Option::::None); let mut confirm_delete = use_signal(|| Option::::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" } } } } } } } } } } } } } } } }