Add welcome wizard, desktop entry, and Nautilus extension
First-run welcome dialog with skill level and output location setup. Desktop entry file for GNOME app launcher integration. Nautilus Python extension with dynamic 'Process with Pixstrip' submenu that reads both built-in and user presets.
This commit is contained in:
11
data/live.lashman.Pixstrip.desktop
Normal file
11
data/live.lashman.Pixstrip.desktop
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Pixstrip
|
||||||
|
Comment=Batch image processor - resize, convert, compress, and more
|
||||||
|
Exec=pixstrip-gtk %F
|
||||||
|
Icon=live.lashman.Pixstrip
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Graphics;ImageProcessing;
|
||||||
|
MimeType=image/jpeg;image/png;image/webp;image/avif;image/gif;image/tiff;image/bmp;
|
||||||
|
Keywords=image;photo;resize;convert;compress;batch;metadata;strip;watermark;rename;
|
||||||
|
StartupNotify=true
|
||||||
112
data/nautilus-pixstrip.py
Normal file
112
data/nautilus-pixstrip.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""Nautilus extension for Pixstrip - adds 'Process with Pixstrip' submenu to image context menu."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
from gi.repository import Nautilus, GObject
|
||||||
|
|
||||||
|
SUPPORTED_MIMETYPES = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/avif",
|
||||||
|
"image/gif",
|
||||||
|
"image/tiff",
|
||||||
|
"image/bmp",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_presets():
|
||||||
|
"""Load built-in and user presets."""
|
||||||
|
presets = [
|
||||||
|
"Blog Photos",
|
||||||
|
"Social Media",
|
||||||
|
"Web Optimization",
|
||||||
|
"Email Friendly",
|
||||||
|
"Privacy Clean",
|
||||||
|
"Photographer Export",
|
||||||
|
"Archive Compress",
|
||||||
|
"Fediverse Ready",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Load user presets
|
||||||
|
config_dir = os.path.expanduser("~/.config/pixstrip/presets")
|
||||||
|
if os.path.isdir(config_dir):
|
||||||
|
for filename in sorted(os.listdir(config_dir)):
|
||||||
|
if filename.endswith(".json"):
|
||||||
|
filepath = os.path.join(config_dir, filename)
|
||||||
|
try:
|
||||||
|
with open(filepath) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
name = data.get("name", "")
|
||||||
|
if name and name not in presets:
|
||||||
|
presets.append(name)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return presets
|
||||||
|
|
||||||
|
|
||||||
|
class PixstripMenuProvider(GObject.GObject, Nautilus.MenuProvider):
|
||||||
|
def get_file_items(self, files):
|
||||||
|
# Only show for image files
|
||||||
|
if not files:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
if f.get_mime_type() not in SUPPORTED_MIMETYPES:
|
||||||
|
return []
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# Main menu item
|
||||||
|
top_item = Nautilus.MenuItem(
|
||||||
|
name="PixstripMenuProvider::process",
|
||||||
|
label="Process with Pixstrip",
|
||||||
|
tip="Open images in Pixstrip for batch processing",
|
||||||
|
)
|
||||||
|
|
||||||
|
submenu = Nautilus.Menu()
|
||||||
|
top_item.set_submenu(submenu)
|
||||||
|
|
||||||
|
# Open in Pixstrip
|
||||||
|
open_item = Nautilus.MenuItem(
|
||||||
|
name="PixstripMenuProvider::open",
|
||||||
|
label="Open in Pixstrip...",
|
||||||
|
tip="Open the Pixstrip wizard with these images",
|
||||||
|
)
|
||||||
|
open_item.connect("activate", self._on_open, files)
|
||||||
|
submenu.append_item(open_item)
|
||||||
|
|
||||||
|
# Separator via disabled item
|
||||||
|
sep = Nautilus.MenuItem(
|
||||||
|
name="PixstripMenuProvider::sep1",
|
||||||
|
label="---",
|
||||||
|
sensitive=False,
|
||||||
|
)
|
||||||
|
submenu.append_item(sep)
|
||||||
|
|
||||||
|
# Preset items
|
||||||
|
for preset in get_presets():
|
||||||
|
safe_name = preset.replace(" ", "_").lower()
|
||||||
|
item = Nautilus.MenuItem(
|
||||||
|
name=f"PixstripMenuProvider::preset_{safe_name}",
|
||||||
|
label=preset,
|
||||||
|
tip=f"Process with {preset} preset",
|
||||||
|
)
|
||||||
|
item.connect("activate", self._on_preset, files, preset)
|
||||||
|
submenu.append_item(item)
|
||||||
|
|
||||||
|
items.append(top_item)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def _on_open(self, menu, files):
|
||||||
|
paths = [f.get_location().get_path() for f in files]
|
||||||
|
subprocess.Popen(["pixstrip-gtk"] + paths)
|
||||||
|
|
||||||
|
def _on_preset(self, menu, files, preset):
|
||||||
|
paths = [f.get_location().get_path() for f in files]
|
||||||
|
subprocess.Popen(
|
||||||
|
["pixstrip-cli", "process"] + paths + ["--preset", preset]
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@ mod processing;
|
|||||||
mod settings;
|
mod settings;
|
||||||
mod step_indicator;
|
mod step_indicator;
|
||||||
mod steps;
|
mod steps;
|
||||||
|
mod welcome;
|
||||||
mod wizard;
|
mod wizard;
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|||||||
197
pixstrip-gtk/src/welcome.rs
Normal file
197
pixstrip-gtk/src/welcome.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub 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();
|
||||||
|
|
||||||
|
// Page 1: Welcome
|
||||||
|
let welcome_page = build_welcome_page();
|
||||||
|
nav_view.add(&welcome_page);
|
||||||
|
|
||||||
|
// Page 2: Skill level
|
||||||
|
let skill_page = build_skill_page();
|
||||||
|
nav_view.add(&skill_page);
|
||||||
|
|
||||||
|
// Page 3: Output defaults
|
||||||
|
let output_page = build_output_page();
|
||||||
|
nav_view.add(&output_page);
|
||||||
|
|
||||||
|
dialog.set_child(Some(&nav_view));
|
||||||
|
dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_welcome_page() -> 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");
|
||||||
|
|
||||||
|
content.append(&status);
|
||||||
|
content.append(&next_button);
|
||||||
|
|
||||||
|
adw::NavigationPage::builder()
|
||||||
|
.title("Welcome")
|
||||||
|
.tag("welcome")
|
||||||
|
.child(&content)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_skill_page() -> 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(>k::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(>k::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");
|
||||||
|
|
||||||
|
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_output_page() -> 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(>k::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(>k::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");
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user