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:
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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: >k::Widget, count: usize) {
|
fn update_count_in_box(widget: >k::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: >k::Widget, files: &[std::path::PathBuf]) {
|
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::<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(>k::Image::from_icon_name("image-x-generic-symbolic"));
|
row.add_prefix(>k::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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user