From da90e0da81c09906de1ccbe1e825c3075ede84bf Mon Sep 17 00:00:00 2001 From: brian Date: Sat, 16 May 2026 16:08:25 -0400 Subject: [PATCH] work in progress --- .gitignore | 1 + README.md | 80 ++++++ default.nix | 4 + pixlit-128.png | Bin 0 -> 5727 bytes pixlit-256.png | Bin 0 -> 12481 bytes pixlit-512.png | Bin 0 -> 28763 bytes pixlit.nix | 77 ++++++ pixlit.py | 717 +++++++++++++++++++++++++++++++++++++++++++++++++ pixlit.svg | 104 +++++++ 9 files changed, 983 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 default.nix create mode 100644 pixlit-128.png create mode 100644 pixlit-256.png create mode 100644 pixlit-512.png create mode 100644 pixlit.nix create mode 100644 pixlit.py create mode 100644 pixlit.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2f5dd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d709357 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Pixlit — Image Converter + +A clean GTK4/libadwaita desktop application for converting images between formats, with first-class support for HEIC/HEIF files from iPhones and modern cameras. + +## Features + +- **HEIC / HEIF → JPEG, PNG, WebP** (and vice-versa) +- Supports: HEIC, HEIF, JPEG, PNG, WebP, BMP, TIFF, GIF as inputs +- Output formats: **JPEG**, **PNG**, **WebP** +- Quality slider (1–100) with per-format behaviour +- Batch conversion — add as many files as you like +- Drag-and-drop file support +- Output directory picker +- Progress tracking per file +- Native GNOME look via libadwaita + +## Building + +### Quick local build + +```bash +cd pixlit-nix +nix-build +./result/bin/pixlit +``` + +### Installing into your profile + +```bash +nix-env -f . -i +pixlit +``` + +### NixOS system package (flake or configuration.nix) + +```nix +# configuration.nix +environment.systemPackages = [ + (pkgs.callPackage /path/to/pixlit-nix {}) +]; +``` + +## Requirements (handled automatically by Nix) + +| Dependency | Purpose | +|---|---| +| Python 3 | Runtime | +| Pillow | Image decoding/encoding | +| pillow-heif (via Pillow HEIC plugin) | HEIC/HEIF support | +| PyGObject | GTK4 + GLib bindings | +| GTK 4 | Widget toolkit | +| libadwaita | GNOME HIG widgets | +| gobject-introspection | GObject type system | + +## File Layout + +``` +pixlit-nix/ +├── default.nix ← Nix derivation +├── pixlit.py ← Application source (GTK4/libadwaita) +├── pixlit.svg ← Scalable app icon +├── pixlit.png ← 512×512 icon +├── pixlit-512.png ← 512×512 icon +├── pixlit-256.png ← 256×256 icon +├── pixlit-128.png ← 128×128 icon +└── README.md ← This file +``` + +## Usage + +1. Launch Pixlit +2. Click **Choose Files…** or drag images onto the drop zone +3. Select your desired output format (JPEG / PNG / WebP) +4. Adjust the quality slider +5. Choose where to save converted files +6. Click **Convert Images** + +## License + +MIT — see `default.nix` meta block. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..651a67d --- /dev/null +++ b/default.nix @@ -0,0 +1,4 @@ +# Entry point for local builds: nix-build / nix-env -f . +# The actual derivation lives in pixlit.nix +{ pkgs ? import {} }: +pkgs.callPackage ./pixlit.nix {} diff --git a/pixlit-128.png b/pixlit-128.png new file mode 100644 index 0000000000000000000000000000000000000000..fce2bc6178ac46481baf7d1835cd898580ed839a GIT binary patch literal 5727 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 G8S*~@UZnW| literal 0 HcmV?d00001 diff --git a/pixlit-256.png b/pixlit-256.png new file mode 100644 index 0000000000000000000000000000000000000000..6e1436a62361e26cdf968f2faa1a71121c8f4bea GIT binary patch literal 12481 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 hLVsLf>)|H#3$M}@ags None: + from PIL import Image + img = Image.open(src_path) + exif = img.info.get("exif", b"") + + if fmt == "JPEG" and img.mode in ("RGBA", "LA", "P"): + if img.mode == "P": + img = img.convert("RGBA") + bg = Image.new("RGB", img.size, (255, 255, 255)) + bg.paste(img, mask=img.split()[-1] if img.mode in ("RGBA", "LA") else None) + img = bg + elif fmt in ("PNG", "WEBP") and img.mode == "P": + img = img.convert("RGBA") + + kwargs = {} + if fmt == "JPEG": + kwargs = {"quality": quality, "optimize": True} + if exif: + kwargs["exif"] = exif + elif fmt == "WEBP": + kwargs = {"quality": quality, "method": 6} + elif fmt == "PNG": + kwargs = {"compress_level": max(0, min(9, 9 - round((quality / 100) * 9)))} + + img.save(dst_path, format=fmt, **kwargs) + + +def build_output_path(src: str, out_dir: str, fmt: str) -> str: + stem = Path(src).stem + ext = FORMAT_EXT[fmt] + candidate = os.path.join(out_dir, stem + ext) + counter = 1 + while os.path.exists(candidate): + candidate = os.path.join(out_dir, f"{stem}_{counter}{ext}") + counter += 1 + return candidate + + +def load_thumbnail_pixbuf(path: str, size: int = 180): + """Load and scale image to a square thumbnail. Returns None on failure.""" + try: + from PIL import Image + img = Image.open(path) + img.thumbnail((size, size), Image.LANCZOS) + if img.mode != "RGBA": + img = img.convert("RGBA") + data = img.tobytes() + w, h = img.size + return GdkPixbuf.Pixbuf.new_from_data( + data, GdkPixbuf.Colorspace.RGB, True, 8, w, h, w * 4, + ) + except Exception as e: + print(f"[pixlit] thumbnail failed for {path}: {e}", file=sys.stderr) + return None + + +def load_preview_pixbuf(path: str, max_size: int = 1200): + """Load image scaled to fit max_size for the preview dialog.""" + try: + from PIL import Image + img = Image.open(path) + img.thumbnail((max_size, max_size), Image.LANCZOS) + if img.mode != "RGBA": + img = img.convert("RGBA") + data = img.tobytes() + w, h = img.size + return GdkPixbuf.Pixbuf.new_from_data( + data, GdkPixbuf.Colorspace.RGB, True, 8, w, h, w * 4, + ) + except Exception as e: + print(f"[pixlit] preview failed for {path}: {e}", file=sys.stderr) + return None + + +# ── Thumbnail card widget ───────────────────────────────────────────────────── + +THUMB_SIZE = 96 + +class ThumbnailCard(Gtk.Box): + """A card showing a thumbnail + filename + status, clickable for preview.""" + + def __init__(self, path: str, on_remove, on_preview): + super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0) + self.path = path + self._on_preview = on_preview + + self.add_css_class("card") + self.set_size_request(THUMB_SIZE + 24, -1) + + # Spinner placeholder + self._spinner = Gtk.Spinner() + self._spinner.start() + self._spinner.set_size_request(THUMB_SIZE, THUMB_SIZE) + self._spinner.set_margin_top(8) + self._spinner.set_margin_start(8) + self._spinner.set_margin_end(8) + self.append(self._spinner) + + # Picture (swapped in after load) + self._picture = Gtk.Picture() + self._picture.set_size_request(THUMB_SIZE, THUMB_SIZE) + self._picture.set_content_fit(Gtk.ContentFit.COVER) + self._picture.set_can_shrink(True) + self._picture.set_margin_top(8) + self._picture.set_margin_start(8) + self._picture.set_margin_end(8) + + # Filename + name_lbl = Gtk.Label(label=Path(path).name) + name_lbl.set_ellipsize(3) + name_lbl.set_max_width_chars(14) + name_lbl.add_css_class("caption") + name_lbl.set_margin_top(4) + name_lbl.set_margin_start(6) + name_lbl.set_margin_end(6) + self.append(name_lbl) + + # Status + self.status_lbl = Gtk.Label(label="") + self.status_lbl.add_css_class("caption") + self.status_lbl.set_margin_bottom(2) + self.append(self.status_lbl) + + # Remove button + remove_btn = Gtk.Button(icon_name="list-remove-symbolic") + remove_btn.add_css_class("flat") + remove_btn.add_css_class("circular") + remove_btn.set_halign(Gtk.Align.END) + remove_btn.set_margin_end(4) + remove_btn.set_margin_bottom(4) + remove_btn.connect("clicked", lambda _: on_remove(self)) + self.append(remove_btn) + + # Click to preview (on the whole card) + click = Gtk.GestureClick() + click.connect("released", self._on_click) + self.add_controller(click) + + threading.Thread(target=self._load_thumb, daemon=True).start() + + def _load_thumb(self): + pb = load_thumbnail_pixbuf(self.path, THUMB_SIZE) + GLib.idle_add(self._set_thumb, pb) + + def _set_thumb(self, pb): + self.remove(self._spinner) + if pb: + self._picture.set_pixbuf(pb) + else: + self._picture.set_icon_name("image-missing") + # Insert picture at position 0 + first = self.get_first_child() + self.prepend(self._picture) + + def _on_click(self, gesture, n_press, x, y): + self._on_preview(self.path) + + def set_status(self, text: str, success: bool): + self.status_lbl.set_label(text) + css = "success" if success else "error" + other = "error" if success else "success" + self.status_lbl.remove_css_class(other) + self.status_lbl.add_css_class(css) + + +# ── Preview dialog ──────────────────────────────────────────────────────────── + +class PreviewDialog(Adw.Dialog): + def __init__(self, path: str): + super().__init__() + self.set_title(Path(path).name) + self.set_content_width(820) + self.set_content_height(680) + self.set_follows_content_size(False) + + toolbar_view = Adw.ToolbarView() + self.set_child(toolbar_view) + + header = Adw.HeaderBar() + toolbar_view.add_top_bar(header) + + try: + stat = os.stat(path) + size_kb = stat.st_size / 1024 + size_str = f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB" + except OSError: + size_str = "" + + header.set_title_widget(Adw.WindowTitle( + title=Path(path).name, + subtitle=f"{Path(path).suffix.upper().lstrip('.')} · {size_str}" + )) + + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.set_vexpand(True) + scroll.set_hexpand(True) + toolbar_view.set_content(scroll) + + self._stack = Gtk.Stack() + self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + scroll.set_child(self._stack) + + spinner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + spinner_box.set_valign(Gtk.Align.CENTER) + spinner_box.set_halign(Gtk.Align.CENTER) + spinner_box.set_vexpand(True) + spinner_box.set_hexpand(True) + sp = Gtk.Spinner() + sp.start() + sp.set_size_request(48, 48) + spinner_box.append(sp) + self._stack.add_named(spinner_box, "loading") + + self._picture = Gtk.Picture() + self._picture.set_content_fit(Gtk.ContentFit.CONTAIN) + self._picture.set_can_shrink(True) + self._picture.set_vexpand(True) + self._picture.set_hexpand(True) + self._stack.add_named(self._picture, "image") + self._stack.set_visible_child_name("loading") + + threading.Thread(target=self._load, args=(path,), daemon=True).start() + + def _load(self, path): + pb = load_preview_pixbuf(path, 1200) + GLib.idle_add(self._show, pb) + + def _show(self, pb): + if pb: + self._picture.set_pixbuf(pb) + else: + self._picture.set_icon_name("image-missing") + self._stack.set_visible_child_name("image") + + +# ── Main window ─────────────────────────────────────────────────────────────── + +class PixlitWindow(Adw.ApplicationWindow): + def __init__(self, app): + super().__init__(application=app) + self.set_title("Pixlit") + self.set_default_size(960, 620) + + self._files: list[ThumbnailCard] = [] + self._out_dir: str = str(Path.home() / "Pictures") + self._converting = False + + self._build_ui() + + def _build_ui(self): + toolbar_view = Adw.ToolbarView() + self.set_content(toolbar_view) + + header = Adw.HeaderBar() + header.set_centering_policy(Adw.CenteringPolicy.STRICT) + header.set_title_widget(Adw.WindowTitle(title="Pixlit", subtitle="Image Converter")) + about_btn = Gtk.Button(icon_name="help-about-symbolic") + about_btn.add_css_class("flat") + about_btn.connect("clicked", self._show_about) + header.pack_end(about_btn) + toolbar_view.add_top_bar(header) + + paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) + paned.set_position(340) + paned.set_shrink_start_child(False) + paned.set_shrink_end_child(False) + toolbar_view.set_content(paned) + + paned.set_start_child(self._build_left_panel()) + paned.set_end_child(self._build_right_panel()) + + def _build_left_panel(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + outer.set_size_request(300, -1) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(16) + box.set_margin_bottom(16) + box.set_margin_start(16) + box.set_margin_end(12) + box.set_vexpand(True) + outer.append(box) + + box.append(self._build_drop_zone()) + box.append(self._build_options_section()) + box.append(self._build_output_section()) + box.append(self._build_convert_button()) + box.append(self._build_progress_section()) + + return outer + + def _build_right_panel(self): + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + panel_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + panel_header.set_margin_top(12) + panel_header.set_margin_bottom(8) + panel_header.set_margin_start(12) + panel_header.set_margin_end(12) + + self._file_count_lbl = Gtk.Label(label="No images added", xalign=0) + self._file_count_lbl.set_hexpand(True) + self._file_count_lbl.add_css_class("heading") + panel_header.append(self._file_count_lbl) + + self._clear_btn = Gtk.Button(label="Clear All") + self._clear_btn.add_css_class("flat") + self._clear_btn.add_css_class("destructive-action") + self._clear_btn.connect("clicked", self._clear_all_files) + self._clear_btn.set_visible(False) + panel_header.append(self._clear_btn) + + outer.append(panel_header) + outer.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) + + # Stack: empty state vs grid + self._right_stack = Gtk.Stack() + self._right_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + self._right_stack.set_vexpand(True) + + empty_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + empty_box.set_vexpand(True) + empty_lbl = Gtk.Label( + label="Add images using the panel on the left\nor drag and drop them here" + ) + empty_lbl.add_css_class("dim-label") + empty_lbl.set_justify(Gtk.Justification.CENTER) + empty_lbl.set_valign(Gtk.Align.CENTER) + empty_lbl.set_halign(Gtk.Align.CENTER) + empty_lbl.set_vexpand(True) + empty_box.append(empty_lbl) + self._right_stack.add_named(empty_box, "empty") + + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scroll.set_vexpand(True) + + self._flowbox = Gtk.FlowBox() + self._flowbox.set_valign(Gtk.Align.START) + self._flowbox.set_homogeneous(True) + self._flowbox.set_column_spacing(8) + self._flowbox.set_row_spacing(8) + self._flowbox.set_margin_top(12) + self._flowbox.set_margin_bottom(12) + self._flowbox.set_margin_start(12) + self._flowbox.set_margin_end(12) + self._flowbox.set_min_children_per_line(2) + self._flowbox.set_max_children_per_line(8) + self._flowbox.set_selection_mode(Gtk.SelectionMode.NONE) + scroll.set_child(self._flowbox) + self._right_stack.add_named(scroll, "grid") + + self._right_stack.set_visible_child_name("empty") + outer.append(self._right_stack) + + # Drag-and-drop on right panel + drop_target = Gtk.DropTarget.new(Gio.File, Gdk.DragAction.COPY) + drop_target.connect("drop", self._on_drop) + outer.add_controller(drop_target) + + return outer + + def _build_drop_zone(self): + group = Adw.PreferencesGroup(title="Add Images") + + drop_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + drop_box.add_css_class("card") + drop_box.set_margin_top(4) + + inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + inner.set_margin_top(16) + inner.set_margin_bottom(16) + inner.set_margin_start(16) + inner.set_margin_end(16) + drop_box.append(inner) + + icon = Gtk.Image.new_from_icon_name("insert-image-symbolic") + icon.set_pixel_size(32) + icon.add_css_class("dim-label") + inner.append(icon) + + lbl = Gtk.Label(label="Drop images here") + lbl.add_css_class("body") + lbl.set_halign(Gtk.Align.CENTER) + inner.append(lbl) + + btn = Gtk.Button(label="Choose Files…") + btn.add_css_class("suggested-action") + btn.add_css_class("pill") + btn.set_halign(Gtk.Align.CENTER) + btn.connect("clicked", self._pick_files) + inner.append(btn) + + drop_target = Gtk.DropTarget.new(Gio.File, Gdk.DragAction.COPY) + drop_target.connect("drop", self._on_drop) + drop_box.add_controller(drop_target) + + group.add(drop_box) + return group + + def _build_options_section(self): + group = Adw.PreferencesGroup(title="Options") + + fmt_row = Adw.ActionRow(title="Output Format") + self._fmt_combo = Gtk.DropDown.new_from_strings(["JPEG", "PNG", "WebP"]) + self._fmt_combo.set_valign(Gtk.Align.CENTER) + self._fmt_combo.connect("notify::selected", self._on_format_changed) + fmt_row.add_suffix(self._fmt_combo) + fmt_row.set_activatable_widget(self._fmt_combo) + group.add(fmt_row) + + quality_row = Adw.ActionRow(title="Quality") + quality_row.set_subtitle("Higher = better quality, larger file") + q_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + q_box.set_valign(Gtk.Align.CENTER) + q_box.set_size_request(180, -1) + + self._quality_scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 1, 100, 1) + self._quality_scale.set_value(85) + self._quality_scale.set_hexpand(True) + self._quality_scale.set_draw_value(False) + self._quality_scale.connect("value-changed", self._on_quality_changed) + + self._quality_lbl = Gtk.Label(label="85") + self._quality_lbl.set_width_chars(3) + + q_box.append(self._quality_scale) + q_box.append(self._quality_lbl) + quality_row.add_suffix(q_box) + group.add(quality_row) + + self._png_note_row = Adw.ActionRow(title="PNG is lossless") + self._png_note_row.set_subtitle("Slider controls compression speed only") + self._png_note_row.add_prefix(Gtk.Image.new_from_icon_name("dialog-information-symbolic")) + self._png_note_row.set_visible(False) + group.add(self._png_note_row) + + return group + + def _build_output_section(self): + group = Adw.PreferencesGroup(title="Output") + self._out_row = Adw.ActionRow(title="Save To") + self._out_row.set_subtitle(self._out_dir) + btn = Gtk.Button(label="Choose…") + btn.add_css_class("flat") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", self._pick_output_dir) + self._out_row.add_suffix(btn) + group.add(self._out_row) + return group + + def _build_convert_button(self): + box = Gtk.Box() + box.set_margin_top(4) + self._convert_btn = Gtk.Button(label="Convert Images") + self._convert_btn.add_css_class("suggested-action") + self._convert_btn.add_css_class("pill") + self._convert_btn.set_hexpand(True) + self._convert_btn.set_size_request(-1, 44) + self._convert_btn.connect("clicked", self._start_conversion) + self._convert_btn.set_sensitive(False) + box.append(self._convert_btn) + return box + + def _build_progress_section(self): + self._progress_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + self._progress_box.set_visible(False) + self._progress_bar = Gtk.ProgressBar() + self._progress_bar.set_show_text(True) + self._progress_box.append(self._progress_bar) + self._result_lbl = Gtk.Label(label="") + self._result_lbl.set_halign(Gtk.Align.CENTER) + self._result_lbl.add_css_class("caption") + self._progress_box.append(self._result_lbl) + return self._progress_box + + # ── Signals & logic ─────────────────────────────────────────────────────── + + def _on_format_changed(self, combo, _param): + fmt = ["JPEG", "PNG", "WEBP"][combo.get_selected()] + self._png_note_row.set_visible(fmt == "PNG") + + def _on_quality_changed(self, scale): + self._quality_lbl.set_label(str(int(scale.get_value()))) + + def _pick_files(self, _btn): + dialog = Gtk.FileDialog() + dialog.set_title("Choose Images") + dialog.set_modal(True) + filters = Gio.ListStore.new(Gtk.FileFilter) + f = Gtk.FileFilter() + f.set_name("Images (HEIC, JPEG, PNG, WebP…)") + for pat in ["*.heic", "*.heif", "*.jpg", "*.jpeg", "*.png", "*.webp", + "*.bmp", "*.tiff", "*.tif", "*.gif", + "*.HEIC", "*.HEIF", "*.JPG", "*.JPEG", "*.PNG", "*.WEBP"]: + f.add_pattern(pat) + filters.append(f) + af = Gtk.FileFilter() + af.set_name("All Files") + af.add_pattern("*") + filters.append(af) + dialog.set_filters(filters) + dialog.open_multiple(self, None, self._on_files_picked) + + def _on_files_picked(self, dialog, result): + try: + files = dialog.open_multiple_finish(result) + except GLib.Error: + return + if files: + for i in range(files.get_n_items()): + gf = files.get_item(i) + if gf and gf.get_path(): + self._add_file(gf.get_path()) + + def _on_drop(self, target, value, _x, _y): + if isinstance(value, Gio.File) and value.get_path(): + self._add_file(value.get_path()) + return True + return False + + def _add_file(self, path: str): + if Path(path).suffix.lower() not in SUPPORTED_INPUT: + self._show_toast(f"Unsupported format: {Path(path).name}") + return + if any(c.path == path for c in self._files): + return + card = ThumbnailCard(path, self._remove_card, self._open_preview) + self._flowbox.append(card) + self._files.append(card) + self._update_ui() + + def _remove_card(self, card: ThumbnailCard): + self._files.remove(card) + parent = card.get_parent() + self._flowbox.remove(parent) + self._update_ui() + + def _clear_all_files(self, _btn=None): + for card in list(self._files): + self._flowbox.remove(card.get_parent()) + self._files.clear() + self._update_ui() + + def _update_ui(self): + n = len(self._files) + if n == 0: + self._file_count_lbl.set_label("No images added") + self._right_stack.set_visible_child_name("empty") + self._clear_btn.set_visible(False) + else: + self._file_count_lbl.set_label(f"{n} image{'s' if n != 1 else ''} queued") + self._right_stack.set_visible_child_name("grid") + self._clear_btn.set_visible(True) + self._convert_btn.set_sensitive(n > 0 and not self._converting) + + def _open_preview(self, path: str): + PreviewDialog(path).present(self) + + def _pick_output_dir(self, _btn): + dialog = Gtk.FileDialog() + dialog.set_title("Choose Output Directory") + dialog.set_modal(True) + dialog.select_folder(self, None, self._on_output_dir_picked) + + def _on_output_dir_picked(self, dialog, result): + try: + folder = dialog.select_folder_finish(result) + except GLib.Error: + return + if folder: + self._out_dir = folder.get_path() + self._out_row.set_subtitle(self._out_dir) + + def _get_fmt(self) -> str: + return ["JPEG", "PNG", "WEBP"][self._fmt_combo.get_selected()] + + def _start_conversion(self, _btn): + if self._converting or not self._files: + return + self._converting = True + self._convert_btn.set_sensitive(False) + self._progress_box.set_visible(True) + self._progress_bar.set_fraction(0) + self._progress_bar.set_text("Starting…") + self._result_lbl.set_label("") + for card in self._files: + card.set_status("", True) + + fmt = self._get_fmt() + quality = int(self._quality_scale.get_value()) + snapshot = list(self._files) + out_dir = self._out_dir + os.makedirs(out_dir, exist_ok=True) + + threading.Thread( + target=self._conversion_worker, + args=(snapshot, fmt, quality, out_dir), + daemon=True, + ).start() + + def _conversion_worker(self, cards, fmt, quality, out_dir): + total = len(cards) + ok = fail = 0 + for i, card in enumerate(cards): + GLib.idle_add(self._progress_bar.set_fraction, i / total) + GLib.idle_add(self._progress_bar.set_text, f"{i}/{total} converted") + try: + dst = build_output_path(card.path, out_dir, fmt) + convert_image(card.path, dst, fmt, quality) + GLib.idle_add(card.set_status, "✓ Done", True) + ok += 1 + except Exception as e: + GLib.idle_add(card.set_status, "✗ Failed", False) + print(f"[pixlit] error converting {card.path}: {e}", file=sys.stderr) + fail += 1 + + GLib.idle_add(self._conversion_done, ok, fail, total) + + def _conversion_done(self, ok, fail, total): + self._progress_bar.set_fraction(1.0) + self._progress_bar.set_text(f"Done — {ok}/{total} converted") + if fail == 0: + self._result_lbl.set_label(f"All {ok} image{'s' if ok != 1 else ''} saved") + self._result_lbl.remove_css_class("error") + self._result_lbl.add_css_class("success") + else: + self._result_lbl.set_label(f"{ok} succeeded, {fail} failed") + self._result_lbl.remove_css_class("success") + self._result_lbl.add_css_class("error") + self._converting = False + self._convert_btn.set_sensitive(len(self._files) > 0) + self._show_toast(f"Converted {ok} of {total} images → {self._out_dir}") + + def _show_toast(self, message: str): + toast = Adw.Toast(title=message) + toast.set_timeout(4) + overlay = getattr(self, "_toast_overlay", None) + if overlay: + overlay.add_toast(toast) + + def _show_about(self, _btn): + about = Adw.AboutDialog() + about.set_application_name("Pixlit") + about.set_version("1.0.0") + about.set_developer_name("Pixlit Contributors") + about.set_license_type(Gtk.License.MIT_X11) + about.set_comments("Convert images between HEIC, JPEG, PNG, and WebP formats") + about.set_application_icon("pixlit") + about.present(self) + + +# ── Application ─────────────────────────────────────────────────────────────── + +class PixlitApp(Adw.Application): + def __init__(self): + super().__init__( + application_id="io.github.pixlit", + flags=Gio.ApplicationFlags.FLAGS_NONE, + ) + self.connect("activate", self._on_activate) + + def _on_activate(self, app): + win = PixlitWindow(app) + toast_overlay = Adw.ToastOverlay() + win._toast_overlay = toast_overlay + content = win.get_content() + win.set_content(toast_overlay) + toast_overlay.set_child(content) + win.present() + + +def main(): + app = PixlitApp() + return app.run(sys.argv) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pixlit.svg b/pixlit.svg new file mode 100644 index 0000000..e78fb4d --- /dev/null +++ b/pixlit.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HEIC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JPG + + + + + PIXLIT + IMAGE CONVERTER + + + + + + +