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

@@ -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::<gtk::Stack>()
{
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: &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>()
&& 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::<gtk::Box>() {
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: &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>() {
// Clear existing rows
if let Some(list_box) = widget.downcast_ref::<gtk::ListBox>()
&& 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(&gtk::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);

View File

@@ -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");

View File

@@ -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");
});