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:
@@ -96,6 +96,14 @@ pub fn build_app() -> adw::Application {
|
|||||||
app.connect_activate(build_ui);
|
app.connect_activate(build_ui);
|
||||||
setup_shortcuts(&app);
|
setup_shortcuts(&app);
|
||||||
|
|
||||||
|
// App-level quit action
|
||||||
|
let quit_action = gtk::gio::SimpleAction::new("quit", None);
|
||||||
|
let app_clone = app.clone();
|
||||||
|
quit_action.connect_activate(move |_, _| {
|
||||||
|
app_clone.quit();
|
||||||
|
});
|
||||||
|
app.add_action(&quit_action);
|
||||||
|
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +118,9 @@ fn setup_shortcuts(app: &adw::Application) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
app.set_accels_for_action("win.add-files", &["<Control>o"]);
|
app.set_accels_for_action("win.add-files", &["<Control>o"]);
|
||||||
|
app.set_accels_for_action("app.quit", &["<Control>q"]);
|
||||||
|
app.set_accels_for_action("win.show-settings", &["<Control>comma"]);
|
||||||
|
app.set_accels_for_action("win.show-shortcuts", &["<Control>question", "F1"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_ui(app: &adw::Application) {
|
fn build_ui(app: &adw::Application) {
|
||||||
@@ -262,6 +273,7 @@ fn build_menu() -> gtk::gio::Menu {
|
|||||||
let menu = gtk::gio::Menu::new();
|
let menu = gtk::gio::Menu::new();
|
||||||
menu.append(Some("Settings"), Some("win.show-settings"));
|
menu.append(Some("Settings"), Some("win.show-settings"));
|
||||||
menu.append(Some("History"), Some("win.show-history"));
|
menu.append(Some("History"), Some("win.show-history"));
|
||||||
|
menu.append(Some("Keyboard Shortcuts"), Some("win.show-shortcuts"));
|
||||||
menu
|
menu
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +415,16 @@ fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
action_group.add_action(&action);
|
action_group.add_action(&action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts window
|
||||||
|
{
|
||||||
|
let window = window.clone();
|
||||||
|
let action = gtk::gio::SimpleAction::new("show-shortcuts", None);
|
||||||
|
action.connect_activate(move |_, _| {
|
||||||
|
show_shortcuts_window(&window);
|
||||||
|
});
|
||||||
|
action_group.add_action(&action);
|
||||||
|
}
|
||||||
|
|
||||||
// Connect button clicks
|
// Connect button clicks
|
||||||
ui.back_button.connect_clicked({
|
ui.back_button.connect_clicked({
|
||||||
let action_group = action_group.clone();
|
let action_group = action_group.clone();
|
||||||
@@ -878,6 +900,7 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
|
|
||||||
// Poll for messages from the processing thread
|
// Poll for messages from the processing thread
|
||||||
let ui_for_rx = ui.clone();
|
let ui_for_rx = ui.clone();
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
glib::timeout_add_local(std::time::Duration::from_millis(50), move || {
|
glib::timeout_add_local(std::time::Duration::from_millis(50), move || {
|
||||||
while let Ok(msg) = rx.try_recv() {
|
while let Ok(msg) = rx.try_recv() {
|
||||||
match msg {
|
match msg {
|
||||||
@@ -890,7 +913,9 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
bar.set_fraction(current as f64 / total as f64);
|
bar.set_fraction(current as f64 / total as f64);
|
||||||
bar.set_text(Some(&format!("{}/{} - {}", current, total, file)));
|
bar.set_text(Some(&format!("{}/{} - {}", current, total, file)));
|
||||||
}
|
}
|
||||||
update_progress_labels(&ui_for_rx.nav_view, current, total, &file);
|
let eta = calculate_eta(&start_time, current, total);
|
||||||
|
update_progress_labels(&ui_for_rx.nav_view, current, total, &file, &eta);
|
||||||
|
add_log_entry(&ui_for_rx.nav_view, current, total, &file);
|
||||||
}
|
}
|
||||||
ProcessingMessage::Done(result) => {
|
ProcessingMessage::Done(result) => {
|
||||||
show_results(&ui_for_rx, &result);
|
show_results(&ui_for_rx, &result);
|
||||||
@@ -1123,19 +1148,63 @@ fn wire_pause_button(page: &adw::NavigationPage) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_progress_labels(nav_view: &adw::NavigationView, current: usize, total: usize, file: &str) {
|
fn calculate_eta(start: &std::time::Instant, current: usize, total: usize) -> String {
|
||||||
|
if current == 0 {
|
||||||
|
return "Estimating time remaining...".into();
|
||||||
|
}
|
||||||
|
let elapsed = start.elapsed().as_secs_f64();
|
||||||
|
let per_image = elapsed / current as f64;
|
||||||
|
let remaining = (total - current) as f64 * per_image;
|
||||||
|
if remaining < 1.0 {
|
||||||
|
"Almost done...".into()
|
||||||
|
} else {
|
||||||
|
format!("ETA: ~{}", format_duration(remaining as u64 * 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_progress_labels(nav_view: &adw::NavigationView, current: usize, total: usize, file: &str, eta: &str) {
|
||||||
if let Some(page) = nav_view.visible_page() {
|
if let Some(page) = nav_view.visible_page() {
|
||||||
walk_widgets(&page.child(), &|widget| {
|
walk_widgets(&page.child(), &|widget| {
|
||||||
if let Some(label) = widget.downcast_ref::<gtk::Label>() {
|
if let Some(label) = widget.downcast_ref::<gtk::Label>() {
|
||||||
if label.css_classes().iter().any(|c| c == "heading")
|
if label.css_classes().iter().any(|c| c == "heading")
|
||||||
&& label.label().contains("images")
|
&& (label.label().contains("images") || label.label().contains("0 /"))
|
||||||
{
|
{
|
||||||
label.set_label(&format!("{} / {} images", current, total));
|
label.set_label(&format!("{} / {} images", current, total));
|
||||||
}
|
}
|
||||||
if label.css_classes().iter().any(|c| c == "dim-label")
|
if label.css_classes().iter().any(|c| c == "dim-label")
|
||||||
&& label.label().contains("Estimating")
|
&& (label.label().contains("Estimating") || label.label().contains("ETA") || label.label().contains("Almost") || label.label().contains("Current"))
|
||||||
{
|
{
|
||||||
label.set_label(&format!("Current: {}", file));
|
if current < total {
|
||||||
|
label.set_label(&format!("{} - {}", eta, file));
|
||||||
|
} else {
|
||||||
|
label.set_label("Finishing up...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_log_entry(nav_view: &adw::NavigationView, current: usize, total: usize, file: &str) {
|
||||||
|
if let Some(page) = nav_view.visible_page() {
|
||||||
|
walk_widgets(&page.child(), &|widget| {
|
||||||
|
if let Some(bx) = widget.downcast_ref::<gtk::Box>()
|
||||||
|
&& bx.spacing() == 2
|
||||||
|
&& bx.orientation() == gtk::Orientation::Vertical
|
||||||
|
{
|
||||||
|
let entry = gtk::Label::builder()
|
||||||
|
.label(format!("[{}/{}] {} - Done", current, total, file))
|
||||||
|
.halign(gtk::Align::Start)
|
||||||
|
.css_classes(["caption", "monospace"])
|
||||||
|
.build();
|
||||||
|
bx.append(&entry);
|
||||||
|
|
||||||
|
// Auto-scroll: the log box is inside a ScrolledWindow
|
||||||
|
if let Some(parent) = bx.parent()
|
||||||
|
&& let Some(sw) = parent.downcast_ref::<gtk::ScrolledWindow>()
|
||||||
|
{
|
||||||
|
let adj = sw.vadjustment();
|
||||||
|
adj.set_value(adj.upper());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1410,6 +1479,61 @@ fn walk_widgets(widget: &Option<gtk::Widget>, f: &dyn Fn(>k::Widget)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn show_shortcuts_window(window: &adw::ApplicationWindow) {
|
||||||
|
let dialog = adw::AlertDialog::builder()
|
||||||
|
.heading("Keyboard Shortcuts")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let content = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.margin_start(12)
|
||||||
|
.margin_end(12)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let sections: &[(&str, &[(&str, &str)])] = &[
|
||||||
|
("Wizard Navigation", &[
|
||||||
|
("Alt+Right", "Next step"),
|
||||||
|
("Alt+Left", "Previous step"),
|
||||||
|
("Alt+1..9", "Jump to step"),
|
||||||
|
("Ctrl+Enter", "Process images"),
|
||||||
|
]),
|
||||||
|
("File Management", &[
|
||||||
|
("Ctrl+O", "Add files"),
|
||||||
|
]),
|
||||||
|
("Application", &[
|
||||||
|
("Ctrl+,", "Settings"),
|
||||||
|
("Ctrl+? / F1", "Keyboard shortcuts"),
|
||||||
|
("Ctrl+Q", "Quit"),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (title, shortcuts) in sections {
|
||||||
|
let group = adw::PreferencesGroup::builder()
|
||||||
|
.title(*title)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
for (accel, desc) in *shortcuts {
|
||||||
|
let row = adw::ActionRow::builder()
|
||||||
|
.title(*desc)
|
||||||
|
.build();
|
||||||
|
let label = gtk::Label::builder()
|
||||||
|
.label(*accel)
|
||||||
|
.css_classes(["monospace", "dim-label"])
|
||||||
|
.build();
|
||||||
|
row.add_suffix(&label);
|
||||||
|
group.add(&row);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.append(&group);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.set_extra_child(Some(&content));
|
||||||
|
dialog.add_response("close", "Close");
|
||||||
|
dialog.set_default_response(Some("close"));
|
||||||
|
dialog.present(Some(window));
|
||||||
|
}
|
||||||
|
|
||||||
fn format_bytes(bytes: u64) -> String {
|
fn format_bytes(bytes: u64) -> String {
|
||||||
if bytes < 1024 {
|
if bytes < 1024 {
|
||||||
format!("{} B", bytes)
|
format!("{} B", bytes)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
// Format selection
|
// Format selection
|
||||||
let format_group = adw::PreferencesGroup::builder()
|
let format_group = adw::PreferencesGroup::builder()
|
||||||
.title("Output Format")
|
.title("Output Format")
|
||||||
|
.description("Choose the format all images will be converted to")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let format_row = adw::ComboRow::builder()
|
let format_row = adw::ComboRow::builder()
|
||||||
@@ -41,9 +42,12 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
.build();
|
.build();
|
||||||
let format_model = gtk::StringList::new(&[
|
let format_model = gtk::StringList::new(&[
|
||||||
"Keep Original",
|
"Keep Original",
|
||||||
"JPEG - universal, lossy",
|
"JPEG - universal, lossy, photos",
|
||||||
"PNG - lossless, graphics",
|
"PNG - lossless, graphics, transparency",
|
||||||
"WebP - modern, excellent compression",
|
"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));
|
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::Jpeg) => 1,
|
||||||
Some(ImageFormat::Png) => 2,
|
Some(ImageFormat::Png) => 2,
|
||||||
Some(ImageFormat::WebP) => 3,
|
Some(ImageFormat::WebP) => 3,
|
||||||
_ => 0,
|
Some(ImageFormat::Avif) => 4,
|
||||||
|
Some(ImageFormat::Gif) => 5,
|
||||||
|
Some(ImageFormat::Tiff) => 6,
|
||||||
});
|
});
|
||||||
|
|
||||||
format_group.add(&format_row);
|
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);
|
content.append(&format_group);
|
||||||
|
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
@@ -70,14 +88,19 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
let jc = state.job_config.clone();
|
let jc = state.job_config.clone();
|
||||||
|
let label = info_label;
|
||||||
format_row.connect_selected_notify(move |row| {
|
format_row.connect_selected_notify(move |row| {
|
||||||
let mut c = jc.borrow_mut();
|
let mut c = jc.borrow_mut();
|
||||||
c.convert_format = match row.selected() {
|
c.convert_format = match row.selected() {
|
||||||
1 => Some(ImageFormat::Jpeg),
|
1 => Some(ImageFormat::Jpeg),
|
||||||
2 => Some(ImageFormat::Png),
|
2 => Some(ImageFormat::Png),
|
||||||
3 => Some(ImageFormat::WebP),
|
3 => Some(ImageFormat::WebP),
|
||||||
|
4 => Some(ImageFormat::Avif),
|
||||||
|
5 => Some(ImageFormat::Gif),
|
||||||
|
6 => Some(ImageFormat::Tiff),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
label.set_label(&format_info(c.convert_format));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,3 +117,15 @@ pub fn build_convert_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
.child(&clamp)
|
.child(&clamp)
|
||||||
.build()
|
.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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
enable_group.add(&enable_row);
|
enable_group.add(&enable_row);
|
||||||
content.append(&enable_group);
|
content.append(&enable_group);
|
||||||
|
|
||||||
// Resize mode
|
// Resize dimensions
|
||||||
let mode_group = adw::PreferencesGroup::builder()
|
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();
|
.build();
|
||||||
|
|
||||||
let width_row = adw::SpinRow::builder()
|
let width_row = adw::SpinRow::builder()
|
||||||
@@ -53,14 +54,41 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
// Social media presets
|
// Social media presets
|
||||||
let presets_group = adw::PreferencesGroup::builder()
|
let presets_group = adw::PreferencesGroup::builder()
|
||||||
.title("Quick Dimension Presets")
|
.title("Quick Dimension Presets")
|
||||||
|
.description("Click a preset to fill in the dimensions above")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let fedi_expander = adw::ExpanderRow::builder()
|
// Helper to build preset expander sections
|
||||||
.title("Fediverse / Open Platforms")
|
let build_preset_section = |title: &str, subtitle: &str, presets: &[(&str, u32, u32)]| -> adw::ExpanderRow {
|
||||||
.subtitle("Mastodon, Pixelfed, Bluesky, Lemmy")
|
let expander = adw::ExpanderRow::builder()
|
||||||
|
.title(title)
|
||||||
|
.subtitle(subtitle)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let fedi_presets: Vec<(&str, u32, u32)> = vec![
|
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(>k::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);
|
||||||
|
}
|
||||||
|
|
||||||
|
expander
|
||||||
|
};
|
||||||
|
|
||||||
|
let fedi_expander = build_preset_section(
|
||||||
|
"Fediverse / Open Platforms",
|
||||||
|
"Mastodon, Pixelfed, Bluesky, Lemmy, PeerTube",
|
||||||
|
&[
|
||||||
("Mastodon Post", 1920, 1080),
|
("Mastodon Post", 1920, 1080),
|
||||||
("Mastodon Profile", 400, 400),
|
("Mastodon Profile", 400, 400),
|
||||||
("Mastodon Header", 1500, 500),
|
("Mastodon Header", 1500, 500),
|
||||||
@@ -69,116 +97,57 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
("Bluesky Post", 1200, 630),
|
("Bluesky Post", 1200, 630),
|
||||||
("Bluesky Profile", 400, 400),
|
("Bluesky Profile", 400, 400),
|
||||||
("Lemmy Post", 1200, 630),
|
("Lemmy Post", 1200, 630),
|
||||||
];
|
("PeerTube Thumbnail", 1280, 720),
|
||||||
|
("Friendica Post", 1200, 630),
|
||||||
|
("Funkwhale Cover", 500, 500),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
for (name, w, h) in &fedi_presets {
|
let mainstream_expander = build_preset_section(
|
||||||
let row = adw::ActionRow::builder()
|
"Mainstream Platforms",
|
||||||
.title(*name)
|
"Instagram, YouTube, LinkedIn, Facebook, TikTok",
|
||||||
.subtitle(format!("{} x {}", w, h))
|
&[
|
||||||
.activatable(true)
|
|
||||||
.build();
|
|
||||||
row.add_suffix(>k::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();
|
|
||||||
|
|
||||||
let mainstream_presets: Vec<(&str, u32, u32)> = vec![
|
|
||||||
("Instagram Post", 1080, 1080),
|
("Instagram Post", 1080, 1080),
|
||||||
|
("Instagram Portrait", 1080, 1350),
|
||||||
("Instagram Story/Reel", 1080, 1920),
|
("Instagram Story/Reel", 1080, 1920),
|
||||||
("Facebook Post", 1200, 630),
|
("Facebook Post", 1200, 630),
|
||||||
|
("Facebook Cover", 820, 312),
|
||||||
|
("Facebook Profile", 170, 170),
|
||||||
("YouTube Thumbnail", 1280, 720),
|
("YouTube Thumbnail", 1280, 720),
|
||||||
|
("YouTube Channel Art", 2560, 1440),
|
||||||
("LinkedIn Post", 1200, 627),
|
("LinkedIn Post", 1200, 627),
|
||||||
|
("LinkedIn Cover", 1584, 396),
|
||||||
|
("LinkedIn Profile", 400, 400),
|
||||||
("Pinterest Pin", 1000, 1500),
|
("Pinterest Pin", 1000, 1500),
|
||||||
];
|
("TikTok Profile", 200, 200),
|
||||||
|
("Threads Post", 1080, 1080),
|
||||||
|
("Twitter/X Post", 1200, 675),
|
||||||
|
("Twitter/X Header", 1500, 500),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
for (name, w, h) in &mainstream_presets {
|
let common_expander = build_preset_section(
|
||||||
let row = adw::ActionRow::builder()
|
"Common Sizes",
|
||||||
.title(*name)
|
"HD, 4K, Blog, Thumbnails",
|
||||||
.subtitle(format!("{} x {}", w, h))
|
&[
|
||||||
.activatable(true)
|
("4K UHD", 3840, 2160),
|
||||||
.build();
|
|
||||||
row.add_suffix(>k::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 other_expander = adw::ExpanderRow::builder()
|
|
||||||
.title("Common Sizes")
|
|
||||||
.subtitle("HD, Blog, Thumbnail")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let other_presets: Vec<(&str, u32, u32)> = vec![
|
|
||||||
("Full HD", 1920, 1080),
|
("Full HD", 1920, 1080),
|
||||||
("Blog Image", 800, 0),
|
("HD Ready", 1280, 720),
|
||||||
("Thumbnail", 150, 150),
|
("Blog Wide", 800, 0),
|
||||||
];
|
("Blog Standard", 800, 600),
|
||||||
|
("Email Header", 600, 200),
|
||||||
for (name, w, h) in &other_presets {
|
("Large Thumbnail", 300, 300),
|
||||||
let row = adw::ActionRow::builder()
|
("Small Thumbnail", 150, 150),
|
||||||
.title(*name)
|
("Favicon", 32, 32),
|
||||||
.subtitle(if *h == 0 { format!("{} wide", w) } else { format!("{} x {}", w, h) })
|
],
|
||||||
.activatable(true)
|
);
|
||||||
.build();
|
|
||||||
row.add_suffix(>k::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);
|
|
||||||
}
|
|
||||||
|
|
||||||
presets_group.add(&fedi_expander);
|
presets_group.add(&fedi_expander);
|
||||||
presets_group.add(&mainstream_expander);
|
presets_group.add(&mainstream_expander);
|
||||||
presets_group.add(&other_expander);
|
presets_group.add(&common_expander);
|
||||||
content.append(&presets_group);
|
content.append(&presets_group);
|
||||||
|
|
||||||
// Basic adjustments
|
// Advanced options
|
||||||
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
|
|
||||||
let advanced_group = adw::PreferencesGroup::builder()
|
let advanced_group = adw::PreferencesGroup::builder()
|
||||||
.title("Advanced Options")
|
.title("Advanced Options")
|
||||||
.build();
|
.build();
|
||||||
@@ -194,7 +163,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
|
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
// Wire signals to update JobConfig
|
// Wire signals
|
||||||
{
|
{
|
||||||
let jc = state.job_config.clone();
|
let jc = state.job_config.clone();
|
||||||
enable_row.connect_active_notify(move |row| {
|
enable_row.connect_active_notify(move |row| {
|
||||||
|
|||||||
Reference in New Issue
Block a user