Files
pixstrip/pixstrip-gtk/src/steps/step_workflow.rs

325 lines
9.5 KiB
Rust

use adw::prelude::*;
use pixstrip_core::preset::Preset;
use pixstrip_core::operations::*;
use crate::app::{AppState, JobConfig, MetadataMode};
pub fn build_workflow_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();
// Built-in presets section
let builtin_group = adw::PreferencesGroup::builder()
.title("Built-in Workflows")
.description("Select a preset to get started quickly")
.build();
let builtin_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)
.build();
let builtins = Preset::all_builtins();
for preset in &builtins {
let card = build_preset_card(preset);
builtin_flow.append(&card);
}
// When a preset card is activated, apply it to JobConfig and advance
{
let jc = state.job_config.clone();
builtin_flow.connect_child_activated(move |flow, child| {
let idx = child.index() as usize;
if let Some(preset) = builtins.get(idx) {
apply_preset_to_config(&mut jc.borrow_mut(), preset);
}
flow.activate_action("win.next-step", None).ok();
});
}
builtin_group.add(&builtin_flow);
content.append(&builtin_group);
// Custom workflow section
let custom_group = adw::PreferencesGroup::builder()
.title("Custom Workflow")
.description("Choose which operations to include")
.build();
let custom_card = build_custom_card();
let custom_flow = gtk::FlowBox::builder()
.selection_mode(gtk::SelectionMode::None)
.max_children_per_line(4)
.min_children_per_line(2)
.build();
custom_flow.append(&custom_card);
custom_flow.connect_child_activated(|flow, _child| {
flow.activate_action("win.next-step", None).ok();
});
custom_group.add(&custom_flow);
content.append(&custom_group);
// User presets section
let user_group = adw::PreferencesGroup::builder()
.title("Your Presets")
.description("Import or save your own workflows")
.build();
// Show saved user presets
let store = pixstrip_core::storage::PresetStore::new();
if let Ok(presets) = store.list() {
for preset in &presets {
if !preset.is_custom {
continue;
}
let row = adw::ActionRow::builder()
.title(&preset.name)
.subtitle(&preset.description)
.activatable(true)
.build();
row.add_prefix(&gtk::Image::from_icon_name(&preset.icon));
row.add_suffix(&gtk::Image::from_icon_name("go-next-symbolic"));
let jc = state.job_config.clone();
let p = preset.clone();
row.connect_activated(move |r| {
apply_preset_to_config(&mut jc.borrow_mut(), &p);
r.activate_action("win.next-step", None).ok();
});
user_group.add(&row);
}
}
let import_button = gtk::Button::builder()
.label("Import Preset")
.icon_name("document-open-symbolic")
.action_name("win.import-preset")
.build();
import_button.add_css_class("flat");
user_group.add(&import_button);
content.append(&user_group);
scrolled.set_child(Some(&content));
let clamp = adw::Clamp::builder()
.maximum_size(800)
.child(&scrolled)
.build();
adw::NavigationPage::builder()
.title("Choose a Workflow")
.tag("step-workflow")
.child(&clamp)
.build()
}
fn apply_preset_to_config(cfg: &mut JobConfig, preset: &Preset) {
// Resize
match &preset.resize {
Some(ResizeConfig::ByWidth(w)) => {
cfg.resize_enabled = true;
cfg.resize_width = *w;
cfg.resize_height = 0;
cfg.allow_upscale = false;
}
Some(ResizeConfig::ByHeight(h)) => {
cfg.resize_enabled = true;
cfg.resize_width = 0;
cfg.resize_height = *h;
cfg.allow_upscale = false;
}
Some(ResizeConfig::FitInBox { max, allow_upscale }) => {
cfg.resize_enabled = true;
cfg.resize_width = max.width;
cfg.resize_height = max.height;
cfg.allow_upscale = *allow_upscale;
}
Some(ResizeConfig::Exact(dims)) => {
cfg.resize_enabled = true;
cfg.resize_width = dims.width;
cfg.resize_height = dims.height;
cfg.allow_upscale = true;
}
None => {
cfg.resize_enabled = false;
}
}
// Convert
match &preset.convert {
Some(ConvertConfig::SingleFormat(fmt)) => {
cfg.convert_enabled = true;
cfg.convert_format = Some(*fmt);
}
Some(_) => {
cfg.convert_enabled = true;
cfg.convert_format = None;
}
None => {
cfg.convert_enabled = false;
cfg.convert_format = None;
}
}
// Compress
match &preset.compress {
Some(CompressConfig::Preset(q)) => {
cfg.compress_enabled = true;
cfg.quality_preset = *q;
}
Some(CompressConfig::Custom { jpeg_quality, png_level, webp_quality, .. }) => {
cfg.compress_enabled = true;
if let Some(jq) = jpeg_quality {
cfg.jpeg_quality = *jq;
}
if let Some(pl) = png_level {
cfg.png_level = *pl;
}
if let Some(wq) = webp_quality {
cfg.webp_quality = *wq as u8;
}
}
None => {
cfg.compress_enabled = false;
}
}
// Metadata
match &preset.metadata {
Some(MetadataConfig::StripAll) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::StripAll;
}
Some(MetadataConfig::Privacy) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::Privacy;
}
Some(MetadataConfig::KeepAll) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::KeepAll;
}
Some(MetadataConfig::Custom { strip_gps, strip_camera, strip_software, strip_timestamps, strip_copyright }) => {
cfg.metadata_enabled = true;
cfg.metadata_mode = MetadataMode::Custom;
cfg.strip_gps = *strip_gps;
cfg.strip_camera = *strip_camera;
cfg.strip_software = *strip_software;
cfg.strip_timestamps = *strip_timestamps;
cfg.strip_copyright = *strip_copyright;
}
None => {
cfg.metadata_enabled = false;
}
}
}
fn build_preset_card(preset: &Preset) -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.halign(gtk::Align::Center)
.valign(gtk::Align::Start)
.build();
card.add_css_class("card");
card.set_size_request(180, 120);
let inner = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.margin_top(16)
.margin_bottom(16)
.margin_start(12)
.margin_end(12)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.vexpand(true)
.build();
let icon = gtk::Image::builder()
.icon_name(&preset.icon)
.pixel_size(32)
.build();
let name_label = gtk::Label::builder()
.label(&preset.name)
.css_classes(["heading"])
.build();
let desc_label = gtk::Label::builder()
.label(&preset.description)
.css_classes(["caption", "dim-label"])
.wrap(true)
.justify(gtk::Justification::Center)
.max_width_chars(20)
.build();
inner.append(&icon);
inner.append(&name_label);
inner.append(&desc_label);
card.append(&inner);
card
}
fn build_custom_card() -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.halign(gtk::Align::Center)
.valign(gtk::Align::Start)
.build();
card.add_css_class("card");
card.set_size_request(180, 120);
let inner = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.margin_top(16)
.margin_bottom(16)
.margin_start(12)
.margin_end(12)
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.vexpand(true)
.build();
let icon = gtk::Image::builder()
.icon_name("applications-system-symbolic")
.pixel_size(32)
.build();
let name_label = gtk::Label::builder()
.label("Custom...")
.css_classes(["heading"])
.build();
let desc_label = gtk::Label::builder()
.label("Pick your own operations")
.css_classes(["caption", "dim-label"])
.wrap(true)
.justify(gtk::Justification::Center)
.build();
inner.append(&icon);
inner.append(&name_label);
inner.append(&desc_label);
card.append(&inner);
card
}