Improve UX, add popover tour, metadata, and hicolor icons

- Redesign tutorial tour from modal dialogs to popovers pointing at actual UI elements
- Add beginner-friendly improvements: help buttons, tooltips, welcome wizard enhancements
- Add AppStream metainfo with screenshots, branding, categories, keywords, provides
- Update desktop file with GTK category and SingleMainWindow
- Add hicolor icon theme with all sizes (16-512px)
- Fix debounce SourceId panic in rename step
- Various step UI improvements and bug fixes
This commit is contained in:
2026-03-08 14:18:15 +02:00
parent 8d754017fa
commit f3668c45c3
26 changed files with 2292 additions and 473 deletions

View File

@@ -38,6 +38,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
.title("Enable Compression")
.subtitle("Reduce file size with quality control")
.active(cfg.compress_enabled)
.tooltip_text("Toggle compression on or off")
.build();
let enable_group = adw::PreferencesGroup::new();
@@ -234,6 +235,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
let compressed_pixbuf: Rc<RefCell<Option<gtk::gdk_pixbuf::Pixbuf>>> = Rc::new(RefCell::new(None));
let divider_dragging = Rc::new(Cell::new(false));
let image_dragging = Rc::new(Cell::new(false));
let divider_hint_visible = Rc::new(Cell::new(true));
// Pan state for cover-fill preview
let pan_x: Rc<Cell<f64>> = Rc::new(Cell::new(0.0));
@@ -256,6 +258,17 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
gtk::accessible::Property::Label("Compression quality comparison. Drag the vertical divider to compare original and compressed image. Drag elsewhere to pan."),
]);
// Hint label shown over the preview until the user first interacts with the divider
let divider_hint_label = gtk::Label::builder()
.label("Drag the divider to compare before and after")
.css_classes(["dim-label", "caption"])
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.build();
divider_hint_label.update_property(&[
gtk::accessible::Property::Label("Hint: drag the divider to compare before and after compression"),
]);
// Draw function - cover fill with pan support
{
let dp = divider_pos.clone();
@@ -376,12 +389,19 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
let dspy = drag_start_pan_y.clone();
let px = pan_x.clone();
let py = pan_y.clone();
let hint_vis = divider_hint_visible.clone();
let hint_lbl = divider_hint_label.clone();
drag_gesture.connect_drag_begin(move |_, x, _| {
let w = drawing.width() as f64;
let current = *dp.borrow() * w;
if (x - current).abs() < 30.0 {
dd.set(true);
id.set(false);
// Hide the hint on first divider interaction
if hint_vis.get() {
hint_vis.set(false);
hint_lbl.set_visible(false);
}
} else {
dd.set(false);
id.set(true);
@@ -437,10 +457,62 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
}
preview_drawing.add_controller(drag_gesture);
// Keyboard support for divider: Left/Right to move divider, Space to reset to center
{
let dp = divider_pos.clone();
let drawing = preview_drawing.clone();
let hint_vis = divider_hint_visible.clone();
let hint_lbl = divider_hint_label.clone();
let key = gtk::EventControllerKey::new();
key.connect_key_pressed(move |_, keyval, _, _| {
let step = 0.02;
match keyval {
gtk::gdk::Key::Left => {
let new_pos = (*dp.borrow() - step).clamp(0.05, 0.95);
*dp.borrow_mut() = new_pos;
drawing.queue_draw();
if hint_vis.get() {
hint_vis.set(false);
hint_lbl.set_visible(false);
}
return gtk::glib::Propagation::Stop;
}
gtk::gdk::Key::Right => {
let new_pos = (*dp.borrow() + step).clamp(0.05, 0.95);
*dp.borrow_mut() = new_pos;
drawing.queue_draw();
if hint_vis.get() {
hint_vis.set(false);
hint_lbl.set_visible(false);
}
return gtk::glib::Propagation::Stop;
}
gtk::gdk::Key::space => {
*dp.borrow_mut() = 0.5;
drawing.queue_draw();
if hint_vis.get() {
hint_vis.set(false);
hint_lbl.set_visible(false);
}
return gtk::glib::Propagation::Stop;
}
_ => {}
}
gtk::glib::Propagation::Proceed
});
preview_drawing.set_focusable(true);
preview_drawing.add_controller(key);
}
let preview_overlay = gtk::Overlay::builder()
.child(&preview_drawing)
.build();
preview_overlay.add_overlay(&divider_hint_label);
let preview_frame = gtk::Frame::builder()
.halign(gtk::Align::Fill)
.build();
preview_frame.set_child(Some(&preview_drawing));
preview_frame.set_child(Some(&preview_overlay));
preview_group.add(&size_box);
preview_group.add(&preview_frame);
@@ -480,11 +552,22 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
frame.add_css_class("accent");
}
let file_name = files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image");
let btn = gtk::Button::builder()
.child(&frame)
.has_frame(false)
.tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image"))
.tooltip_text(file_name)
.build();
let selected_label = if i == 0 { "currently selected" } else { "" };
btn.update_property(&[
gtk::accessible::Property::Label(
&if selected_label.is_empty() {
format!("Preview thumbnail: {}", file_name)
} else {
format!("Preview thumbnail: {} ({})", file_name, selected_label)
}
),
]);
thumb_box.append(&btn);
}
@@ -816,7 +899,7 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
.child(&scrolled)
.build();
// On page map: refresh thumbnail strip, preview, and show/hide per-format rows
// On page map: sync enable toggle, refresh thumbnail strip, preview, and show/hide per-format rows
{
let up = update_preview.clone();
let jc = state.job_config.clone();
@@ -832,7 +915,10 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
let ts = thumb_scrolled.clone();
let pidx = preview_index.clone();
let up2 = update_preview.clone();
let er = enable_row.clone();
page.connect_map(move |_| {
let enabled = jc.borrow().compress_enabled;
er.set_active(enabled);
// Rebuild thumbnail strip from current file list
while let Some(child) = tb.first_child() {
tb.remove(&child);
@@ -856,11 +942,22 @@ pub fn build_compress_page(state: &AppState) -> adw::NavigationPage {
let up_c = up2.clone();
let tb_c = tb.clone();
let current_idx = i;
let file_name = files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image");
let btn = gtk::Button::builder()
.child(&frame)
.has_frame(false)
.tooltip_text(files[i].file_name().and_then(|n| n.to_str()).unwrap_or("image"))
.tooltip_text(file_name)
.build();
let is_selected = i == *pidx.borrow();
btn.update_property(&[
gtk::accessible::Property::Label(
&if is_selected {
format!("Preview thumbnail: {} (currently selected)", file_name)
} else {
format!("Preview thumbnail: {}", file_name)
}
),
]);
btn.connect_clicked(move |_| {
*pidx_c.borrow_mut() = current_idx;
up_c(true);