diff --git a/data/live.lashman.Pixstrip.desktop b/data/live.lashman.Pixstrip.desktop new file mode 100644 index 0000000..bbc19e8 --- /dev/null +++ b/data/live.lashman.Pixstrip.desktop @@ -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 diff --git a/data/nautilus-pixstrip.py b/data/nautilus-pixstrip.py new file mode 100644 index 0000000..112628e --- /dev/null +++ b/data/nautilus-pixstrip.py @@ -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] + ) diff --git a/pixstrip-gtk/src/main.rs b/pixstrip-gtk/src/main.rs index eb3d59b..3f925f9 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 welcome; mod wizard; use gtk::prelude::*; diff --git a/pixstrip-gtk/src/welcome.rs b/pixstrip-gtk/src/welcome.rs new file mode 100644 index 0000000..0855494 --- /dev/null +++ b/pixstrip-gtk/src/welcome.rs @@ -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() +}