From e969c4165ecbcc6c386b22f4361c93652b9ab5f0 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 12:27:19 +0200 Subject: [PATCH] Add window persistence, clickable step indicator, file list improvements - Window size/position remembered between sessions via SessionStore - Step indicator dots now clickable to navigate directly to that step, with keyboard shortcut hints in tooltips - File list in add-files dialog shows format and size per image, header shows total count and total size - Welcome dialog now saves skill level choice to config - SessionState extended with window_width, window_height, window_maximized --- pixstrip-core/src/storage.rs | 3 ++ pixstrip-core/tests/storage_tests.rs | 3 ++ pixstrip-gtk/src/app.rs | 79 ++++++++++++++++++++-------- pixstrip-gtk/src/step_indicator.rs | 5 +- pixstrip-gtk/src/welcome.rs | 11 ++++ 5 files changed, 78 insertions(+), 23 deletions(-) diff --git a/pixstrip-core/src/storage.rs b/pixstrip-core/src/storage.rs index 31546cb..8fa50a5 100644 --- a/pixstrip-core/src/storage.rs +++ b/pixstrip-core/src/storage.rs @@ -172,6 +172,9 @@ pub struct SessionState { pub last_output_dir: Option, pub last_preset_name: Option, pub current_step: u32, + pub window_width: Option, + pub window_height: Option, + pub window_maximized: bool, } pub struct SessionStore { diff --git a/pixstrip-core/tests/storage_tests.rs b/pixstrip-core/tests/storage_tests.rs index 7bdaf05..6a331d7 100644 --- a/pixstrip-core/tests/storage_tests.rs +++ b/pixstrip-core/tests/storage_tests.rs @@ -160,6 +160,9 @@ fn save_and_load_session() { last_output_dir: Some("/home/user/processed".into()), last_preset_name: Some("Blog Photos".into()), current_step: 3, + window_width: Some(1024), + window_height: Some(768), + window_maximized: false, }; session_store.save(&session).unwrap(); diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index ba2d888..7ca540f 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -236,15 +236,40 @@ fn build_ui(app: &adw::Application) { let toast_overlay = adw::ToastOverlay::new(); toast_overlay.set_child(Some(&toolbar_view)); - // Window + // Restore window size from session + let session = pixstrip_core::storage::SessionStore::new(); + let session_state = session.load().unwrap_or_default(); + let win_width = session_state.window_width.unwrap_or(900); + let win_height = session_state.window_height.unwrap_or(700); + let window = adw::ApplicationWindow::builder() .application(app) - .default_width(900) - .default_height(700) + .default_width(win_width) + .default_height(win_height) .content(&toast_overlay) .title("Pixstrip") .build(); + if session_state.window_maximized { + window.maximize(); + } + + // Save window size on close + window.connect_close_request(|win| { + let session = pixstrip_core::storage::SessionStore::new(); + let mut state = session.load().unwrap_or_default(); + state.window_maximized = win.is_maximized(); + if !win.is_maximized() { + let (w, h) = (win.default_size().0, win.default_size().1); + if w > 0 && h > 0 { + state.window_width = Some(w); + state.window_height = Some(h); + } + } + let _ = session.save(&state); + glib::Propagation::Proceed + }); + let ui = WizardUi { nav_view, step_indicator, @@ -577,59 +602,69 @@ fn update_output_label(ui: &WizardUi, path: &std::path::Path) { } fn update_images_count_label(ui: &WizardUi, count: usize) { + let files = ui.state.loaded_files.borrow(); + let total_size: u64 = files.iter() + .filter_map(|p| std::fs::metadata(p).ok()) + .map(|m| m.len()) + .sum(); + // Find the step-images page and switch its stack to "loaded" if we have files if let Some(page) = ui.pages.get(1) && let Some(stack) = page.child().and_downcast::() { if count > 0 { stack.set_visible_child_name("loaded"); + } else { + stack.set_visible_child_name("empty"); } if let Some(loaded_box) = stack.child_by_name("loaded") { - update_count_in_box(&loaded_box, count); - // Also update the file list - update_file_list(&loaded_box, &ui.state.loaded_files.borrow()); + update_count_in_box(&loaded_box, count, total_size); + update_file_list(&loaded_box, &files); } } } -fn update_count_in_box(widget: >k::Widget, count: usize) { +fn update_count_in_box(widget: >k::Widget, count: usize, total_size: u64) { if let Some(label) = widget.downcast_ref::() && label.css_classes().iter().any(|c| c == "heading") { - label.set_label(&format!("{} images loaded", count)); + label.set_label(&format!("{} images ({})", count, format_bytes(total_size))); return; } - if let Some(bx) = widget.downcast_ref::() { - let mut child = bx.first_child(); - while let Some(c) = child { - update_count_in_box(&c, count); - child = c.next_sibling(); - } + let mut child = widget.first_child(); + while let Some(c) = child { + update_count_in_box(&c, count, total_size); + child = c.next_sibling(); } } fn update_file_list(widget: >k::Widget, files: &[std::path::PathBuf]) { - // Find the ListBox inside the loaded state and populate it - if let Some(list_box) = widget.downcast_ref::() { - // Clear existing rows + if let Some(list_box) = widget.downcast_ref::() + && list_box.css_classes().iter().any(|c| c == "boxed-list") + { while let Some(child) = list_box.first_child() { list_box.remove(&child); } - // Add rows for each file for path in files { - let filename = path.file_name() + let name = path.file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown"); + let size = std::fs::metadata(path) + .map(|m| format_bytes(m.len())) + .unwrap_or_default(); + let ext = path.extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_uppercase(); let row = adw::ActionRow::builder() - .title(filename) - .subtitle(path.parent().map(|p| p.display().to_string()).unwrap_or_default()) + .title(name) + .subtitle(format!("{} - {}", ext, size)) .build(); row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic")); list_box.append(&row); } return; } - // Recurse into containers let mut child = widget.first_child(); while let Some(c) = child { update_file_list(&c, files); diff --git a/pixstrip-gtk/src/step_indicator.rs b/pixstrip-gtk/src/step_indicator.rs index 5ae6e59..3409989 100644 --- a/pixstrip-gtk/src/step_indicator.rs +++ b/pixstrip-gtk/src/step_indicator.rs @@ -1,4 +1,5 @@ use gtk::prelude::*; +use gtk::glib::prelude::ToVariant; use std::cell::RefCell; #[derive(Clone)] @@ -54,8 +55,10 @@ impl StepIndicator { let button = gtk::Button::builder() .child(&icon) .has_frame(false) - .tooltip_text(format!("Step {}: {}", i + 1, name)) + .tooltip_text(format!("Step {}: {} (Alt+{})", i + 1, name, i + 1)) .sensitive(false) + .action_name("win.goto-step") + .action_target(&(i as i32 + 1).to_variant()) .build(); button.add_css_class("circular"); diff --git a/pixstrip-gtk/src/welcome.rs b/pixstrip-gtk/src/welcome.rs index b415ea3..bf6fccc 100644 --- a/pixstrip-gtk/src/welcome.rs +++ b/pixstrip-gtk/src/welcome.rs @@ -138,7 +138,18 @@ fn build_skill_page(nav_view: &adw::NavigationView) -> adw::NavigationPage { next_button.add_css_class("pill"); let nav = nav_view.clone(); + let sc = simple_check.clone(); next_button.connect_clicked(move |_| { + // Save skill level choice + let store = pixstrip_core::storage::ConfigStore::new(); + if let Ok(mut cfg) = store.load() { + cfg.skill_level = if sc.is_active() { + pixstrip_core::config::SkillLevel::Simple + } else { + pixstrip_core::config::SkillLevel::Detailed + }; + let _ = store.save(&cfg); + } nav.push_by_tag("output-location"); });