- Redesign tutorial tour from modal dialogs to popovers pointing at actual UI elements - Add beginner-friendly improvements: help buttons, tooltips, welcome wizard enhancements - Add AppStream metainfo with screenshots, branding, categories, keywords, provides - Update desktop file with GTK category and SingleMainWindow - Add hicolor icon theme with all sizes (16-512px) - Fix debounce SourceId panic in rename step - Various step UI improvements and bug fixes
950 lines
32 KiB
Rust
950 lines
32 KiB
Rust
use adw::prelude::*;
|
|
use gtk::glib;
|
|
use std::cell::Cell;
|
|
use std::rc::Rc;
|
|
use crate::app::AppState;
|
|
|
|
fn get_first_image_aspect(files: &[std::path::PathBuf]) -> f64 {
|
|
let Some(first) = files.first() else { return 0.0 };
|
|
let Ok((w, h)) = image::image_dimensions(first) else { return 0.0 };
|
|
if h == 0 { return 0.0; }
|
|
w as f64 / h as f64
|
|
}
|
|
|
|
fn get_image_dims(path: &std::path::Path) -> (u32, u32) {
|
|
image::image_dimensions(path).unwrap_or((4000, 3000))
|
|
}
|
|
|
|
fn get_first_image_dims(files: &[std::path::PathBuf]) -> (u32, u32) {
|
|
let Some(first) = files.first() else { return (4000, 3000) };
|
|
get_image_dims(first)
|
|
}
|
|
|
|
fn algo_index_to_filter(idx: u32) -> image::imageops::FilterType {
|
|
match idx {
|
|
1 => image::imageops::FilterType::CatmullRom,
|
|
2 => image::imageops::FilterType::Triangle,
|
|
3 => image::imageops::FilterType::Nearest,
|
|
_ => image::imageops::FilterType::Lanczos3,
|
|
}
|
|
}
|
|
|
|
const CATEGORIES: &[&str] = &[
|
|
"Fediverse / Open Platforms",
|
|
"Mainstream Platforms",
|
|
"Common / Web",
|
|
];
|
|
|
|
fn presets_for_category(cat: u32) -> &'static [(&'static str, u32, u32)] {
|
|
match cat {
|
|
0 => &[
|
|
("Mastodon Post (1920 x 1080)", 1920, 1080),
|
|
("Mastodon Profile (400 x 400)", 400, 400),
|
|
("Mastodon Header (1500 x 500)", 1500, 500),
|
|
("Pixelfed Post (1080 x 1080)", 1080, 1080),
|
|
("Pixelfed Story (1080 x 1920)", 1080, 1920),
|
|
("Bluesky Post (1200 x 630)", 1200, 630),
|
|
("Bluesky Profile (400 x 400)", 400, 400),
|
|
("Bluesky Banner (1500 x 500)", 1500, 500),
|
|
("Lemmy Post (1200 x 630)", 1200, 630),
|
|
("PeerTube Thumbnail (1280 x 720)", 1280, 720),
|
|
("Friendica Post (1200 x 630)", 1200, 630),
|
|
("Funkwhale Cover (1400 x 1400)", 1400, 1400),
|
|
],
|
|
1 => &[
|
|
("Instagram Post Square (1080 x 1080)", 1080, 1080),
|
|
("Instagram Post Portrait (1080 x 1350)", 1080, 1350),
|
|
("Instagram Story/Reel (1080 x 1920)", 1080, 1920),
|
|
("Facebook Post (1200 x 630)", 1200, 630),
|
|
("Facebook Cover (820 x 312)", 820, 312),
|
|
("Facebook Profile (170 x 170)", 170, 170),
|
|
("YouTube Thumbnail (1280 x 720)", 1280, 720),
|
|
("YouTube Channel Art (2560 x 1440)", 2560, 1440),
|
|
("LinkedIn Post (1200 x 627)", 1200, 627),
|
|
("LinkedIn Cover (1584 x 396)", 1584, 396),
|
|
("LinkedIn Profile (400 x 400)", 400, 400),
|
|
("Pinterest Pin (1000 x 1500)", 1000, 1500),
|
|
("TikTok Video Cover (1080 x 1920)", 1080, 1920),
|
|
("Threads Post (1080 x 1080)", 1080, 1080),
|
|
],
|
|
_ => &[
|
|
("4K UHD (3840 x 2160)", 3840, 2160),
|
|
("Full HD (1920 x 1080)", 1920, 1080),
|
|
("HD Ready (1280 x 720)", 1280, 720),
|
|
("Blog Standard (800 wide)", 800, 0),
|
|
("Email Header (600 x 200)", 600, 200),
|
|
("Large Thumbnail (300 x 300)", 300, 300),
|
|
("Small Thumbnail (150 x 150)", 150, 150),
|
|
("Favicon (32 x 32)", 32, 32),
|
|
],
|
|
}
|
|
}
|
|
|
|
fn rebuild_size_model(size_row: &adw::ComboRow, cat: u32) {
|
|
let presets = presets_for_category(cat);
|
|
let mut names: Vec<&str> = vec!["(select a size)"];
|
|
names.extend(presets.iter().map(|p| p.0));
|
|
size_row.set_model(Some(>k::StringList::new(&names)));
|
|
size_row.set_list_factory(Some(&super::full_text_list_factory()));
|
|
size_row.set_selected(0);
|
|
}
|
|
|
|
pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|
let cfg = state.job_config.borrow();
|
|
|
|
// === OUTER LAYOUT ===
|
|
let outer = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.spacing(0)
|
|
.vexpand(true)
|
|
.build();
|
|
|
|
// --- Enable toggle (full width) ---
|
|
let enable_group = adw::PreferencesGroup::builder()
|
|
.margin_start(12)
|
|
.margin_end(12)
|
|
.margin_top(12)
|
|
.build();
|
|
let enable_row = adw::SwitchRow::builder()
|
|
.title("Enable Resize")
|
|
.subtitle("Scale images to new dimensions")
|
|
.active(cfg.resize_enabled)
|
|
.tooltip_text("Toggle resizing of images on or off")
|
|
.build();
|
|
enable_group.add(&enable_row);
|
|
outer.append(&enable_group);
|
|
|
|
// --- Horizontal split: Preview (left 60%) | Controls (right 40%) ---
|
|
let split = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.spacing(12)
|
|
.margin_start(12)
|
|
.margin_end(12)
|
|
.margin_top(12)
|
|
.margin_bottom(12)
|
|
.vexpand(true)
|
|
.build();
|
|
|
|
// ========== LEFT: Preview ==========
|
|
let preview_box = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.spacing(8)
|
|
.hexpand(true)
|
|
.valign(gtk::Align::Start)
|
|
.build();
|
|
|
|
let thumb_picture = gtk::Picture::builder()
|
|
.content_fit(gtk::ContentFit::Contain)
|
|
.height_request(250)
|
|
.hexpand(true)
|
|
.build();
|
|
thumb_picture.add_css_class("card");
|
|
thumb_picture.update_property(&[
|
|
gtk::accessible::Property::Label("Resize preview - click to cycle images"),
|
|
]);
|
|
|
|
let dims_label = gtk::Label::builder()
|
|
.css_classes(["dim-label", "caption"])
|
|
.halign(gtk::Align::Center)
|
|
.margin_top(4)
|
|
.build();
|
|
|
|
let no_preview_label = gtk::Label::builder()
|
|
.label("Add images to see resize preview")
|
|
.css_classes(["dim-label"])
|
|
.halign(gtk::Align::Center)
|
|
.build();
|
|
|
|
preview_box.append(&thumb_picture);
|
|
preview_box.append(&dims_label);
|
|
preview_box.append(&no_preview_label);
|
|
|
|
// ========== RIGHT: Controls ==========
|
|
let controls_scroll = gtk::ScrolledWindow::builder()
|
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
.vexpand(true)
|
|
.width_request(340)
|
|
.build();
|
|
|
|
let controls = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.spacing(12)
|
|
.build();
|
|
|
|
// --- Group 1: Preset (two-level: category then size) ---
|
|
let preset_group = adw::PreferencesGroup::builder()
|
|
.title("Preset")
|
|
.description("Pick a category, then a size")
|
|
.build();
|
|
|
|
let category_row = adw::ComboRow::builder()
|
|
.title("Category")
|
|
.use_subtitle(true)
|
|
.tooltip_text("Choose a category of size presets")
|
|
.build();
|
|
category_row.set_model(Some(>k::StringList::new(CATEGORIES)));
|
|
category_row.set_list_factory(Some(&super::full_text_list_factory()));
|
|
|
|
let size_row = adw::ComboRow::builder()
|
|
.title("Size")
|
|
.subtitle("Select a preset to fill dimensions")
|
|
.use_subtitle(true)
|
|
.tooltip_text("Pick a preset size to fill dimensions")
|
|
.build();
|
|
rebuild_size_model(&size_row, 0);
|
|
|
|
preset_group.add(&category_row);
|
|
preset_group.add(&size_row);
|
|
controls.append(&preset_group);
|
|
|
|
// --- Group 2: Dimensions ---
|
|
let dims_group = adw::PreferencesGroup::builder()
|
|
.title("Dimensions")
|
|
.build();
|
|
|
|
// Custom horizontal row: [W] [width_spin] [lock] [height_spin] [H] [px|%]
|
|
let dim_row = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.spacing(6)
|
|
.margin_start(12)
|
|
.margin_end(12)
|
|
.margin_top(10)
|
|
.margin_bottom(10)
|
|
.halign(gtk::Align::Center)
|
|
.build();
|
|
|
|
let w_label = gtk::Label::builder()
|
|
.label("W")
|
|
.css_classes(["dim-label"])
|
|
.build();
|
|
w_label.set_accessible_role(gtk::AccessibleRole::Presentation);
|
|
let width_spin = gtk::SpinButton::builder()
|
|
.adjustment(>k::Adjustment::new(
|
|
cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0,
|
|
))
|
|
.numeric(true)
|
|
.width_chars(6)
|
|
.tooltip_text("Width")
|
|
.build();
|
|
width_spin.update_property(&[
|
|
gtk::accessible::Property::Label("Width"),
|
|
]);
|
|
|
|
let lock_btn = gtk::ToggleButton::builder()
|
|
.icon_name("changes-prevent-symbolic")
|
|
.active(true)
|
|
.tooltip_text("Aspect ratio locked")
|
|
.build();
|
|
lock_btn.update_property(&[
|
|
gtk::accessible::Property::Label("Lock aspect ratio"),
|
|
]);
|
|
|
|
let height_spin = gtk::SpinButton::builder()
|
|
.adjustment(>k::Adjustment::new(
|
|
cfg.resize_height as f64, 0.0, 10000.0, 1.0, 100.0, 0.0,
|
|
))
|
|
.numeric(true)
|
|
.width_chars(6)
|
|
.tooltip_text("Height")
|
|
.build();
|
|
height_spin.update_property(&[
|
|
gtk::accessible::Property::Label("Height"),
|
|
]);
|
|
let h_label = gtk::Label::builder()
|
|
.label("H")
|
|
.css_classes(["dim-label"])
|
|
.build();
|
|
h_label.set_accessible_role(gtk::AccessibleRole::Presentation);
|
|
|
|
// Unit segmented toggle (px / %)
|
|
let unit_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
|
unit_box.add_css_class("linked");
|
|
unit_box.update_property(&[
|
|
gtk::accessible::Property::Label("Dimension unit toggle"),
|
|
]);
|
|
let px_btn = gtk::Button::builder()
|
|
.label("px")
|
|
.tooltip_text("Use pixel dimensions (currently active)")
|
|
.build();
|
|
px_btn.update_property(&[
|
|
gtk::accessible::Property::Label("Pixels - currently active"),
|
|
]);
|
|
let pct_btn = gtk::Button::builder()
|
|
.label("%")
|
|
.tooltip_text("Use percentage dimensions")
|
|
.build();
|
|
pct_btn.update_property(&[
|
|
gtk::accessible::Property::Label("Percentage"),
|
|
]);
|
|
px_btn.add_css_class("suggested-action");
|
|
unit_box.append(&px_btn);
|
|
unit_box.append(&pct_btn);
|
|
|
|
dim_row.append(&w_label);
|
|
dim_row.append(&width_spin);
|
|
dim_row.append(&lock_btn);
|
|
dim_row.append(&height_spin);
|
|
dim_row.append(&h_label);
|
|
dim_row.append(&unit_box);
|
|
dims_group.add(&dim_row);
|
|
|
|
// Mode
|
|
let mode_row = adw::ComboRow::builder()
|
|
.title("Mode")
|
|
.subtitle("How dimensions are applied to images")
|
|
.use_subtitle(true)
|
|
.tooltip_text("Exact stretches to dimensions; Fit keeps aspect ratio")
|
|
.build();
|
|
mode_row.set_model(Some(>k::StringList::new(&[
|
|
"Exact Size",
|
|
"Fit Within Box",
|
|
])));
|
|
mode_row.set_list_factory(Some(&super::full_text_list_factory()));
|
|
|
|
// Upscale
|
|
let upscale_row = adw::SwitchRow::builder()
|
|
.title("Allow Upscaling")
|
|
.subtitle("Enlarge images smaller than target size")
|
|
.active(cfg.allow_upscale)
|
|
.tooltip_text("When off, images smaller than target are left as-is")
|
|
.build();
|
|
|
|
dims_group.add(&mode_row);
|
|
dims_group.add(&upscale_row);
|
|
controls.append(&dims_group);
|
|
|
|
// --- Group 3: Advanced ---
|
|
let advanced_group = adw::PreferencesGroup::builder()
|
|
.title("Advanced")
|
|
.build();
|
|
let advanced_expander = adw::ExpanderRow::builder()
|
|
.title("Advanced Settings")
|
|
.subtitle("Resize algorithm and output DPI")
|
|
.show_enable_switch(false)
|
|
.expanded(state.is_section_expanded("resize-advanced"))
|
|
.build();
|
|
{
|
|
let st = state.clone();
|
|
advanced_expander.connect_expanded_notify(move |row| {
|
|
st.set_section_expanded("resize-advanced", row.is_expanded());
|
|
});
|
|
}
|
|
|
|
let algorithm_row = adw::ComboRow::builder()
|
|
.title("Resize Algorithm")
|
|
.use_subtitle(true)
|
|
.build();
|
|
algorithm_row.set_model(Some(>k::StringList::new(&[
|
|
"Lanczos3 (Best quality)",
|
|
"CatmullRom (Good, faster)",
|
|
"Bilinear (Fast)",
|
|
"Nearest (Pixelated)",
|
|
])));
|
|
algorithm_row.set_list_factory(Some(&super::full_text_list_factory()));
|
|
|
|
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(&algorithm_row);
|
|
advanced_expander.add_row(&dpi_row);
|
|
advanced_group.add(&advanced_expander);
|
|
controls.append(&advanced_group);
|
|
|
|
controls_scroll.set_child(Some(&controls));
|
|
|
|
split.append(&preview_box);
|
|
split.append(&controls_scroll);
|
|
outer.append(&split);
|
|
|
|
// === SHARED STATE ===
|
|
let preview_width = std::rc::Rc::new(std::cell::RefCell::new(cfg.resize_width));
|
|
let preview_height = std::rc::Rc::new(std::cell::RefCell::new(cfg.resize_height));
|
|
let preview_upscale = std::rc::Rc::new(std::cell::Cell::new(cfg.allow_upscale));
|
|
let preview_mode = std::rc::Rc::new(std::cell::Cell::new(cfg.resize_mode));
|
|
let preview_algo = std::rc::Rc::new(std::cell::Cell::new(0u32));
|
|
let preview_index = std::rc::Rc::new(std::cell::Cell::new(0usize));
|
|
let is_pct = std::rc::Rc::new(std::cell::Cell::new(false));
|
|
let updating = std::rc::Rc::new(std::cell::Cell::new(false));
|
|
let loaded_files = state.loaded_files.clone();
|
|
|
|
drop(cfg);
|
|
|
|
// === RENDER CLOSURE ===
|
|
let resize_preview_gen: Rc<Cell<u32>> = Rc::new(Cell::new(0));
|
|
let render_thumb = {
|
|
let lf = loaded_files.clone();
|
|
let pw = preview_width.clone();
|
|
let ph = preview_height.clone();
|
|
let pu = preview_upscale.clone();
|
|
let pm = preview_mode.clone();
|
|
let pa = preview_algo.clone();
|
|
let pi = preview_index.clone();
|
|
let pic = thumb_picture.clone();
|
|
let dlbl = dims_label.clone();
|
|
let npl = no_preview_label.clone();
|
|
let bind_gen = resize_preview_gen.clone();
|
|
std::rc::Rc::new(move || {
|
|
let files = lf.borrow();
|
|
if files.is_empty() {
|
|
pic.set_paintable(gtk::gdk::Paintable::NONE);
|
|
pic.set_visible(false);
|
|
npl.set_visible(true);
|
|
npl.set_label("Add images to see resize preview");
|
|
dlbl.set_label("");
|
|
return;
|
|
}
|
|
let idx = pi.get() % files.len();
|
|
let Some(current) = files.get(idx) else { return };
|
|
pic.set_visible(true);
|
|
npl.set_visible(false);
|
|
|
|
let tw = *pw.borrow();
|
|
let th = *ph.borrow();
|
|
if tw == 0 && th == 0 {
|
|
pic.set_paintable(gtk::gdk::Paintable::NONE);
|
|
npl.set_visible(true);
|
|
npl.set_label("Set dimensions to preview");
|
|
pic.set_visible(false);
|
|
dlbl.set_label("");
|
|
return;
|
|
}
|
|
|
|
let file_count = files.len();
|
|
let (orig_w, orig_h) = get_image_dims(current.as_path());
|
|
|
|
let actual_tw = if tw > 0 { tw } else { orig_w };
|
|
let actual_th = if th > 0 {
|
|
th
|
|
} else if tw > 0 {
|
|
let scale = tw as f64 / orig_w as f64;
|
|
(orig_h as f64 * scale).round() as u32
|
|
} else {
|
|
orig_h
|
|
};
|
|
|
|
let allow_up = pu.get();
|
|
let (render_tw, render_th) = if !allow_up {
|
|
(actual_tw.min(orig_w), actual_th.min(orig_h))
|
|
} else {
|
|
(actual_tw, actual_th)
|
|
};
|
|
|
|
let scale_pct = if orig_w > 0 {
|
|
(render_tw as f64 / orig_w as f64 * 100.0).round() as u32
|
|
} else {
|
|
100
|
|
};
|
|
|
|
let counter = if file_count > 1 {
|
|
format!(" [{}/{}]", idx + 1, file_count)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
let clamp_note = if !allow_up && (actual_tw > orig_w || actual_th > orig_h) {
|
|
" (clamped)"
|
|
} else {
|
|
""
|
|
};
|
|
|
|
dlbl.set_label(&format!(
|
|
"{} x {} -> {} x {} ({}%){}{}", orig_w, orig_h,
|
|
render_tw, render_th, scale_pct, clamp_note, counter,
|
|
));
|
|
|
|
let my_gen = bind_gen.get().wrapping_add(1);
|
|
bind_gen.set(my_gen);
|
|
let gen_check = bind_gen.clone();
|
|
|
|
let path = current.clone();
|
|
let pic = pic.clone();
|
|
let algo = algo_index_to_filter(pa.get());
|
|
let mode = pm.get();
|
|
let (tx, rx) = std::sync::mpsc::channel::<Option<Vec<u8>>>();
|
|
std::thread::spawn(move || {
|
|
let result = (|| -> Option<Vec<u8>> {
|
|
let img = image::open(&path).ok()?;
|
|
let target_w = if render_tw > 0 { render_tw } else { img.width().max(1) };
|
|
let target_h = if render_th > 0 {
|
|
render_th
|
|
} else if img.width() > 0 {
|
|
let scale = target_w as f64 / img.width() as f64;
|
|
(img.height() as f64 * scale).round().max(1.0) as u32
|
|
} else {
|
|
target_w
|
|
};
|
|
let resized = if mode == 0 && render_th > 0 {
|
|
// Exact: stretch to exact dimensions
|
|
img.resize_exact(target_w.min(1024), target_h.min(1024), algo)
|
|
} else {
|
|
// Fit within box (or width-only): maintain aspect ratio
|
|
img.resize(target_w.min(1024), target_h.min(1024), algo)
|
|
};
|
|
let mut buf = Vec::new();
|
|
resized.write_to(
|
|
&mut std::io::Cursor::new(&mut buf),
|
|
image::ImageFormat::Png,
|
|
).ok()?;
|
|
Some(buf)
|
|
})();
|
|
let _ = tx.send(result);
|
|
});
|
|
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
|
|
if gen_check.get() != my_gen {
|
|
return glib::ControlFlow::Break;
|
|
}
|
|
match rx.try_recv() {
|
|
Ok(Some(bytes)) => {
|
|
let gbytes = glib::Bytes::from(&bytes);
|
|
if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
|
|
pic.set_paintable(Some(&texture));
|
|
}
|
|
glib::ControlFlow::Break
|
|
}
|
|
Ok(None) => glib::ControlFlow::Break,
|
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
|
Err(_) => glib::ControlFlow::Break,
|
|
}
|
|
});
|
|
})
|
|
};
|
|
|
|
// === WIRE SIGNALS ===
|
|
|
|
// Enable toggle
|
|
{
|
|
let jc = state.job_config.clone();
|
|
enable_row.connect_active_notify(move |row| {
|
|
jc.borrow_mut().resize_enabled = row.is_active();
|
|
});
|
|
}
|
|
|
|
// Category selection - rebuild the size dropdown
|
|
{
|
|
let sr = size_row.clone();
|
|
category_row.connect_selected_notify(move |row| {
|
|
rebuild_size_model(&sr, row.selected());
|
|
});
|
|
}
|
|
|
|
// Size selection - auto-fill dimensions and lock aspect ratio
|
|
{
|
|
let ws = width_spin.clone();
|
|
let hs = height_spin.clone();
|
|
let lb = lock_btn.clone();
|
|
let jc = state.job_config.clone();
|
|
let pw = preview_width.clone();
|
|
let ph = preview_height.clone();
|
|
let upd = updating.clone();
|
|
let ip = is_pct.clone();
|
|
let px = px_btn.clone();
|
|
let pct = pct_btn.clone();
|
|
let rt = render_thumb.clone();
|
|
let cr = category_row.clone();
|
|
size_row.connect_selected_notify(move |row| {
|
|
let sel = row.selected() as usize;
|
|
if sel == 0 {
|
|
return; // "(select a size)" - don't change anything
|
|
}
|
|
let presets = presets_for_category(cr.selected());
|
|
let Some(&(_, w, h)) = presets.get(sel - 1) else { return };
|
|
|
|
// Switch to pixels if currently in percentage
|
|
if ip.get() {
|
|
ip.set(false);
|
|
px.add_css_class("suggested-action");
|
|
pct.remove_css_class("suggested-action");
|
|
ws.set_range(0.0, 10000.0);
|
|
ws.set_increments(1.0, 100.0);
|
|
hs.set_range(0.0, 10000.0);
|
|
hs.set_increments(1.0, 100.0);
|
|
}
|
|
|
|
upd.set(true);
|
|
ws.set_value(w as f64);
|
|
hs.set_value(h as f64);
|
|
upd.set(false);
|
|
|
|
// Lock aspect ratio
|
|
if !lb.is_active() {
|
|
lb.set_active(true);
|
|
}
|
|
|
|
let mut c = jc.borrow_mut();
|
|
c.resize_width = w;
|
|
c.resize_height = h;
|
|
*pw.borrow_mut() = w;
|
|
*ph.borrow_mut() = h;
|
|
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Lock button icon change
|
|
{
|
|
let lb = lock_btn.clone();
|
|
lock_btn.connect_active_notify(move |btn| {
|
|
if btn.is_active() {
|
|
lb.set_icon_name("changes-prevent-symbolic");
|
|
lb.set_tooltip_text(Some("Aspect ratio locked"));
|
|
lb.update_property(&[
|
|
gtk::accessible::Property::Label("Aspect ratio locked - click to unlock"),
|
|
]);
|
|
} else {
|
|
lb.set_icon_name("changes-allow-symbolic");
|
|
lb.set_tooltip_text(Some("Aspect ratio unlocked"));
|
|
lb.update_property(&[
|
|
gtk::accessible::Property::Label("Aspect ratio unlocked - click to lock"),
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Width spin with aspect lock
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let pw = preview_width.clone();
|
|
let ph = preview_height.clone();
|
|
let rt = render_thumb.clone();
|
|
let lb = lock_btn.clone();
|
|
let hs = height_spin.clone();
|
|
let files = state.loaded_files.clone();
|
|
let upd = updating.clone();
|
|
let ip = is_pct.clone();
|
|
let pr = size_row.clone();
|
|
width_spin.connect_value_notify(move |spin| {
|
|
if upd.get() { return; }
|
|
let val = spin.value();
|
|
|
|
let pixel_val = if ip.get() {
|
|
let dims = get_first_image_dims(&files.borrow());
|
|
(val / 100.0 * dims.0 as f64).round() as u32
|
|
} else {
|
|
val as u32
|
|
};
|
|
|
|
jc.borrow_mut().resize_width = pixel_val;
|
|
*pw.borrow_mut() = pixel_val;
|
|
|
|
// Reset preset to Custom when manually editing
|
|
if pr.selected() != 0 {
|
|
upd.set(true);
|
|
pr.set_selected(0);
|
|
upd.set(false);
|
|
}
|
|
|
|
if lb.is_active() && val > 0.0 {
|
|
if ip.get() {
|
|
upd.set(true);
|
|
hs.set_value(val);
|
|
let dims = get_first_image_dims(&files.borrow());
|
|
let pixel_h = (val / 100.0 * dims.1 as f64).round() as u32;
|
|
jc.borrow_mut().resize_height = pixel_h;
|
|
*ph.borrow_mut() = pixel_h;
|
|
upd.set(false);
|
|
} else {
|
|
let aspect = get_first_image_aspect(&files.borrow());
|
|
if aspect > 0.0 {
|
|
let new_h = (val / aspect).round() as u32;
|
|
upd.set(true);
|
|
hs.set_value(new_h as f64);
|
|
jc.borrow_mut().resize_height = new_h;
|
|
*ph.borrow_mut() = new_h;
|
|
upd.set(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Height spin with aspect lock
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let pw = preview_width.clone();
|
|
let ph = preview_height.clone();
|
|
let rt = render_thumb.clone();
|
|
let lb = lock_btn.clone();
|
|
let ws = width_spin.clone();
|
|
let files = state.loaded_files.clone();
|
|
let upd = updating.clone();
|
|
let ip = is_pct.clone();
|
|
let pr = size_row.clone();
|
|
height_spin.connect_value_notify(move |spin| {
|
|
if upd.get() { return; }
|
|
let val = spin.value();
|
|
|
|
let pixel_val = if ip.get() {
|
|
let dims = get_first_image_dims(&files.borrow());
|
|
(val / 100.0 * dims.1 as f64).round() as u32
|
|
} else {
|
|
val as u32
|
|
};
|
|
|
|
jc.borrow_mut().resize_height = pixel_val;
|
|
*ph.borrow_mut() = pixel_val;
|
|
|
|
// Reset preset to Custom when manually editing
|
|
if pr.selected() != 0 {
|
|
upd.set(true);
|
|
pr.set_selected(0);
|
|
upd.set(false);
|
|
}
|
|
|
|
if lb.is_active() && val > 0.0 {
|
|
if ip.get() {
|
|
upd.set(true);
|
|
ws.set_value(val);
|
|
let dims = get_first_image_dims(&files.borrow());
|
|
let pixel_w = (val / 100.0 * dims.0 as f64).round() as u32;
|
|
jc.borrow_mut().resize_width = pixel_w;
|
|
*pw.borrow_mut() = pixel_w;
|
|
upd.set(false);
|
|
} else {
|
|
let aspect = get_first_image_aspect(&files.borrow());
|
|
if aspect > 0.0 {
|
|
let new_w = (val * aspect).round() as u32;
|
|
upd.set(true);
|
|
ws.set_value(new_w as f64);
|
|
jc.borrow_mut().resize_width = new_w;
|
|
*pw.borrow_mut() = new_w;
|
|
upd.set(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Unit toggle: px button
|
|
{
|
|
let pct = pct_btn.clone();
|
|
let px = px_btn.clone();
|
|
let ip = is_pct.clone();
|
|
let ws = width_spin.clone();
|
|
let hs = height_spin.clone();
|
|
let files = state.loaded_files.clone();
|
|
let upd = updating.clone();
|
|
let jc = state.job_config.clone();
|
|
let pw = preview_width.clone();
|
|
let ph = preview_height.clone();
|
|
let rt = render_thumb.clone();
|
|
px_btn.connect_clicked(move |_| {
|
|
if !ip.get() { return; } // already pixels
|
|
ip.set(false);
|
|
px.add_css_class("suggested-action");
|
|
pct.remove_css_class("suggested-action");
|
|
px.update_property(&[
|
|
gtk::accessible::Property::Label("Pixels - currently active"),
|
|
]);
|
|
pct.update_property(&[
|
|
gtk::accessible::Property::Label("Percentage"),
|
|
]);
|
|
px.set_tooltip_text(Some("Use pixel dimensions (currently active)"));
|
|
pct.set_tooltip_text(Some("Use percentage dimensions"));
|
|
|
|
let dims = get_first_image_dims(&files.borrow());
|
|
let pct_w = ws.value();
|
|
let pct_h = hs.value();
|
|
let pixel_w = (pct_w / 100.0 * dims.0 as f64).round();
|
|
let pixel_h = (pct_h / 100.0 * dims.1 as f64).round();
|
|
|
|
upd.set(true);
|
|
ws.set_range(0.0, 10000.0);
|
|
ws.set_increments(1.0, 100.0);
|
|
hs.set_range(0.0, 10000.0);
|
|
hs.set_increments(1.0, 100.0);
|
|
ws.set_value(pixel_w);
|
|
hs.set_value(pixel_h);
|
|
upd.set(false);
|
|
|
|
let mut c = jc.borrow_mut();
|
|
c.resize_width = pixel_w as u32;
|
|
c.resize_height = pixel_h as u32;
|
|
*pw.borrow_mut() = pixel_w as u32;
|
|
*ph.borrow_mut() = pixel_h as u32;
|
|
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Unit toggle: % button
|
|
{
|
|
let px = px_btn.clone();
|
|
let pct = pct_btn.clone();
|
|
let ip = is_pct.clone();
|
|
let ws = width_spin.clone();
|
|
let hs = height_spin.clone();
|
|
let files = state.loaded_files.clone();
|
|
let upd = updating.clone();
|
|
let rt = render_thumb.clone();
|
|
let jc = state.job_config.clone();
|
|
let pw = preview_width.clone();
|
|
let ph = preview_height.clone();
|
|
pct_btn.connect_clicked(move |_| {
|
|
if ip.get() { return; } // already percentage
|
|
ip.set(true);
|
|
pct.add_css_class("suggested-action");
|
|
px.remove_css_class("suggested-action");
|
|
pct.update_property(&[
|
|
gtk::accessible::Property::Label("Percentage - currently active"),
|
|
]);
|
|
px.update_property(&[
|
|
gtk::accessible::Property::Label("Pixels"),
|
|
]);
|
|
pct.set_tooltip_text(Some("Use percentage dimensions (currently active)"));
|
|
px.set_tooltip_text(Some("Use pixel dimensions"));
|
|
|
|
let dims = get_first_image_dims(&files.borrow());
|
|
let cur_w = ws.value();
|
|
let cur_h = hs.value();
|
|
let pct_w = if dims.0 > 0 { (cur_w / dims.0 as f64 * 100.0).round() } else { 100.0 };
|
|
let pct_h = if dims.1 > 0 { (cur_h / dims.1 as f64 * 100.0).round() } else { 100.0 };
|
|
|
|
upd.set(true);
|
|
ws.set_range(0.0, 1000.0);
|
|
ws.set_increments(1.0, 10.0);
|
|
hs.set_range(0.0, 1000.0);
|
|
hs.set_increments(1.0, 10.0);
|
|
ws.set_value(pct_w);
|
|
hs.set_value(pct_h);
|
|
upd.set(false);
|
|
|
|
// Update job_config with the pixel values
|
|
let pixel_w = (pct_w / 100.0 * dims.0 as f64).round() as u32;
|
|
let pixel_h = (pct_h / 100.0 * dims.1 as f64).round() as u32;
|
|
let mut c = jc.borrow_mut();
|
|
c.resize_width = pixel_w;
|
|
c.resize_height = pixel_h;
|
|
*pw.borrow_mut() = pixel_w;
|
|
*ph.borrow_mut() = pixel_h;
|
|
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Mode toggle - update labels and job_config
|
|
{
|
|
let ws = width_spin.clone();
|
|
let hs = height_spin.clone();
|
|
let jc = state.job_config.clone();
|
|
let rt = render_thumb.clone();
|
|
let pmode = preview_mode.clone();
|
|
mode_row.connect_selected_notify(move |row| {
|
|
jc.borrow_mut().resize_mode = row.selected();
|
|
pmode.set(row.selected());
|
|
if row.selected() == 1 {
|
|
ws.set_tooltip_text(Some("Maximum width"));
|
|
hs.set_tooltip_text(Some("Maximum height"));
|
|
} else {
|
|
ws.set_tooltip_text(Some("Width"));
|
|
hs.set_tooltip_text(Some("Height"));
|
|
}
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Upscale toggle
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let pu = preview_upscale.clone();
|
|
let rt = render_thumb.clone();
|
|
upscale_row.connect_active_notify(move |row| {
|
|
jc.borrow_mut().allow_upscale = row.is_active();
|
|
pu.set(row.is_active());
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// Algorithm
|
|
{
|
|
let jc = state.job_config.clone();
|
|
let pa = preview_algo.clone();
|
|
let rt = render_thumb.clone();
|
|
algorithm_row.connect_selected_notify(move |row| {
|
|
jc.borrow_mut().resize_algorithm = row.selected();
|
|
pa.set(row.selected());
|
|
rt();
|
|
});
|
|
}
|
|
|
|
// DPI
|
|
{
|
|
let jc = state.job_config.clone();
|
|
dpi_row.connect_value_notify(move |row| {
|
|
jc.borrow_mut().output_dpi = row.value() as u32;
|
|
});
|
|
}
|
|
|
|
// Click preview to cycle images
|
|
{
|
|
let pi = preview_index.clone();
|
|
let rt = render_thumb.clone();
|
|
let lf = loaded_files.clone();
|
|
let click = gtk::GestureClick::new();
|
|
click.connect_released(move |gesture, _, _, _| {
|
|
let count = lf.borrow().len();
|
|
if count > 1 {
|
|
pi.set((pi.get() + 1) % count);
|
|
rt();
|
|
}
|
|
gesture.set_state(gtk::EventSequenceState::Claimed);
|
|
});
|
|
thumb_picture.set_can_target(true);
|
|
thumb_picture.set_focusable(true);
|
|
thumb_picture.add_controller(click);
|
|
thumb_picture.set_cursor_from_name(Some("pointer"));
|
|
}
|
|
|
|
// Keyboard support for preview cycling (Space/Enter)
|
|
{
|
|
let pi = preview_index.clone();
|
|
let rt = render_thumb.clone();
|
|
let lf = loaded_files.clone();
|
|
let key = gtk::EventControllerKey::new();
|
|
key.connect_key_pressed(move |_, keyval, _, _| {
|
|
if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return {
|
|
let count = lf.borrow().len();
|
|
if count > 1 {
|
|
pi.set((pi.get() + 1) % count);
|
|
rt();
|
|
}
|
|
return glib::Propagation::Stop;
|
|
}
|
|
glib::Propagation::Proceed
|
|
});
|
|
thumb_picture.add_controller(key);
|
|
}
|
|
|
|
// Initial render
|
|
{
|
|
let rt = render_thumb.clone();
|
|
glib::idle_add_local_once(move || rt());
|
|
}
|
|
|
|
let page = adw::NavigationPage::builder()
|
|
.title("Resize")
|
|
.tag("step-resize")
|
|
.child(&outer)
|
|
.build();
|
|
|
|
// Sync enable toggle and re-render on page map
|
|
{
|
|
let rt = render_thumb.clone();
|
|
let jc = state.job_config.clone();
|
|
let er = enable_row.clone();
|
|
page.connect_map(move |_| {
|
|
let enabled = jc.borrow().resize_enabled;
|
|
er.set_active(enabled);
|
|
rt();
|
|
});
|
|
}
|
|
|
|
page
|
|
}
|