Add visual format cards, per-image remove, shortcuts dialog, wire threads

Convert step: replace ComboRow with visual format card grid showing
icon, name, and description for each format. Much more beginner-friendly.

Images step: add per-image remove button on each file row so users
can exclude individual images from the batch.

Shortcuts: use adw::Dialog with structured layout since GtkShortcutsWindow
is deprecated in GTK 4.18+. Add file management and undo shortcuts.

Settings: wire thread count selection to actually save/restore the
ThreadCount config value instead of always defaulting to Auto.
This commit is contained in:
2026-03-06 12:58:43 +02:00
parent 29770be8b5
commit 8f6e4382c4
4 changed files with 207 additions and 58 deletions

View File

@@ -28,7 +28,6 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
&& let Some(path) = file.path()
{
if path.is_dir() {
// Recursively add images from directory
let mut files = loaded_files.borrow_mut();
add_images_from_dir(&path, &mut files);
let count = files.len();
@@ -106,11 +105,16 @@ fn update_count_and_list(
.sum();
let size_str = format_size(total_size);
// Walk widget tree to find and update components
walk_loaded_widgets(widget, count, &size_str, &files);
walk_loaded_widgets(widget, count, &size_str, &files, loaded_files);
}
fn walk_loaded_widgets(widget: &gtk::Widget, count: usize, size_str: &str, files: &[std::path::PathBuf]) {
fn walk_loaded_widgets(
widget: &gtk::Widget,
count: usize,
size_str: &str,
files: &[std::path::PathBuf],
loaded_files: &std::rc::Rc<std::cell::RefCell<Vec<std::path::PathBuf>>>,
) {
if let Some(label) = widget.downcast_ref::<gtk::Label>()
&& label.css_classes().iter().any(|c| c == "heading")
{
@@ -123,8 +127,8 @@ fn walk_loaded_widgets(widget: &gtk::Widget, count: usize, size_str: &str, files
while let Some(row) = list_box.first_child() {
list_box.remove(&row);
}
// Add rows for each file
for path in files {
// Add rows for each file with remove button
for (idx, path) in files.iter().enumerate() {
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
@@ -140,13 +144,45 @@ fn walk_loaded_widgets(widget: &gtk::Widget, count: usize, size_str: &str, files
.subtitle(format!("{} - {}", ext, size))
.build();
row.add_prefix(&gtk::Image::from_icon_name("image-x-generic-symbolic"));
// Per-image remove button
let remove_btn = gtk::Button::builder()
.icon_name("list-remove-symbolic")
.tooltip_text("Remove this image from batch")
.valign(gtk::Align::Center)
.build();
remove_btn.add_css_class("flat");
{
let loaded = loaded_files.clone();
let list = list_box.clone();
let file_idx = idx;
remove_btn.connect_clicked(move |_btn| {
let mut files = loaded.borrow_mut();
if file_idx < files.len() {
files.remove(file_idx);
}
let count = files.len();
drop(files);
// Refresh by finding the parent stack
if let Some(parent) = list.ancestor(gtk::Stack::static_type())
&& let Some(stack) = parent.downcast_ref::<gtk::Stack>()
{
if count == 0 {
stack.set_visible_child_name("empty");
} else if let Some(loaded_widget) = stack.child_by_name("loaded") {
update_count_and_list(&loaded_widget, &loaded);
}
}
});
}
row.add_suffix(&remove_btn);
list_box.append(&row);
}
}
// Recurse
let mut child = widget.first_child();
while let Some(c) = child {
walk_loaded_widgets(&c, count, size_str, files);
walk_loaded_widgets(&c, count, size_str, files, loaded_files);
child = c.next_sibling();
}
}