Files
pixstrip/pixstrip-gtk/src/welcome.rs
lashman 7840765e5b Add File Manager Integration page to welcome wizard
Add fourth welcome wizard page that detects installed file managers
(Nautilus, Nemo, Thunar, Dolphin) and offers integration toggles for
each. Follows the design doc specification for the first-run experience.
2026-03-06 13:16:11 +02:00

335 lines
10 KiB
Rust

use adw::prelude::*;
pub fn show_welcome_if_first_launch(window: &adw::ApplicationWindow) {
let config = pixstrip_core::storage::ConfigStore::new();
if let Ok(cfg) = config.load()
&& cfg.first_run_complete
{
return;
}
let dialog = build_welcome_dialog();
let win = 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);
}
});
dialog.present(Some(&win));
}
fn build_welcome_dialog() -> adw::Dialog {
let dialog = adw::Dialog::builder()
.title("Welcome to Pixstrip")
.content_width(500)
.content_height(450)
.build();
let nav_view = adw::NavigationView::new();
let welcome_page = build_welcome_page(&nav_view);
nav_view.add(&welcome_page);
let skill_page = build_skill_page(&nav_view);
nav_view.add(&skill_page);
let fm_page = build_file_manager_page(&nav_view);
nav_view.add(&fm_page);
let output_page = build_output_page(&dialog);
nav_view.add(&output_page);
dialog.set_child(Some(&nav_view));
dialog
}
fn build_welcome_page(nav_view: &adw::NavigationView) -> adw::NavigationPage {
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.margin_top(24)
.margin_bottom(24)
.margin_start(24)
.margin_end(24)
.build();
let status = adw::StatusPage::builder()
.title("Welcome to Pixstrip")
.description("A quick and powerful batch image processor.\nResize, convert, compress, strip metadata, watermark, and rename - all in a simple wizard.")
.icon_name("image-x-generic-symbolic")
.vexpand(true)
.build();
let next_button = gtk::Button::builder()
.label("Get Started")
.halign(gtk::Align::Center)
.build();
next_button.add_css_class("suggested-action");
next_button.add_css_class("pill");
let nav = nav_view.clone();
next_button.connect_clicked(move |_| {
nav.push_by_tag("skill-level");
});
content.append(&status);
content.append(&next_button);
adw::NavigationPage::builder()
.title("Welcome")
.tag("welcome")
.child(&content)
.build()
}
fn build_skill_page(nav_view: &adw::NavigationView) -> adw::NavigationPage {
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.margin_top(24)
.margin_bottom(24)
.margin_start(24)
.margin_end(24)
.build();
let title = gtk::Label::builder()
.label("How much detail do you want?")
.css_classes(["title-2"])
.halign(gtk::Align::Start)
.build();
let subtitle = gtk::Label::builder()
.label("You can change this anytime in Settings.")
.css_classes(["dim-label"])
.halign(gtk::Align::Start)
.build();
let group = adw::PreferencesGroup::new();
let simple_row = adw::ActionRow::builder()
.title("Simple")
.subtitle("Fewer options visible, great for quick tasks. Advanced options are still available behind expanders.")
.activatable(true)
.build();
simple_row.add_prefix(&gtk::Image::from_icon_name("emblem-ok-symbolic"));
let simple_check = gtk::CheckButton::new();
simple_check.set_active(true);
simple_row.add_suffix(&simple_check);
simple_row.set_activatable_widget(Some(&simple_check));
let detailed_row = adw::ActionRow::builder()
.title("Detailed")
.subtitle("All options visible by default. Best for power users who want full control.")
.activatable(true)
.build();
detailed_row.add_prefix(&gtk::Image::from_icon_name("preferences-system-symbolic"));
let detailed_check = gtk::CheckButton::new();
detailed_check.set_group(Some(&simple_check));
detailed_row.add_suffix(&detailed_check);
detailed_row.set_activatable_widget(Some(&detailed_check));
group.add(&simple_row);
group.add(&detailed_row);
let next_button = gtk::Button::builder()
.label("Continue")
.halign(gtk::Align::Center)
.build();
next_button.add_css_class("suggested-action");
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("file-manager");
});
content.append(&title);
content.append(&subtitle);
content.append(&group);
content.append(&next_button);
adw::NavigationPage::builder()
.title("Skill Level")
.tag("skill-level")
.child(&content)
.build()
}
fn build_file_manager_page(nav_view: &adw::NavigationView) -> adw::NavigationPage {
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.margin_top(24)
.margin_bottom(24)
.margin_start(24)
.margin_end(24)
.build();
let title = gtk::Label::builder()
.label("File Manager Integration")
.css_classes(["title-2"])
.halign(gtk::Align::Start)
.build();
let subtitle = gtk::Label::builder()
.label("Add 'Process with Pixstrip' to your file manager's right-click menu.")
.css_classes(["dim-label"])
.halign(gtk::Align::Start)
.wrap(true)
.build();
let group = adw::PreferencesGroup::builder()
.title("Detected File Managers")
.description("Toggle integration for each file manager")
.build();
// Detect installed file managers
let file_managers = [
("Nautilus", "org.gnome.Nautilus", "system-file-manager-symbolic"),
("Nemo", "org.nemo.Nemo", "system-file-manager-symbolic"),
("Thunar", "thunar", "system-file-manager-symbolic"),
("Dolphin", "org.kde.dolphin", "system-file-manager-symbolic"),
];
let mut found_any = false;
for (name, desktop_id, icon) in &file_managers {
// Check if the file manager is installed by looking for its .desktop file
let is_installed = gtk::gio::AppInfo::all()
.iter()
.any(|info| {
info.id()
.map(|id| id.as_str().contains(desktop_id))
.unwrap_or(false)
});
if is_installed {
found_any = true;
let row = adw::SwitchRow::builder()
.title(*name)
.subtitle(format!("Add right-click menu to {}", name))
.active(false)
.build();
row.add_prefix(&gtk::Image::from_icon_name(icon));
group.add(&row);
}
}
if !found_any {
let row = adw::ActionRow::builder()
.title("No supported file managers found")
.subtitle("You can configure this later in Settings")
.build();
row.add_prefix(&gtk::Image::from_icon_name("dialog-information-symbolic"));
group.add(&row);
}
let next_button = gtk::Button::builder()
.label("Continue")
.halign(gtk::Align::Center)
.build();
next_button.add_css_class("suggested-action");
next_button.add_css_class("pill");
let nav = nav_view.clone();
next_button.connect_clicked(move |_| {
nav.push_by_tag("output-location");
});
content.append(&title);
content.append(&subtitle);
content.append(&group);
content.append(&next_button);
adw::NavigationPage::builder()
.title("File Manager")
.tag("file-manager")
.child(&content)
.build()
}
fn build_output_page(dialog: &adw::Dialog) -> adw::NavigationPage {
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.margin_top(24)
.margin_bottom(24)
.margin_start(24)
.margin_end(24)
.build();
let title = gtk::Label::builder()
.label("Where should processed images go?")
.css_classes(["title-2"])
.halign(gtk::Align::Start)
.build();
let subtitle = gtk::Label::builder()
.label("You can change this per-batch or in Settings.")
.css_classes(["dim-label"])
.halign(gtk::Align::Start)
.build();
let group = adw::PreferencesGroup::new();
let subfolder_row = adw::ActionRow::builder()
.title("Subfolder next to originals")
.subtitle("Creates a 'processed' folder next to your images")
.activatable(true)
.build();
subfolder_row.add_prefix(&gtk::Image::from_icon_name("folder-symbolic"));
let subfolder_check = gtk::CheckButton::new();
subfolder_check.set_active(true);
subfolder_row.add_suffix(&subfolder_check);
subfolder_row.set_activatable_widget(Some(&subfolder_check));
let fixed_row = adw::ActionRow::builder()
.title("Fixed output folder")
.subtitle("Always save to the same folder")
.activatable(true)
.build();
fixed_row.add_prefix(&gtk::Image::from_icon_name("folder-open-symbolic"));
let fixed_check = gtk::CheckButton::new();
fixed_check.set_group(Some(&subfolder_check));
fixed_row.add_suffix(&fixed_check);
fixed_row.set_activatable_widget(Some(&fixed_check));
group.add(&subfolder_row);
group.add(&fixed_row);
let done_button = gtk::Button::builder()
.label("Start Using Pixstrip")
.halign(gtk::Align::Center)
.build();
done_button.add_css_class("suggested-action");
done_button.add_css_class("pill");
let dlg = dialog.clone();
done_button.connect_clicked(move |_| {
dlg.close();
});
content.append(&title);
content.append(&subtitle);
content.append(&group);
content.append(&done_button);
adw::NavigationPage::builder()
.title("Output Location")
.tag("output-location")
.child(&content)
.build()
}