use adw::prelude::*; use crate::app::AppState; use pixstrip_core::types::ImageFormat; pub fn build_convert_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 Format Conversion") .subtitle("Convert images to a different format") .active(cfg.convert_enabled) .build(); let enable_group = adw::PreferencesGroup::new(); enable_group.add(&enable_row); content.append(&enable_group); // Visual format cards grid let cards_group = adw::PreferencesGroup::builder() .title("Output Format") .description("Choose the format all images will be converted to") .build(); let flow = gtk::FlowBox::builder() .selection_mode(gtk::SelectionMode::Single) .max_children_per_line(4) .min_children_per_line(2) .row_spacing(8) .column_spacing(8) .homogeneous(true) .margin_top(4) .margin_bottom(4) .build(); let formats: &[(&str, &str, &str, Option)] = &[ ("Keep Original", "No conversion", "edit-copy-symbolic", None), ("JPEG", "Universal photo format\nLossy, small files", "image-x-generic-symbolic", Some(ImageFormat::Jpeg)), ("PNG", "Lossless, transparency\nGraphics, screenshots", "image-x-generic-symbolic", Some(ImageFormat::Png)), ("WebP", "Modern web format\nExcellent compression", "web-browser-symbolic", Some(ImageFormat::WebP)), ("AVIF", "Next-gen format\nBest compression", "emblem-favorite-symbolic", Some(ImageFormat::Avif)), ("GIF", "Animations, 256 colors\nSimple graphics", "media-playback-start-symbolic", Some(ImageFormat::Gif)), ("TIFF", "Archival, lossless\nVery large files", "drive-harddisk-symbolic", Some(ImageFormat::Tiff)), ]; // Track which card should be initially selected let initial_format = cfg.convert_format; for (name, desc, icon_name, _fmt) in formats { let card = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .build(); card.add_css_class("card"); card.set_size_request(130, 110); let inner = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(4) .margin_top(12) .margin_bottom(12) .margin_start(8) .margin_end(8) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .vexpand(true) .build(); let icon = gtk::Image::builder() .icon_name(*icon_name) .pixel_size(28) .build(); let name_label = gtk::Label::builder() .label(*name) .css_classes(["heading"]) .build(); let desc_label = gtk::Label::builder() .label(*desc) .css_classes(["caption", "dim-label"]) .wrap(true) .justify(gtk::Justification::Center) .max_width_chars(18) .build(); inner.append(&icon); inner.append(&name_label); inner.append(&desc_label); card.append(&inner); flow.append(&card); } // Select the initial card let initial_idx = match initial_format { None => 0, Some(ImageFormat::Jpeg) => 1, Some(ImageFormat::Png) => 2, Some(ImageFormat::WebP) => 3, Some(ImageFormat::Avif) => 4, Some(ImageFormat::Gif) => 5, Some(ImageFormat::Tiff) => 6, }; if let Some(child) = flow.child_at_index(initial_idx) { flow.select_child(&child); } // Format info label (updates based on selection) let info_label = gtk::Label::builder() .label(format_info(cfg.convert_format)) .css_classes(["dim-label"]) .halign(gtk::Align::Start) .wrap(true) .margin_top(8) .margin_bottom(4) .margin_start(4) .build(); cards_group.add(&flow); cards_group.add(&info_label); content.append(&cards_group); // Advanced options expander let advanced_group = adw::PreferencesGroup::builder() .title("Advanced Options") .build(); let advanced_expander = adw::ExpanderRow::builder() .title("Format Mapping") .subtitle("Different input formats can convert to different outputs") .show_enable_switch(false) .expanded(state.detailed_mode) .build(); let progressive_row = adw::SwitchRow::builder() .title("Progressive JPEG") .subtitle("Loads gradually in browsers, slightly larger") .active(cfg.progressive_jpeg) .build(); // Format mapping rows - per input format output selection let mapping_header = adw::ActionRow::builder() .title("Per-Format Mapping") .subtitle("Override the output format for specific input types") .build(); mapping_header.add_prefix(>k::Image::from_icon_name("preferences-system-symbolic")); let output_choices = ["Same as above", "JPEG", "PNG", "WebP", "AVIF", "Keep Original"]; let jpeg_mapping = adw::ComboRow::builder() .title("JPEG inputs") .subtitle("Output format for JPEG source files") .build(); jpeg_mapping.set_model(Some(>k::StringList::new(&output_choices))); jpeg_mapping.set_selected(cfg.format_mapping_jpeg); let png_mapping = adw::ComboRow::builder() .title("PNG inputs") .subtitle("Output format for PNG source files") .build(); png_mapping.set_model(Some(>k::StringList::new(&output_choices))); png_mapping.set_selected(cfg.format_mapping_png); let webp_mapping = adw::ComboRow::builder() .title("WebP inputs") .subtitle("Output format for WebP source files") .build(); webp_mapping.set_model(Some(>k::StringList::new(&output_choices))); webp_mapping.set_selected(cfg.format_mapping_webp); let tiff_mapping = adw::ComboRow::builder() .title("TIFF inputs") .subtitle("Output format for TIFF source files") .build(); tiff_mapping.set_model(Some(>k::StringList::new(&output_choices))); tiff_mapping.set_selected(cfg.format_mapping_tiff); advanced_expander.add_row(&progressive_row); advanced_expander.add_row(&mapping_header); advanced_expander.add_row(&jpeg_mapping); advanced_expander.add_row(&png_mapping); advanced_expander.add_row(&webp_mapping); advanced_expander.add_row(&tiff_mapping); 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().convert_enabled = row.is_active(); }); } { let jc = state.job_config.clone(); let label = info_label; flow.connect_child_activated(move |_flow, child| { let idx = child.index() as usize; let mut c = jc.borrow_mut(); c.convert_format = match idx { 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)); }); } { let jc = state.job_config.clone(); progressive_row.connect_active_notify(move |row| { jc.borrow_mut().progressive_jpeg = row.is_active(); }); } { let jc = state.job_config.clone(); jpeg_mapping.connect_selected_notify(move |row| { jc.borrow_mut().format_mapping_jpeg = row.selected(); }); } { let jc = state.job_config.clone(); png_mapping.connect_selected_notify(move |row| { jc.borrow_mut().format_mapping_png = row.selected(); }); } { let jc = state.job_config.clone(); webp_mapping.connect_selected_notify(move |row| { jc.borrow_mut().format_mapping_webp = row.selected(); }); } { let jc = state.job_config.clone(); tiff_mapping.connect_selected_notify(move |row| { jc.borrow_mut().format_mapping_tiff = row.selected(); }); } scrolled.set_child(Some(&content)); let clamp = adw::Clamp::builder() .maximum_size(600) .child(&scrolled) .build(); adw::NavigationPage::builder() .title("Convert") .tag("step-convert") .child(&clamp) .build() } fn format_info(format: Option) -> String { match format { None => "Images will keep their original format. No conversion applied.".into(), Some(ImageFormat::Jpeg) => "JPEG: Best for photographs. Lossy compression, no transparency support. Universally compatible with all devices and browsers.".into(), Some(ImageFormat::Png) => "PNG: Best for graphics, screenshots, and logos. Lossless compression, supports full transparency. Produces larger files than JPEG or WebP.".into(), Some(ImageFormat::WebP) => "WebP: Modern format with excellent lossy and lossless compression. Supports transparency and animation. Widely supported in modern browsers.".into(), Some(ImageFormat::Avif) => "AVIF: Next-generation format based on AV1 video codec. Best compression ratios available. Supports transparency and HDR. Slower to encode, growing browser support.".into(), Some(ImageFormat::Gif) => "GIF: Limited to 256 colors per frame. Supports basic animation and binary transparency. Best for simple graphics and short animations.".into(), Some(ImageFormat::Tiff) => "TIFF: Professional archival format. Lossless compression, supports layers and rich metadata. Very large files. Not suitable for web.".into(), } }