use adw::prelude::*; use crate::app::AppState; pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .build(); let content = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .margin_top(12) .margin_bottom(12) .margin_start(24) .margin_end(24) .build(); let cfg = state.job_config.borrow(); // Enable toggle let enable_row = adw::SwitchRow::builder() .title("Enable Resize") .subtitle("Resize images to new dimensions") .active(cfg.resize_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); enable_group.add(&enable_row); content.append(&enable_group); // Mode selector using GtkStack with a stack switcher let mode_stack = gtk::Stack::builder() .transition_type(gtk::StackTransitionType::Crossfade) .build(); let switcher = gtk::StackSwitcher::builder() .stack(&mode_stack) .halign(gtk::Align::Center) .margin_top(6) .margin_bottom(6) .build(); content.append(&switcher); // --- Mode 1: Width/Height --- let wh_box = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .build(); let wh_group = adw::PreferencesGroup::builder() .title("Target Dimensions") .description("Set width and/or height. Set either to 0 to maintain aspect ratio.") .build(); let width_row = adw::SpinRow::builder() .title("Width") .subtitle("Target width in pixels") .adjustment(>k::Adjustment::new(cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0)) .build(); let height_row = adw::SpinRow::builder() .title("Height") .subtitle("Target height in pixels (0 = auto from aspect ratio)") .adjustment(>k::Adjustment::new(cfg.resize_height as f64, 0.0, 10000.0, 1.0, 100.0, 0.0)) .build(); wh_group.add(&width_row); wh_group.add(&height_row); wh_box.append(&wh_group); mode_stack.add_titled(&wh_box, Some("width-height"), "Width / Height"); // --- Mode 2: Preset Dimensions --- let preset_box = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .build(); let presets_group = adw::PreferencesGroup::builder() .title("Quick Dimension Presets") .description("Select a preset to set the dimensions") .build(); // Clone for closures let width_for_preset = width_row.clone(); let height_for_preset = height_row.clone(); let build_preset_section = |title: &str, subtitle: &str, presets: &[(&str, u32, u32)]| -> adw::ExpanderRow { let expander = adw::ExpanderRow::builder() .title(title) .subtitle(subtitle) .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(>k::Image::from_icon_name("go-next-symbolic")); let width_c = width_for_preset.clone(); let height_c = height_for_preset.clone(); let w = *w; let h = *h; let stack_c = mode_stack.clone(); row.connect_activated(move |_| { width_c.set_value(w as f64); height_c.set_value(h as f64); // Switch to width/height tab to show the values stack_c.set_visible_child_name("width-height"); }); expander.add_row(&row); } expander }; 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), ("Bluesky Banner", 1500, 500), ("Lemmy Post", 1200, 630), ("PeerTube Thumbnail", 1280, 720), ("Friendica Post", 1200, 630), ("Funkwhale Cover", 1400, 1400), ], ); let mainstream_expander = build_preset_section( "Mainstream Platforms", "Instagram, YouTube, LinkedIn, Facebook, TikTok", &[ ("Instagram Post Square", 1080, 1080), ("Instagram Post 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 Video Cover", 1080, 1920), ("Threads Post", 1080, 1080), ], ); 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 Standard", 800, 0), ("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(&common_expander); preset_box.append(&presets_group); mode_stack.add_titled(&preset_box, Some("presets"), "Presets"); // --- Mode 3: Fit in Box --- let fit_box = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(12) .build(); let fit_group = adw::PreferencesGroup::builder() .title("Fit in Bounding Box") .description("Images are scaled down to fit within these maximum dimensions while maintaining their aspect ratio. Images smaller than the box are not enlarged.") .build(); let max_width_row = adw::SpinRow::builder() .title("Maximum Width") .subtitle("Images wider than this are scaled down") .adjustment(>k::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0)) .build(); let max_height_row = adw::SpinRow::builder() .title("Maximum Height") .subtitle("Images taller than this are scaled down") .adjustment(>k::Adjustment::new(1200.0, 1.0, 10000.0, 1.0, 100.0, 0.0)) .build(); fit_group.add(&max_width_row); fit_group.add(&max_height_row); fit_box.append(&fit_group); // Wire fit-in-box to update width/height { let width_c = width_row.clone(); let height_c = height_row.clone(); max_width_row.connect_value_notify(move |row| { width_c.set_value(row.value()); }); let height_c2 = height_row.clone(); max_height_row.connect_value_notify(move |row| { height_c2.set_value(row.value()); }); let _ = height_c; // suppress unused } mode_stack.add_titled(&fit_box, Some("fit-box"), "Fit in Box"); content.append(&mode_stack); // Advanced options (AdwExpanderRow per design doc) let advanced_group = adw::PreferencesGroup::builder() .title("Advanced") .build(); let advanced_expander = adw::ExpanderRow::builder() .title("Advanced Options") .subtitle("Resize algorithm, DPI, upscale behavior") .show_enable_switch(false) .build(); let upscale_row = adw::SwitchRow::builder() .title("Allow Upscaling") .subtitle("Enlarge images smaller than target size") .active(cfg.allow_upscale) .build(); let algorithm_row = adw::ComboRow::builder() .title("Resize Algorithm") .subtitle("Method used for pixel interpolation") .build(); let algo_model = gtk::StringList::new(&[ "Lanczos3 (Best quality)", "CatmullRom (Good quality, faster)", "Bilinear (Fast, lower quality)", "Nearest (Fastest, pixelated)", ]); algorithm_row.set_model(Some(&algo_model)); let dpi_row = adw::SpinRow::builder() .title("DPI") .subtitle("Output resolution in dots per inch") .adjustment(>k::Adjustment::new(72.0, 72.0, 600.0, 1.0, 10.0, 0.0)) .build(); advanced_expander.add_row(&upscale_row); advanced_expander.add_row(&algorithm_row); advanced_expander.add_row(&dpi_row); advanced_group.add(&advanced_expander); content.append(&advanced_group); drop(cfg); // Wire signals { let jc = state.job_config.clone(); enable_row.connect_active_notify(move |row| { jc.borrow_mut().resize_enabled = row.is_active(); }); } { let jc = state.job_config.clone(); width_row.connect_value_notify(move |row| { jc.borrow_mut().resize_width = row.value() as u32; }); } { let jc = state.job_config.clone(); height_row.connect_value_notify(move |row| { jc.borrow_mut().resize_height = row.value() as u32; }); } { let jc = state.job_config.clone(); upscale_row.connect_active_notify(move |row| { jc.borrow_mut().allow_upscale = row.is_active(); }); } scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() .maximum_size(600) .child(&scrolled) .build(); adw::NavigationPage::builder() .title("Resize") .tag("step-resize") .child(&clamp) .build() }