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

@@ -33,6 +33,7 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
// Format selection
let format_group = adw::PreferencesGroup::builder()
.title("Output Format")
.description("Choose the format all images will be converted to")
.build();
let format_row = adw::ComboRow::builder()
@@ -41,9 +42,12 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
.build();
let format_model = gtk::StringList::new(&[
"Keep Original",
"JPEG - universal, lossy",
"PNG - lossless, graphics",
"JPEG - universal, lossy, photos",
"PNG - lossless, graphics, transparency",
"WebP - modern, excellent compression",
"AVIF - next-gen, best compression",
"GIF - animations, limited colors",
"TIFF - archival, lossless, large files",
]);
format_row.set_model(Some(&format_model));
@@ -53,10 +57,24 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
Some(ImageFormat::Jpeg) => 1,
Some(ImageFormat::Png) => 2,
Some(ImageFormat::WebP) => 3,
_ => 0,
Some(ImageFormat::Avif) => 4,
Some(ImageFormat::Gif) => 5,
Some(ImageFormat::Tiff) => 6,
});
format_group.add(&format_row);
// Format info label
let info_label = gtk::Label::builder()
.label(format_info(cfg.convert_format))
.css_classes(["dim-label", "caption"])
.halign(gtk::Align::Start)
.wrap(true)
.margin_top(4)
.margin_bottom(8)
.margin_start(12)
.build();
format_group.add(&info_label);
content.append(&format_group);
drop(cfg);
@@ -70,14 +88,19 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
}
{
let jc = state.job_config.clone();
let label = info_label;
format_row.connect_selected_notify(move |row| {
let mut c = jc.borrow_mut();
c.convert_format = match row.selected() {
1 => Some(ImageFormat::Jpeg),
2 => Some(ImageFormat::Png),
3 => Some(ImageFormat::WebP),
4 => Some(ImageFormat::Avif),
5 => Some(ImageFormat::Gif),
6 => Some(ImageFormat::Tiff),
_ => None,
};
label.set_label(&format_info(c.convert_format));
});
}
@@ -94,3 +117,15 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
.child(&clamp)
.build()
}
fn format_info(format: Option<ImageFormat>) -> String {
match format {
None => "Images will keep their original format.".into(),
Some(ImageFormat::Jpeg) => "JPEG: Best for photographs. Lossy compression, no transparency. Universally supported.".into(),
Some(ImageFormat::Png) => "PNG: Best for graphics, screenshots, logos. Lossless, supports transparency. Larger files.".into(),
Some(ImageFormat::WebP) => "WebP: Modern format with excellent lossy and lossless compression. Supports transparency and animation. Widely supported in browsers.".into(),
Some(ImageFormat::Avif) => "AVIF: Next-generation format based on AV1. Best compression ratios, supports transparency and HDR. Slower to encode, growing browser support.".into(),
Some(ImageFormat::Gif) => "GIF: Limited to 256 colors. Supports animation and transparency. Best for simple graphics and short animations.".into(),
Some(ImageFormat::Tiff) => "TIFF: Professional archival format. Lossless, supports layers and metadata. Very large files. Not suitable for web use.".into(),
}
}