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

@@ -109,6 +109,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.title("Enable Resize")
.subtitle("Scale images to new dimensions")
.active(cfg.resize_enabled)
.tooltip_text("Toggle resizing of images on or off")
.build();
enable_group.add(&enable_row);
outer.append(&enable_group);
@@ -179,6 +180,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
let category_row = adw::ComboRow::builder()
.title("Category")
.use_subtitle(true)
.tooltip_text("Choose a category of size presets")
.build();
category_row.set_model(Some(&gtk::StringList::new(CATEGORIES)));
category_row.set_list_factory(Some(&super::full_text_list_factory()));
@@ -187,6 +189,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.title("Size")
.subtitle("Select a preset to fill dimensions")
.use_subtitle(true)
.tooltip_text("Pick a preset size to fill dimensions")
.build();
rebuild_size_model(&size_row, 0);
@@ -214,6 +217,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.label("W")
.css_classes(["dim-label"])
.build();
w_label.set_accessible_role(gtk::AccessibleRole::Presentation);
let width_spin = gtk::SpinButton::builder()
.adjustment(&gtk::Adjustment::new(
cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0,
@@ -250,12 +254,28 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.label("H")
.css_classes(["dim-label"])
.build();
h_label.set_accessible_role(gtk::AccessibleRole::Presentation);
// Unit segmented toggle (px / %)
let unit_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
unit_box.add_css_class("linked");
let px_btn = gtk::Button::builder().label("px").build();
let pct_btn = gtk::Button::builder().label("%").build();
unit_box.update_property(&[
gtk::accessible::Property::Label("Dimension unit toggle"),
]);
let px_btn = gtk::Button::builder()
.label("px")
.tooltip_text("Use pixel dimensions (currently active)")
.build();
px_btn.update_property(&[
gtk::accessible::Property::Label("Pixels - currently active"),
]);
let pct_btn = gtk::Button::builder()
.label("%")
.tooltip_text("Use percentage dimensions")
.build();
pct_btn.update_property(&[
gtk::accessible::Property::Label("Percentage"),
]);
px_btn.add_css_class("suggested-action");
unit_box.append(&px_btn);
unit_box.append(&pct_btn);
@@ -273,6 +293,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.title("Mode")
.subtitle("How dimensions are applied to images")
.use_subtitle(true)
.tooltip_text("Exact stretches to dimensions; Fit keeps aspect ratio")
.build();
mode_row.set_model(Some(&gtk::StringList::new(&[
"Exact Size",
@@ -285,6 +306,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.title("Allow Upscaling")
.subtitle("Enlarge images smaller than target size")
.active(cfg.allow_upscale)
.tooltip_text("When off, images smaller than target are left as-is")
.build();
dims_group.add(&mode_row);
@@ -568,9 +590,15 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
if btn.is_active() {
lb.set_icon_name("changes-prevent-symbolic");
lb.set_tooltip_text(Some("Aspect ratio locked"));
lb.update_property(&[
gtk::accessible::Property::Label("Aspect ratio locked - click to unlock"),
]);
} else {
lb.set_icon_name("changes-allow-symbolic");
lb.set_tooltip_text(Some("Aspect ratio unlocked"));
lb.update_property(&[
gtk::accessible::Property::Label("Aspect ratio unlocked - click to lock"),
]);
}
});
}
@@ -711,6 +739,14 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
ip.set(false);
px.add_css_class("suggested-action");
pct.remove_css_class("suggested-action");
px.update_property(&[
gtk::accessible::Property::Label("Pixels - currently active"),
]);
pct.update_property(&[
gtk::accessible::Property::Label("Percentage"),
]);
px.set_tooltip_text(Some("Use pixel dimensions (currently active)"));
pct.set_tooltip_text(Some("Use percentage dimensions"));
let dims = get_first_image_dims(&files.borrow());
let pct_w = ws.value();
@@ -755,6 +791,14 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
ip.set(true);
pct.add_css_class("suggested-action");
px.remove_css_class("suggested-action");
pct.update_property(&[
gtk::accessible::Property::Label("Percentage - currently active"),
]);
px.update_property(&[
gtk::accessible::Property::Label("Pixels"),
]);
pct.set_tooltip_text(Some("Use percentage dimensions (currently active)"));
px.set_tooltip_text(Some("Use pixel dimensions"));
let dims = get_first_image_dims(&files.borrow());
let cur_w = ws.value();
@@ -852,10 +896,31 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
gesture.set_state(gtk::EventSequenceState::Claimed);
});
thumb_picture.set_can_target(true);
thumb_picture.set_focusable(true);
thumb_picture.add_controller(click);
thumb_picture.set_cursor_from_name(Some("pointer"));
}
// Keyboard support for preview cycling (Space/Enter)
{
let pi = preview_index.clone();
let rt = render_thumb.clone();
let lf = loaded_files.clone();
let key = gtk::EventControllerKey::new();
key.connect_key_pressed(move |_, keyval, _, _| {
if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return {
let count = lf.borrow().len();
if count > 1 {
pi.set((pi.get() + 1) % count);
rt();
}
return glib::Propagation::Stop;
}
glib::Propagation::Proceed
});
thumb_picture.add_controller(key);
}
// Initial render
{
let rt = render_thumb.clone();
@@ -868,10 +933,14 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
.child(&outer)
.build();
// Re-render on page map
// Sync enable toggle and re-render on page map
{
let rt = render_thumb.clone();
let jc = state.job_config.clone();
let er = enable_row.clone();
page.connect_map(move |_| {
let enabled = jc.borrow().resize_enabled;
er.set_active(enabled);
rt();
});
}