pinakes: import in parallel; various UI improvements

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1eb47cd79cd4145c56af966f6756fe1d6a6a6964
This commit is contained in:
raf 2026-02-03 10:31:20 +03:00
commit 116fe7b059
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
42 changed files with 4316 additions and 316 deletions

View file

@ -23,6 +23,10 @@ pub fn Import(
preview_total_size: u64,
scan_progress: Option<ScanStatusResponse>,
#[props(default = false)] is_importing: bool,
// Extended import state
#[props(default)] current_file: Option<String>,
#[props(default)] import_queue: Vec<String>,
#[props(default = (0, 0))] import_progress: (usize, usize),
) -> Element {
let mut import_mode = use_signal(|| 0usize);
let mut file_path = use_signal(String::new);
@ -47,13 +51,45 @@ pub fn Import(
rsx! {
// Import status panel (shown when import is in progress)
if is_importing {
div { class: "import-status-panel",
div { class: "import-status-header",
div { class: "status-dot checking" }
span { "Import in progress..." }
}
div { class: "progress-bar",
div { class: "progress-fill indeterminate" }
{
let (completed, total) = import_progress;
let has_progress = total > 0;
let pct = if total > 0 { (completed * 100) / total } else { 0 };
let queue_count = import_queue.len();
rsx! {
div { class: "import-status-panel",
div { class: "import-status-header",
div { class: "status-dot checking" }
span {
if has_progress {
"Importing {completed}/{total}..."
} else {
"Import in progress..."
}
}
}
// Show current file being imported
if let Some(ref file_name) = current_file {
div { class: "import-current-file",
span { class: "import-file-label", "Current: " }
span { class: "import-file-name", "{file_name}" }
}
}
// Show queue indicator
if queue_count > 0 {
div { class: "import-queue-indicator",
span { class: "import-queue-badge", "{queue_count}" }
span { class: "import-queue-text", " item(s) queued" }
}
}
div { class: "progress-bar",
if has_progress {
div { class: "progress-fill", style: "width: {pct}%;" }
} else {
div { class: "progress-fill indeterminate" }
}
}
}
}
}
}
@ -229,13 +265,13 @@ pub fn Import(
// Recursive toggle
div { class: "form-group",
label { class: "form-row",
label { class: "checkbox-label",
input {
r#type: "checkbox",
checked: *recursive.read(),
onchange: move |_| recursive.toggle(),
}
span { style: "margin-left: 6px;", "Recursive (include subdirectories)" }
span { "Recursive (include subdirectories)" }
}
}
}
@ -299,9 +335,12 @@ pub fn Import(
}
}
div { class: "filter-bar",
div { class: "flex-row mb-8",
label {
div { class: "filter-row",
span { class: "filter-label", "Types" }
label { class: if types_snapshot[0] { "filter-chip active" } else { "filter-chip" },
input {
r#type: "checkbox",
checked: types_snapshot[0],
@ -311,9 +350,9 @@ pub fn Import(
filter_types.set(types);
},
}
" Audio"
"Audio"
}
label {
label { class: if types_snapshot[1] { "filter-chip active" } else { "filter-chip" },
input {
r#type: "checkbox",
checked: types_snapshot[1],
@ -323,9 +362,9 @@ pub fn Import(
filter_types.set(types);
},
}
" Video"
"Video"
}
label {
label { class: if types_snapshot[2] { "filter-chip active" } else { "filter-chip" },
input {
r#type: "checkbox",
checked: types_snapshot[2],
@ -335,9 +374,9 @@ pub fn Import(
filter_types.set(types);
},
}
" Image"
"Image"
}
label {
label { class: if types_snapshot[3] { "filter-chip active" } else { "filter-chip" },
input {
r#type: "checkbox",
checked: types_snapshot[3],
@ -347,9 +386,9 @@ pub fn Import(
filter_types.set(types);
},
}
" Document"
"Document"
}
label {
label { class: if types_snapshot[4] { "filter-chip active" } else { "filter-chip" },
input {
r#type: "checkbox",
checked: types_snapshot[4],
@ -359,9 +398,9 @@ pub fn Import(
filter_types.set(types);
},
}
" Text"
"Text"
}
label {
label { class: if types_snapshot[5] { "filter-chip active" } else { "filter-chip" },
input {
r#type: "checkbox",
checked: types_snapshot[5],
@ -371,33 +410,41 @@ pub fn Import(
filter_types.set(types);
},
}
" Other"
"Other"
}
}
div { class: "flex-row",
label { class: "form-label", "Min size (MB): " }
input {
r#type: "number",
value: "{min / (1024 * 1024)}",
oninput: move |e| {
if let Ok(mb) = e.value().parse::<u64>() {
filter_min_size.set(mb * 1024 * 1024);
} else {
filter_min_size.set(0);
}
},
div { class: "size-filters",
div { class: "size-filter-group",
label { "Min size" }
input {
r#type: "number",
placeholder: "MB",
value: if min > 0 { format!("{}", min / (1024 * 1024)) } else { String::new() },
oninput: move |e| {
if let Ok(mb) = e.value().parse::<u64>() {
filter_min_size.set(mb * 1024 * 1024);
} else {
filter_min_size.set(0);
}
},
}
span { class: "text-muted text-sm", "MB" }
}
label { class: "form-label", "Max size (MB): " }
input {
r#type: "number",
value: "{max / (1024 * 1024)}",
oninput: move |e| {
if let Ok(mb) = e.value().parse::<u64>() {
filter_max_size.set(mb * 1024 * 1024);
} else {
filter_max_size.set(0);
}
},
div { class: "size-filter-group",
label { "Max size" }
input {
r#type: "number",
placeholder: "MB",
value: if max > 0 { format!("{}", max / (1024 * 1024)) } else { String::new() },
oninput: move |e| {
if let Ok(mb) = e.value().parse::<u64>() {
filter_max_size.set(mb * 1024 * 1024);
} else {
filter_max_size.set(0);
}
},
}
span { class: "text-muted text-sm", "MB" }
}
}
}
@ -565,34 +612,46 @@ pub fn Import(
}
// Import entire directory
button {
class: "btn btn-secondary",
disabled: is_importing,
onclick: {
let mut dir_path = dir_path;
let mut selected_tags = selected_tags;
let mut new_tags_input = new_tags_input;
let mut selected_collection = selected_collection;
let mut selected_file_paths = selected_file_paths;
move |_| {
let path = dir_path.read().clone();
if !path.is_empty() {
let tag_ids = selected_tags.read().clone();
let new_tags = parse_new_tags(&new_tags_input.read());
let col_id = selected_collection.read().clone();
on_import_directory.call((path, tag_ids, new_tags, col_id));
dir_path.set(String::new());
selected_tags.set(Vec::new());
new_tags_input.set(String::new());
selected_collection.set(None);
selected_file_paths.set(HashSet::new());
{
let has_dir = !dir_path.read().is_empty();
let has_preview = !preview_files.is_empty();
let file_count = preview_files.len();
rsx! {
button {
class: if has_dir { "btn btn-secondary" } else { "btn btn-secondary btn-disabled-hint" },
disabled: is_importing || !has_dir,
title: if !has_dir { "Select a directory first" } else { "" },
onclick: {
let mut dir_path = dir_path;
let mut selected_tags = selected_tags;
let mut new_tags_input = new_tags_input;
let mut selected_collection = selected_collection;
let mut selected_file_paths = selected_file_paths;
move |_| {
let path = dir_path.read().clone();
if !path.is_empty() {
let tag_ids = selected_tags.read().clone();
let new_tags = parse_new_tags(&new_tags_input.read());
let col_id = selected_collection.read().clone();
on_import_directory.call((path, tag_ids, new_tags, col_id));
dir_path.set(String::new());
selected_tags.set(Vec::new());
new_tags_input.set(String::new());
selected_collection.set(None);
selected_file_paths.set(HashSet::new());
}
}
},
if is_importing {
"Importing..."
} else if has_preview {
"Import All ({file_count} files)"
} else if has_dir {
"Import Entire Directory"
} else {
"Select Directory First"
}
}
},
if is_importing {
"Importing..."
} else {
"Import Entire Directory"
}
}
}