Add welcome wizard, desktop entry, and Nautilus extension
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