From ced65f10ece09199eec25aabbf74fd8a2fb8049d Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 15:48:06 +0200 Subject: [PATCH] Add tutorial overlay tour after welcome wizard Semi-transparent dialog-based tour with 6 stops covering step indicator, workflow selection, image adding, navigation, menu, and a final ready message. Skippable at any time. State persisted via tutorial_complete flag in AppConfig. --- pixstrip-core/src/config.rs | 2 + pixstrip-gtk/src/main.rs | 1 + pixstrip-gtk/src/settings.rs | 1 + pixstrip-gtk/src/tutorial.rs | 195 +++++++++++++++++++++++++++++++++++ pixstrip-gtk/src/welcome.rs | 6 +- 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 pixstrip-gtk/src/tutorial.rs diff --git a/pixstrip-core/src/config.rs b/pixstrip-core/src/config.rs index f859827..aacc032 100644 --- a/pixstrip-core/src/config.rs +++ b/pixstrip-core/src/config.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; #[serde(default)] pub struct AppConfig { pub first_run_complete: bool, + pub tutorial_complete: bool, pub output_subfolder: String, pub output_fixed_path: Option, pub overwrite_behavior: OverwriteBehavior, @@ -25,6 +26,7 @@ impl Default for AppConfig { fn default() -> Self { Self { first_run_complete: false, + tutorial_complete: false, output_subfolder: "processed".into(), output_fixed_path: None, overwrite_behavior: OverwriteBehavior::Ask, diff --git a/pixstrip-gtk/src/main.rs b/pixstrip-gtk/src/main.rs index 3f925f9..c30ebcb 100644 --- a/pixstrip-gtk/src/main.rs +++ b/pixstrip-gtk/src/main.rs @@ -3,6 +3,7 @@ mod processing; mod settings; mod step_indicator; mod steps; +mod tutorial; mod welcome; mod wizard; diff --git a/pixstrip-gtk/src/settings.rs b/pixstrip-gtk/src/settings.rs index e7c6329..110c71e 100644 --- a/pixstrip-gtk/src/settings.rs +++ b/pixstrip-gtk/src/settings.rs @@ -291,6 +291,7 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { dialog.connect_closed(move |_| { let new_config = AppConfig { first_run_complete: true, + tutorial_complete: true, // preserve if settings are being saved output_subfolder: subfolder_row.text().to_string(), output_fixed_path: None, overwrite_behavior: match overwrite_row.selected() { diff --git a/pixstrip-gtk/src/tutorial.rs b/pixstrip-gtk/src/tutorial.rs new file mode 100644 index 0000000..bcc5c64 --- /dev/null +++ b/pixstrip-gtk/src/tutorial.rs @@ -0,0 +1,195 @@ +use adw::prelude::*; +use gtk::glib; + +struct TourStop { + title: &'static str, + description: &'static str, + icon: &'static str, +} + +const TOUR_STOPS: &[TourStop] = &[ + TourStop { + title: "Step Indicator", + description: "This bar shows your progress through the wizard. Click any completed step to jump back to it.", + icon: "view-list-symbolic", + }, + TourStop { + title: "Choose a Workflow", + description: "Start by picking a preset that matches what you need, or build a custom workflow from scratch.", + icon: "applications-graphics-symbolic", + }, + TourStop { + title: "Add Your Images", + description: "Drag and drop files here, or use the Add button. You can paste from the clipboard too.", + icon: "image-x-generic-symbolic", + }, + TourStop { + title: "Navigation", + description: "Use the Back and Next buttons to move between steps, or press Alt+Left/Right. Disabled steps are automatically skipped.", + icon: "go-next-symbolic", + }, + TourStop { + title: "Main Menu", + description: "Access settings, keyboard shortcuts, processing history, and preset management from here.", + icon: "open-menu-symbolic", + }, + TourStop { + title: "You're Ready!", + description: "That's everything you need to know. Each step also has a help button (?) in the header bar for detailed guidance.", + icon: "emblem-ok-symbolic", + }, +]; + +/// Show the tutorial overlay if the user hasn't completed it yet. +/// Called after the welcome wizard closes on first launch. +pub fn show_tutorial_if_needed(window: &adw::ApplicationWindow) { + let config_store = pixstrip_core::storage::ConfigStore::new(); + if let Ok(cfg) = config_store.load() + && cfg.tutorial_complete + { + return; + } + + // Small delay to let the welcome dialog fully dismiss + let win = window.clone(); + glib::timeout_add_local_once(std::time::Duration::from_millis(400), move || { + show_tour_dialog(&win, 0); + }); +} + +fn show_tour_dialog(window: &adw::ApplicationWindow, stop_index: usize) { + let stop = &TOUR_STOPS[stop_index]; + let total = TOUR_STOPS.len(); + + let dialog = adw::Dialog::builder() + .title("Quick Tour") + .content_width(420) + .content_height(300) + .build(); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(16) + .margin_top(24) + .margin_bottom(24) + .margin_start(24) + .margin_end(24) + .build(); + + // Progress dots + let dots_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(6) + .halign(gtk::Align::Center) + .build(); + + for i in 0..total { + let dot = gtk::Label::builder() + .label(if i == stop_index { "\u{25CF}" } else { "\u{25CB}" }) + .build(); + if i == stop_index { + dot.add_css_class("accent"); + } else { + dot.add_css_class("dim-label"); + } + dots_box.append(&dot); + } + content.append(&dots_box); + + // Icon + let icon = gtk::Image::builder() + .icon_name(stop.icon) + .pixel_size(64) + .halign(gtk::Align::Center) + .build(); + icon.add_css_class("accent"); + content.append(&icon); + + // Title + let title = gtk::Label::builder() + .label(stop.title) + .css_classes(["title-2"]) + .halign(gtk::Align::Center) + .build(); + content.append(&title); + + // Step counter + let counter = gtk::Label::builder() + .label(&format!("{} of {}", stop_index + 1, total)) + .css_classes(["dim-label", "caption"]) + .halign(gtk::Align::Center) + .build(); + content.append(&counter); + + // Description + let desc = gtk::Label::builder() + .label(stop.description) + .wrap(true) + .halign(gtk::Align::Center) + .justify(gtk::Justification::Center) + .build(); + content.append(&desc); + + // Buttons + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(12) + .halign(gtk::Align::Center) + .margin_top(8) + .build(); + + let skip_button = gtk::Button::builder() + .label("Skip Tour") + .build(); + skip_button.add_css_class("flat"); + + let is_last = stop_index + 1 >= total; + let next_label = if is_last { "Get Started" } else { "Next" }; + let next_button = gtk::Button::builder() + .label(next_label) + .build(); + next_button.add_css_class("suggested-action"); + next_button.add_css_class("pill"); + + button_box.append(&skip_button); + button_box.append(&next_button); + content.append(&button_box); + + dialog.set_child(Some(&content)); + + // Wire skip - mark tutorial complete and close + { + let dlg = dialog.clone(); + skip_button.connect_clicked(move |_| { + mark_tutorial_complete(); + dlg.close(); + }); + } + + // Wire next + { + let dlg = dialog.clone(); + let win = window.clone(); + next_button.connect_clicked(move |_| { + dlg.close(); + if is_last { + mark_tutorial_complete(); + } else { + let w = win.clone(); + glib::idle_add_local_once(move || { + show_tour_dialog(&w, stop_index + 1); + }); + } + }); + } + + dialog.present(Some(window)); +} + +fn mark_tutorial_complete() { + let config_store = pixstrip_core::storage::ConfigStore::new(); + if let Ok(mut cfg) = config_store.load() { + cfg.tutorial_complete = true; + let _ = config_store.save(&cfg); + } +} diff --git a/pixstrip-gtk/src/welcome.rs b/pixstrip-gtk/src/welcome.rs index fdff6db..670cb53 100644 --- a/pixstrip-gtk/src/welcome.rs +++ b/pixstrip-gtk/src/welcome.rs @@ -10,14 +10,18 @@ pub fn show_welcome_if_first_launch(window: &adw::ApplicationWindow) { let dialog = build_welcome_dialog(); let win = window.clone(); + let win_for_present = window.clone(); dialog.connect_closed(move |_| { let config = pixstrip_core::storage::ConfigStore::new(); if let Ok(mut cfg) = config.load() { cfg.first_run_complete = true; let _ = config.save(&cfg); } + + // Launch the tutorial tour after the welcome wizard + crate::tutorial::show_tutorial_if_needed(&win); }); - dialog.present(Some(&win)); + dialog.present(Some(&win_for_present)); } fn build_welcome_dialog() -> adw::Dialog {