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
170
crates/pinakes-ui/src/components/duplicates.rs
Normal file
170
crates/pinakes-ui/src/components/duplicates.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
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.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}",
|
||||
|
||||
// Group header
|
||||
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}"
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded: show items
|
||||
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}",
|
||||
|
||||
// Thumbnail
|
||||
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}" }
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
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)}" }
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue