pinakes-ui: streamline sidebar design

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0176fa480e5ba40eea5a39685a4f97896a6a6964
This commit is contained in:
raf 2026-02-03 10:25:31 +03:00
commit 278bcaa4b0
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
25 changed files with 1805 additions and 1686 deletions

View file

@ -148,7 +148,11 @@ pub fn Import(
}
}
},
if is_importing { "Importing..." } else { "Import" }
if is_importing {
"Importing..."
} else {
"Import"
}
}
}
}
@ -157,9 +161,9 @@ pub fn Import(
ImportOptions {
tags: tags.clone(),
collections: collections.clone(),
selected_tags: selected_tags,
new_tags_input: new_tags_input,
selected_collection: selected_collection,
selected_tags,
new_tags_input,
selected_collection,
}
}
@ -244,32 +248,48 @@ pub fn Import(
let min = *filter_min_size.read();
let max = *filter_max_size.read();
let filtered: Vec<&DirectoryPreviewFile> = preview_files.iter().filter(|f| {
let type_idx = match type_badge_class(&f.media_type) {
"type-audio" => 0,
"type-video" => 1,
"type-image" => 2,
"type-document" => 3,
"type-text" => 4,
_ => 5,
};
if !types_snapshot[type_idx] { return false; }
if min > 0 && f.file_size < min { return false; }
if max > 0 && f.file_size > max { return false; }
true
}).collect();
let filtered: Vec<&DirectoryPreviewFile> = preview_files
// Read selection once for display
.iter()
// Filter bar
// Selection toolbar
// Deselect all filtered
// Select all filtered
.filter(|f| {
let type_idx = match type_badge_class(&f.media_type) {
"type-audio" => 0,
"type-video" => 1,
"type-image" => 2,
"type-document" => 3,
"type-text" => 4,
_ => 5,
};
if !types_snapshot[type_idx] {
return false;
}
if min > 0 && f.file_size < min {
return false;
}
if max > 0 && f.file_size > max {
return false;
}
true
})
.collect();
let filtered_count = filtered.len();
let total_count = preview_files.len();
// Read selection once for display
let selection = selected_file_paths.read().clone();
let selected_count = selection.len();
let all_filtered_selected = !filtered.is_empty()
&& filtered.iter().all(|f| selection.contains(&f.path));
let filtered_paths: Vec<String> = filtered.iter().map(|f| f.path.clone()).collect();
let filtered_paths: Vec<String> = filtered
.iter()
.map(|f| f.path.clone())
.collect();
rsx! {
div { class: "card mb-16",
div { class: "card-header",
@ -279,7 +299,6 @@ pub fn Import(
}
}
// Filter bar
div { class: "filter-bar",
div { class: "flex-row mb-8",
label {
@ -383,8 +402,9 @@ pub fn Import(
}
}
// Selection toolbar
div { class: "flex-row mb-8", style: "gap: 8px; align-items: center; padding: 0 8px;",
div {
class: "flex-row mb-8",
style: "gap: 8px; align-items: center; padding: 0 8px;",
button {
class: "btn btn-sm btn-secondary",
onclick: {
@ -407,9 +427,7 @@ pub fn Import(
"Deselect All"
}
if selected_count > 0 {
span { class: "text-muted text-sm",
"{selected_count} files selected"
}
span { class: "text-muted text-sm", "{selected_count} files selected" }
}
}
@ -425,13 +443,17 @@ pub fn Import(
let filtered_paths = filtered_paths.clone();
move |_| {
if all_filtered_selected {
// Deselect all filtered
let filtered_set: HashSet<String> = filtered_paths.iter().cloned().collect();
let filtered_set: HashSet<String> = filtered_paths
.iter()
.cloned()
.collect();
let sel = selected_file_paths.read().clone();
let new_sel: HashSet<String> = sel.difference(&filtered_set).cloned().collect();
let new_sel: HashSet<String> = sel
.difference(&filtered_set)
.cloned()
.collect();
selected_file_paths.set(new_sel);
} else {
// Select all filtered
let mut sel = selected_file_paths.read().clone();
for p in &filtered_paths {
sel.insert(p.clone());
@ -455,9 +477,7 @@ pub fn Import(
let is_selected = selection.contains(&file.path);
let file_path_clone = file.path.clone();
rsx! {
tr {
key: "{file.path}",
class: if is_selected { "row-selected" } else { "" },
tr { key: "{file.path}", class: if is_selected { "row-selected" } else { "" },
td {
input {
r#type: "checkbox",
@ -496,9 +516,9 @@ pub fn Import(
ImportOptions {
tags: tags.clone(),
collections: collections.clone(),
selected_tags: selected_tags,
new_tags_input: new_tags_input,
selected_collection: selected_collection,
selected_tags,
new_tags_input,
selected_collection,
}
div { class: "flex-row mb-16", style: "gap: 8px;",
@ -516,7 +536,11 @@ pub fn Import(
let mut new_tags_input = new_tags_input;
let mut selected_collection = selected_collection;
move |_| {
let paths: Vec<String> = selected_file_paths.read().iter().cloned().collect();
let paths: Vec<String> = selected_file_paths
.read()
.iter()
.cloned()
.collect();
if !paths.is_empty() {
let tag_ids = selected_tags.read().clone();
let new_tags = parse_new_tags(&new_tags_input.read());
@ -565,7 +589,11 @@ pub fn Import(
}
}
},
if is_importing { "Importing..." } else { "Import Entire Directory" }
if is_importing {
"Importing..."
} else {
"Import Entire Directory"
}
}
}
}
@ -589,36 +617,37 @@ pub fn Import(
class: "btn btn-primary",
disabled: is_importing,
onclick: move |_| on_scan.call(()),
if is_importing { "Scanning..." } else { "Scan All Roots" }
if is_importing {
"Scanning..."
} else {
"Scan All Roots"
}
}
}
if let Some(ref progress) = scan_progress {
{
let pct = (progress.files_processed * 100).checked_div(progress.files_found).unwrap_or(0);
let pct = (progress.files_processed * 100)
.checked_div(progress.files_found)
.unwrap_or(0);
rsx! {
div { class: "mb-16",
div { class: "progress-bar",
div {
class: "progress-fill",
style: "width: {pct}%;",
div { class: "mb-16",
div { class: "progress-bar",
div { class: "progress-fill", style: "width: {pct}%;" }
}
p { class: "text-muted text-sm",
"{progress.files_processed} / {progress.files_found} files processed"
}
if progress.error_count > 0 {
p { class: "text-muted text-sm", "{progress.error_count} errors" }
}
if progress.scanning {
p { class: "text-muted text-sm", "Scanning..." }
} else {
p { class: "text-muted text-sm", "Scan complete" }
}
}
}
p { class: "text-muted text-sm",
"{progress.files_processed} / {progress.files_found} files processed"
}
if progress.error_count > 0 {
p { class: "text-muted text-sm",
"{progress.error_count} errors"
}
}
if progress.scanning {
p { class: "text-muted text-sm", "Scanning..." }
} else {
p { class: "text-muted text-sm", "Scan complete" }
}
}
}
}
}
}
@ -647,7 +676,9 @@ fn ImportOptions(
div { class: "form-group",
label { class: "form-label", "Tags" }
if tags.is_empty() {
p { class: "text-muted text-sm", "No tags available. Create tags from the Tags page." }
p { class: "text-muted text-sm",
"No tags available. Create tags from the Tags page."
}
} else {
div { class: "tag-list",
for tag in tags.iter() {
@ -655,11 +686,7 @@ fn ImportOptions(
let tag_id = tag.id.clone();
let tag_name = tag.name.clone();
let is_selected = selected_tags.read().contains(&tag_id);
let badge_class = if is_selected {
"tag-badge selected"
} else {
"tag-badge"
};
let badge_class = if is_selected { "tag-badge selected" } else { "tag-badge" };
rsx! {
span {
class: "{badge_class}",
@ -693,7 +720,9 @@ fn ImportOptions(
value: "{new_tags_input}",
oninput: move |e| new_tags_input.set(e.value()),
}
p { class: "text-muted text-sm", "Comma-separated. Will be created if they don't exist." }
p { class: "text-muted text-sm",
"Comma-separated. Will be created if they don't exist."
}
}
div { class: "form-group",