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
This commit is contained in:
2026-03-06 12:27:19 +02:00
parent 4fc4ea7017
commit e969c4165e
5 changed files with 78 additions and 23 deletions

View File

@@ -172,6 +172,9 @@ pub struct SessionState {
pub last_output_dir: Option<String>, pub last_output_dir: Option<String>,
pub last_preset_name: Option<String>, pub last_preset_name: Option<String>,
pub current_step: u32, pub current_step: u32,
pub window_width: Option<i32>,
pub window_height: Option<i32>,
pub window_maximized: bool,
} }
pub struct SessionStore { pub struct SessionStore {

View File

@@ -160,6 +160,9 @@ fn save_and_load_session() {
last_output_dir: Some("/home/user/processed".into()), last_output_dir: Some("/home/user/processed".into()),
last_preset_name: Some("Blog Photos".into()), last_preset_name: Some("Blog Photos".into()),
current_step: 3, current_step: 3,
window_width: Some(1024),
window_height: Some(768),
window_maximized: false,
}; };
session_store.save(&session).unwrap(); session_store.save(&session).unwrap();

View File

@@ -236,15 +236,40 @@ fn build_ui(app: &adw::Application) {
let toast_overlay = adw::ToastOverlay::new(); let toast_overlay = adw::ToastOverlay::new();
toast_overlay.set_child(Some(&toolbar_view)); 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() let window = adw::ApplicationWindow::builder()
.application(app) .application(app)
.default_width(900) .default_width(win_width)
.default_height(700) .default_height(win_height)
.content(&toast_overlay) .content(&toast_overlay)
.title("Pixstrip") .title("Pixstrip")
.build(); .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 { let ui = WizardUi {
nav_view, nav_view,
step_indicator, 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) { 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 // Find the step-images page and switch its stack to "loaded" if we have files
if let Some(page) = ui.pages.get(1) if let Some(page) = ui.pages.get(1)
&& let Some(stack) = page.child().and_downcast::<gtk::Stack>() && let Some(stack) = page.child().and_downcast::<gtk::Stack>()
{ {
if count > 0 { if count > 0 {
stack.set_visible_child_name("loaded"); stack.set_visible_child_name("loaded");
} else {
stack.set_visible_child_name("empty");
} }
if let Some(loaded_box) = stack.child_by_name("loaded") { if let Some(loaded_box) = stack.child_by_name("loaded") {
update_count_in_box(&loaded_box, count); update_count_in_box(&loaded_box, count, total_size);
// Also update the file list update_file_list(&loaded_box, &files);
update_file_list(&loaded_box, &ui.state.loaded_files.borrow());
} }
} }
} }
fn update_count_in_box(widget: &gtk::Widget, count: usize) { fn update_count_in_box(widget: &gtk::Widget, count: usize, total_size: u64) {
if let Some(label) = widget.downcast_ref::<gtk::Label>() if let Some(label) = widget.downcast_ref::<gtk::Label>()
&& label.css_classes().iter().any(|c| c == "heading") && 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; return;
} }
if let Some(bx) = widget.downcast_ref::<gtk::Box>() { let mut child = widget.first_child();
let mut child = bx.first_child();
while let Some(c) = child { while let Some(c) = child {
update_count_in_box(&c, count); update_count_in_box(&c, count, total_size);
child = c.next_sibling(); child = c.next_sibling();
} }
}
} }
fn update_file_list(widget: &gtk::Widget, files: &[std::path::PathBuf]) { fn update_file_list(widget: &gtk::Widget, files: &[std::path::PathBuf]) {
// Find the ListBox inside the loaded state and populate it if let Some(list_box) = widget.downcast_ref::<gtk::ListBox>()
if let Some(list_box) = widget.downcast_ref::<gtk::ListBox>() { && list_box.css_classes().iter().any(|c| c == "boxed-list")
// Clear existing rows {
while let Some(child) = list_box.first_child() { while let Some(child) = list_box.first_child() {
list_box.remove(&child); list_box.remove(&child);
} }
// Add rows for each file
for path in files { for path in files {
let filename = path.file_name() let name = path.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("unknown"); .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() let row = adw::ActionRow::builder()
.title(filename) .title(name)
.subtitle(path.parent().map(|p| p.display().to_string()).unwrap_or_default()) .subtitle(format!("{} - {}", ext, size))
.build(); .build();
row.add_prefix(&gtk::Image::from_icon_name("image-x-generic-symbolic")); row.add_prefix(&gtk::Image::from_icon_name("image-x-generic-symbolic"));
list_box.append(&row); list_box.append(&row);
} }
return; return;
} }
// Recurse into containers
let mut child = widget.first_child(); let mut child = widget.first_child();
while let Some(c) = child { while let Some(c) = child {
update_file_list(&c, files); update_file_list(&c, files);

View File

@@ -1,4 +1,5 @@
use gtk::prelude::*; use gtk::prelude::*;
use gtk::glib::prelude::ToVariant;
use std::cell::RefCell; use std::cell::RefCell;
#[derive(Clone)] #[derive(Clone)]
@@ -54,8 +55,10 @@ impl StepIndicator {
let button = gtk::Button::builder() let button = gtk::Button::builder()
.child(&icon) .child(&icon)
.has_frame(false) .has_frame(false)
.tooltip_text(format!("Step {}: {}", i + 1, name)) .tooltip_text(format!("Step {}: {} (Alt+{})", i + 1, name, i + 1))
.sensitive(false) .sensitive(false)
.action_name("win.goto-step")
.action_target(&(i as i32 + 1).to_variant())
.build(); .build();
button.add_css_class("circular"); button.add_css_class("circular");

View File

@@ -138,7 +138,18 @@ fn build_skill_page(nav_view: &adw::NavigationView) -> adw::NavigationPage {
next_button.add_css_class("pill"); next_button.add_css_class("pill");
let nav = nav_view.clone(); let nav = nav_view.clone();
let sc = simple_check.clone();
next_button.connect_clicked(move |_| { 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"); nav.push_by_tag("output-location");
}); });