Files
pixstrip/pixstrip-gtk/src/steps/step_metadata.rs
lashman f3668c45c3 Improve UX, add popover tour, metadata, and hicolor icons
- 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
2026-03-08 14:18:15 +02:00

297 lines
10 KiB
Rust

use adw::prelude::*;
use crate::app::{AppState, MetadataMode};
pub fn build_metadata_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 Metadata Handling")
.subtitle("Control what image metadata to keep or remove")
.active(cfg.metadata_enabled)
.tooltip_text("Toggle metadata handling on or off")
.build();
let enable_group = adw::PreferencesGroup::new();
enable_group.add(&enable_row);
content.append(&enable_group);
// Quick presets
let presets_group = adw::PreferencesGroup::builder()
.title("Metadata Mode")
.build();
let strip_all_row = adw::ActionRow::builder()
.title("Strip All")
.subtitle("Remove all metadata - smallest files, maximum privacy")
.activatable(true)
.build();
let strip_all_icon = gtk::Image::from_icon_name("user-trash-symbolic");
strip_all_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
strip_all_row.add_prefix(&strip_all_icon);
let strip_all_check = gtk::CheckButton::new();
strip_all_check.set_active(cfg.metadata_mode == MetadataMode::StripAll);
strip_all_row.add_suffix(&strip_all_check);
strip_all_row.set_activatable_widget(Some(&strip_all_check));
let privacy_row = adw::ActionRow::builder()
.title("Privacy Mode")
.subtitle("Strip GPS and camera serial, keep copyright")
.activatable(true)
.build();
let privacy_icon = gtk::Image::from_icon_name("security-medium-symbolic");
privacy_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
privacy_row.add_prefix(&privacy_icon);
let privacy_check = gtk::CheckButton::new();
privacy_check.set_group(Some(&strip_all_check));
privacy_check.set_active(cfg.metadata_mode == MetadataMode::Privacy);
privacy_row.add_suffix(&privacy_check);
privacy_row.set_activatable_widget(Some(&privacy_check));
let keep_all_row = adw::ActionRow::builder()
.title("Keep All")
.subtitle("Preserve all original metadata")
.activatable(true)
.build();
let keep_all_icon = gtk::Image::from_icon_name("emblem-ok-symbolic");
keep_all_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
keep_all_row.add_prefix(&keep_all_icon);
let keep_all_check = gtk::CheckButton::new();
keep_all_check.set_group(Some(&strip_all_check));
keep_all_check.set_active(cfg.metadata_mode == MetadataMode::KeepAll);
keep_all_row.add_suffix(&keep_all_check);
keep_all_row.set_activatable_widget(Some(&keep_all_check));
let photographer_row = adw::ActionRow::builder()
.title("Photographer")
.subtitle("Keep copyright and camera model, strip GPS and software")
.activatable(true)
.build();
let photographer_icon = gtk::Image::from_icon_name("camera-photo-symbolic");
photographer_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
photographer_row.add_prefix(&photographer_icon);
let photographer_check = gtk::CheckButton::new();
photographer_check.set_group(Some(&strip_all_check));
photographer_row.add_suffix(&photographer_check);
photographer_row.set_activatable_widget(Some(&photographer_check));
let custom_row = adw::ActionRow::builder()
.title("Custom")
.subtitle("Choose exactly which metadata categories to strip")
.activatable(true)
.build();
let custom_icon = gtk::Image::from_icon_name("emblem-system-symbolic");
custom_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
custom_row.add_prefix(&custom_icon);
let custom_check = gtk::CheckButton::new();
custom_check.set_group(Some(&strip_all_check));
custom_check.set_active(cfg.metadata_mode == MetadataMode::Custom);
custom_row.add_suffix(&custom_check);
custom_row.set_activatable_widget(Some(&custom_check));
presets_group.add(&strip_all_row);
presets_group.add(&privacy_row);
presets_group.add(&photographer_row);
presets_group.add(&keep_all_row);
presets_group.add(&custom_row);
content.append(&presets_group);
// Custom category checkboxes
let custom_group = adw::PreferencesGroup::builder()
.title("Custom Categories")
.description("Select which metadata categories to strip")
.build();
let gps_row = adw::SwitchRow::builder()
.title("GPS / Location")
.subtitle("GPS coordinates, location name, altitude")
.active(cfg.strip_gps)
.tooltip_text("Strip GPS coordinates, location name, and altitude")
.build();
let camera_row = adw::SwitchRow::builder()
.title("Camera Info")
.subtitle("Camera model, serial number, lens data")
.active(cfg.strip_camera)
.tooltip_text("Strip camera model, serial number, and lens data")
.build();
let software_row = adw::SwitchRow::builder()
.title("Software")
.subtitle("Editing software, processing history")
.active(cfg.strip_software)
.tooltip_text("Strip editing software and processing history")
.build();
let timestamps_row = adw::SwitchRow::builder()
.title("Timestamps")
.subtitle("Date taken, date modified, date digitized")
.active(cfg.strip_timestamps)
.tooltip_text("Strip date taken, date modified, date digitized")
.build();
let copyright_row = adw::SwitchRow::builder()
.title("Copyright / Author")
.subtitle("Copyright notice, artist name, credits")
.active(cfg.strip_copyright)
.tooltip_text("Strip copyright notice, artist name, and credits")
.build();
custom_group.add(&gps_row);
custom_group.add(&camera_row);
custom_group.add(&software_row);
custom_group.add(&timestamps_row);
custom_group.add(&copyright_row);
// Only show custom group when Custom mode is selected
custom_group.set_visible(cfg.metadata_mode == MetadataMode::Custom);
content.append(&custom_group);
drop(cfg);
// Wire signals
{
let jc = state.job_config.clone();
enable_row.connect_active_notify(move |row| {
jc.borrow_mut().metadata_enabled = row.is_active();
});
}
{
let jc = state.job_config.clone();
let cg = custom_group.clone();
strip_all_check.connect_toggled(move |check| {
if check.is_active() {
jc.borrow_mut().metadata_mode = MetadataMode::StripAll;
cg.set_visible(false);
}
});
}
{
let jc = state.job_config.clone();
let cg = custom_group.clone();
privacy_check.connect_toggled(move |check| {
if check.is_active() {
jc.borrow_mut().metadata_mode = MetadataMode::Privacy;
cg.set_visible(false);
}
});
}
{
let jc = state.job_config.clone();
let cg = custom_group.clone();
let gps_c = gps_row.clone();
let camera_c = camera_row.clone();
let software_c = software_row.clone();
let timestamps_c = timestamps_row.clone();
let copyright_c = copyright_row.clone();
photographer_check.connect_toggled(move |check| {
if check.is_active() {
{
let mut cfg = jc.borrow_mut();
cfg.metadata_mode = MetadataMode::Custom;
// Photographer: keep copyright + camera model, strip GPS + software
cfg.strip_gps = true;
cfg.strip_camera = false;
cfg.strip_software = true;
cfg.strip_timestamps = false;
cfg.strip_copyright = false;
}
// Update UI to match (after dropping borrow to avoid re-entrancy)
gps_c.set_active(true);
camera_c.set_active(false);
software_c.set_active(true);
timestamps_c.set_active(false);
copyright_c.set_active(false);
cg.set_visible(false);
}
});
}
{
let jc = state.job_config.clone();
let cg = custom_group.clone();
keep_all_check.connect_toggled(move |check| {
if check.is_active() {
jc.borrow_mut().metadata_mode = MetadataMode::KeepAll;
cg.set_visible(false);
}
});
}
{
let jc = state.job_config.clone();
let cg = custom_group;
custom_check.connect_toggled(move |check| {
if check.is_active() {
jc.borrow_mut().metadata_mode = MetadataMode::Custom;
cg.set_visible(true);
}
});
}
// Wire custom category toggles
{
let jc = state.job_config.clone();
gps_row.connect_active_notify(move |row| {
jc.borrow_mut().strip_gps = row.is_active();
});
}
{
let jc = state.job_config.clone();
camera_row.connect_active_notify(move |row| {
jc.borrow_mut().strip_camera = row.is_active();
});
}
{
let jc = state.job_config.clone();
software_row.connect_active_notify(move |row| {
jc.borrow_mut().strip_software = row.is_active();
});
}
{
let jc = state.job_config.clone();
timestamps_row.connect_active_notify(move |row| {
jc.borrow_mut().strip_timestamps = row.is_active();
});
}
{
let jc = state.job_config.clone();
copyright_row.connect_active_notify(move |row| {
jc.borrow_mut().strip_copyright = row.is_active();
});
}
scrolled.set_child(Some(&content));
let page = adw::NavigationPage::builder()
.title("Metadata")
.tag("step-metadata")
.child(&scrolled)
.build();
// Sync enable toggle when navigating to this page
{
let jc = state.job_config.clone();
let er = enable_row.clone();
page.connect_map(move |_| {
let enabled = jc.borrow().metadata_enabled;
er.set_active(enabled);
});
}
page
}