- Add detailed_mode to AppState, derived from skill_level setting - Expand advanced option sections by default in Detailed mode (resize, convert, compress, watermark steps) - Fix high contrast to use HighContrast GTK theme - Add completion sound via canberra-gtk-play
286 lines
10 KiB
Rust
286 lines
10 KiB
Rust
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<ImageFormat>)] = &[
|
|
("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<ImageFormat>) -> 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(),
|
|
}
|
|
}
|