Add ETA calculation, activity log, keyboard shortcuts, expand format options

- Processing: ETA calculated from elapsed time and progress, per-image log
  entries added to activity log with auto-scroll
- Keyboard shortcuts: Ctrl+Q quit, Ctrl+, settings, Ctrl+?/F1 shortcuts dialog
- Shortcuts dialog: AdwAlertDialog with all keyboard shortcuts listed
- Hamburger menu: added Keyboard Shortcuts entry
- Convert step: added AVIF, GIF, TIFF format options with descriptions
- Resize step: removed duplicate rotate/flip (now in Adjustments step),
  added missing social media presets (PeerTube, Friendica, Funkwhale,
  Instagram Portrait, Facebook Cover/Profile, LinkedIn Cover/Profile,
  TikTok, YouTube Channel Art, Threads, Twitter/X, 4K, etc.)
This commit is contained in:
2026-03-06 12:20:04 +02:00
parent 8154324929
commit e8cdddd08d
3 changed files with 252 additions and 124 deletions

View File

@@ -29,9 +29,10 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
enable_group.add(&enable_row);
content.append(&enable_group);
// Resize mode
// Resize dimensions
let mode_group = adw::PreferencesGroup::builder()
.title("Resize Mode")
.title("Dimensions")
.description("Set target width and height. Set height to 0 to preserve aspect ratio.")
.build();
let width_row = adw::SpinRow::builder()
@@ -53,132 +54,100 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
// Social media presets
let presets_group = adw::PreferencesGroup::builder()
.title("Quick Dimension Presets")
.description("Click a preset to fill in the dimensions above")
.build();
let fedi_expander = adw::ExpanderRow::builder()
.title("Fediverse / Open Platforms")
.subtitle("Mastodon, Pixelfed, Bluesky, Lemmy")
.build();
let fedi_presets: Vec<(&str, u32, u32)> = vec![
("Mastodon Post", 1920, 1080),
("Mastodon Profile", 400, 400),
("Mastodon Header", 1500, 500),
("Pixelfed Post", 1080, 1080),
("Pixelfed Story", 1080, 1920),
("Bluesky Post", 1200, 630),
("Bluesky Profile", 400, 400),
("Lemmy Post", 1200, 630),
];
for (name, w, h) in &fedi_presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(format!("{} x {}", w, h))
.activatable(true)
// Helper to build preset expander sections
let build_preset_section = |title: &str, subtitle: &str, presets: &[(&str, u32, u32)]| -> adw::ExpanderRow {
let expander = adw::ExpanderRow::builder()
.title(title)
.subtitle(subtitle)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let width_row_c = width_row.clone();
let height_row_c = height_row.clone();
let w = *w;
let h = *h;
row.connect_activated(move |_| {
width_row_c.set_value(w as f64);
height_row_c.set_value(h as f64);
});
fedi_expander.add_row(&row);
}
let mainstream_expander = adw::ExpanderRow::builder()
.title("Mainstream Platforms")
.subtitle("Instagram, YouTube, LinkedIn, Pinterest")
.build();
for (name, w, h) in presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(if *h == 0 { format!("{} wide", w) } else { format!("{} x {}", w, h) })
.activatable(true)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let width_c = width_row.clone();
let height_c = height_row.clone();
let w = *w;
let h = *h;
row.connect_activated(move |_| {
width_c.set_value(w as f64);
height_c.set_value(h as f64);
});
expander.add_row(&row);
}
let mainstream_presets: Vec<(&str, u32, u32)> = vec![
("Instagram Post", 1080, 1080),
("Instagram Story/Reel", 1080, 1920),
("Facebook Post", 1200, 630),
("YouTube Thumbnail", 1280, 720),
("LinkedIn Post", 1200, 627),
("Pinterest Pin", 1000, 1500),
];
expander
};
for (name, w, h) in &mainstream_presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(format!("{} x {}", w, h))
.activatable(true)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let width_row_c = width_row.clone();
let height_row_c = height_row.clone();
let w = *w;
let h = *h;
row.connect_activated(move |_| {
width_row_c.set_value(w as f64);
height_row_c.set_value(h as f64);
});
mainstream_expander.add_row(&row);
}
let fedi_expander = build_preset_section(
"Fediverse / Open Platforms",
"Mastodon, Pixelfed, Bluesky, Lemmy, PeerTube",
&[
("Mastodon Post", 1920, 1080),
("Mastodon Profile", 400, 400),
("Mastodon Header", 1500, 500),
("Pixelfed Post", 1080, 1080),
("Pixelfed Story", 1080, 1920),
("Bluesky Post", 1200, 630),
("Bluesky Profile", 400, 400),
("Lemmy Post", 1200, 630),
("PeerTube Thumbnail", 1280, 720),
("Friendica Post", 1200, 630),
("Funkwhale Cover", 500, 500),
],
);
let other_expander = adw::ExpanderRow::builder()
.title("Common Sizes")
.subtitle("HD, Blog, Thumbnail")
.build();
let mainstream_expander = build_preset_section(
"Mainstream Platforms",
"Instagram, YouTube, LinkedIn, Facebook, TikTok",
&[
("Instagram Post", 1080, 1080),
("Instagram Portrait", 1080, 1350),
("Instagram Story/Reel", 1080, 1920),
("Facebook Post", 1200, 630),
("Facebook Cover", 820, 312),
("Facebook Profile", 170, 170),
("YouTube Thumbnail", 1280, 720),
("YouTube Channel Art", 2560, 1440),
("LinkedIn Post", 1200, 627),
("LinkedIn Cover", 1584, 396),
("LinkedIn Profile", 400, 400),
("Pinterest Pin", 1000, 1500),
("TikTok Profile", 200, 200),
("Threads Post", 1080, 1080),
("Twitter/X Post", 1200, 675),
("Twitter/X Header", 1500, 500),
],
);
let other_presets: Vec<(&str, u32, u32)> = vec![
("Full HD", 1920, 1080),
("Blog Image", 800, 0),
("Thumbnail", 150, 150),
];
for (name, w, h) in &other_presets {
let row = adw::ActionRow::builder()
.title(*name)
.subtitle(if *h == 0 { format!("{} wide", w) } else { format!("{} x {}", w, h) })
.activatable(true)
.build();
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let width_row_c = width_row.clone();
let height_row_c = height_row.clone();
let w = *w;
let h = *h;
row.connect_activated(move |_| {
width_row_c.set_value(w as f64);
height_row_c.set_value(h as f64);
});
other_expander.add_row(&row);
}
let common_expander = build_preset_section(
"Common Sizes",
"HD, 4K, Blog, Thumbnails",
&[
("4K UHD", 3840, 2160),
("Full HD", 1920, 1080),
("HD Ready", 1280, 720),
("Blog Wide", 800, 0),
("Blog Standard", 800, 600),
("Email Header", 600, 200),
("Large Thumbnail", 300, 300),
("Small Thumbnail", 150, 150),
("Favicon", 32, 32),
],
);
presets_group.add(&fedi_expander);
presets_group.add(&mainstream_expander);
presets_group.add(&other_expander);
presets_group.add(&common_expander);
content.append(&presets_group);
// Basic adjustments
let adjust_group = adw::PreferencesGroup::builder()
.title("Basic Adjustments")
.build();
let rotate_row = adw::ComboRow::builder()
.title("Rotate")
.subtitle("Rotation applied after resize")
.build();
let rotate_model = gtk::StringList::new(&["None", "90 clockwise", "180", "270 clockwise", "Auto-orient (EXIF)"]);
rotate_row.set_model(Some(&rotate_model));
let flip_row = adw::ComboRow::builder()
.title("Flip")
.subtitle("Mirror the image")
.build();
let flip_model = gtk::StringList::new(&["None", "Horizontal", "Vertical"]);
flip_row.set_model(Some(&flip_model));
adjust_group.add(&rotate_row);
adjust_group.add(&flip_row);
content.append(&adjust_group);
// Advanced
// Advanced options
let advanced_group = adw::PreferencesGroup::builder()
.title("Advanced Options")
.build();
@@ -194,7 +163,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
drop(cfg);
// Wire signals to update JobConfig
// Wire signals
{
let jc = state.job_config.clone();
enable_row.connect_active_notify(move |row| {