diff --git a/.gitignore b/.gitignore index 4c07f7a..b5a6689 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pkg/ build/ dist/ src/recoder.egg-info/ + +*.gresource diff --git a/PKGBUILD b/PKGBUILD index a9aa550..32d5056 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,4 +1,4 @@ -# Maintainer: Jeena +# Maintainer: Jeena pkgname=recoder pkgver=1.0.0 pkgrel=1 @@ -16,27 +16,31 @@ depends=( ) optdepends=( 'libcanberra: play system notification sounds' - 'sound-theme-freedesktop: standard system sounds like \"complete.oga\"' + 'sound-theme-freedesktop: standard system sounds like "complete.oga"' ) makedepends=( 'python-setuptools' 'python-build' 'python-installer' + 'glib2' ) source=() sha256sums=() build() { cd "$srcdir/../" # go to your project root + glib-compile-resources src/resources/resources.xml \ + --target=src/recoder/resources.gresource \ + --sourcedir=src/resources python -m build --wheel --outdir dist } package() { - cd "$srcdir/../" # back to project root where dist/ is + cd "$srcdir/../" python -m installer --destdir="$pkgdir" dist/*.whl - install -Dm644 resources/net.jeena.Recoder.desktop \ + install -Dm644 src/resources/net.jeena.Recoder.desktop \ "$pkgdir/usr/share/applications/net.jeena.Recoder.desktop" - install -Dm644 resources/net.jeena.Recoder.png \ + install -Dm644 src/resources/net.jeena.Recoder.png \ "$pkgdir/usr/share/icons/hicolor/256x256/apps/net.jeena.Recoder.png" } diff --git a/src/recoder/app.py b/src/recoder/app.py index 8b8b362..86a6b39 100755 --- a/src/recoder/app.py +++ b/src/recoder/app.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import sys +import os import gi gi.require_version('Gtk', '4.0') @@ -9,7 +10,14 @@ from gi.repository import Adw, Gio Adw.init() +def load_resources(): + resource_path = os.path.join(os.path.dirname(__file__), "../resources/resources.gresource") + resource = Gio.Resource.load(resource_path) + Gio.resources_register(resource) + def main(): + load_resources() + from recoder.ui import RecoderWindow # delayed import class RecoderApp(Adw.Application): @@ -21,7 +29,7 @@ def main(): def do_activate(self): if not self.window: self.window = RecoderWindow(application=self) - self.window.present() + self.window.window.present() app = RecoderApp() return app.run(sys.argv) diff --git a/src/recoder/config.py b/src/recoder/config.py deleted file mode 100644 index c2bc835..0000000 --- a/src/recoder/config.py +++ /dev/null @@ -1,16 +0,0 @@ -import json -from pathlib import Path -from gi.repository import GLib - -CONFIG_PATH = Path(GLib.get_user_config_dir()) / "recoder" / "config.json" -CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) - -def save_config(config): - with open(CONFIG_PATH, "w") as f: - json.dump(config, f) - -def load_config(): - if CONFIG_PATH.exists(): - with open(CONFIG_PATH) as f: - return json.load(f) - return {} diff --git a/src/recoder/transcoder_worker.py b/src/recoder/transcoder_worker.py index 8d7c054..e381a40 100644 --- a/src/recoder/transcoder_worker.py +++ b/src/recoder/transcoder_worker.py @@ -7,10 +7,11 @@ from gi.repository import GLib class TranscoderWorker: TIME_RE = re.compile(r"time=(\d+):(\d+):(\d+)\.(\d+)") - def __init__(self, files, progress_callback=None, done_callback=None): + def __init__(self, files, progress_callback=None, done_callback=None, file_done_callback=None): self.files = files self.progress_callback = progress_callback self.done_callback = done_callback + self.file_done_callback = file_done_callback self.is_processing = False def start(self): @@ -27,12 +28,21 @@ class TranscoderWorker: basename = os.path.basename(filepath) self._update_progress(f"Starting {basename}", (idx - 1) / total) - success, output_path = self._transcode_file(filepath, os.path.join(os.path.dirname(filepath), "transcoded"), basename, idx, total) + success, output_path = self._transcode_file( + filepath, + os.path.join(os.path.dirname(filepath), "transcoded"), + basename, + idx, + total, + ) if not success: self._update_progress(f"Error transcoding {basename}", idx / total) else: self._update_progress(f"Finished {basename}", idx / total) + if self.file_done_callback: + GLib.idle_add(self.file_done_callback, filepath) + self.is_processing = False self._update_progress("All done!", 1.0) self._notify_done() @@ -47,9 +57,17 @@ class TranscoderWorker: duration = 1.0 # fallback to avoid division by zero cmd = [ - "ffmpeg", "-y", "-i", input_path, - "-c:v", "libx264", "-preset", "fast", - "-c:a", "aac", output_path + "ffmpeg", + "-y", + "-i", + input_path, + "-c:v", + "libx264", + "-preset", + "fast", + "-c:a", + "aac", + output_path, ] process = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True) @@ -68,16 +86,19 @@ class TranscoderWorker: self._update_progress(f"Transcoding {basename}", overall_fraction) process.wait() - success = (process.returncode == 0) + success = process.returncode == 0 return success, output_path def _get_duration(self, input_path): - # Run ffprobe to get duration in seconds cmd = [ - "ffprobe", "-v", "error", - "-show_entries", "format=duration", - "-of", "default=noprint_wrappers=1:nokey=1", - input_path + "ffprobe", + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + input_path, ] try: output = subprocess.check_output(cmd, universal_newlines=True) diff --git a/src/recoder/ui.py b/src/recoder/ui.py index a9315c6..e91418e 100644 --- a/src/recoder/ui.py +++ b/src/recoder/ui.py @@ -8,104 +8,75 @@ gi.require_version('Notify', '0.7') from gi.repository import Gtk, Adw, Gio, Gdk, GLib, Notify from functools import partial -from recoder.config import load_config, save_config from recoder.transcoder_worker import TranscoderWorker -class RecoderWindow(Adw.ApplicationWindow): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.set_title("Recoder") - self.set_default_size(700, 400) +class FileEntryRow(Gtk.ListBoxRow): + def __init__(self, path): + super().__init__() + self.path = path + self.icon = Gtk.Image.new_from_icon_name("media-playback-pause-symbolic") + self.label = Gtk.Label(label=os.path.basename(path), xalign=0) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + hbox.append(self.icon) + hbox.append(self.label) + self.set_child(hbox) - self.config = load_config() - self.current_dir = self.config.get("last_directory", str(os.path.expanduser("~"))) + def mark_done(self): + self.icon.set_from_icon_name("emblem-ok-symbolic") + self.label.set_text(f"{os.path.basename(self.path)} - Done") + +class RecoderWindow(): + + def __init__(self, application, **kwargs): self.files_to_process = [] self.transcoder = None + self.file_rows = {} - # UI setup - self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) - self.header_bar = Adw.HeaderBar() - self.toolbar_view = Adw.ToolbarView() - self.toolbar_view.add_top_bar(self.header_bar) - self.toolbar_view.set_content(self.vbox) - self.set_content(self.toolbar_view) + # Load UI from resource + builder = Gtk.Builder() + builder.add_from_resource("/net/jeena/recoder/recoder.ui") - self.textview = Gtk.TextView() - self.textview.set_editable(False) - self.textbuffer = self.textview.get_buffer() + self.window = builder.get_object("main_window") + self.window.set_application(application) + + self.overlay = builder.get_object("overlay") + self.progress = builder.get_object("progress_bar") + self.drop_hint = builder.get_object("drop_hint") + self.listbox = builder.get_object("listbox") + self.scrolled_window = builder.get_object("scrolled_window") self.drop_target = Gtk.DropTarget.new(Gio.File, Gdk.DragAction.COPY) + self.drop_target.connect("drop", self.on_drop) self.drop_target.connect("enter", self.on_drop_enter) self.drop_target.connect("leave", self.on_drop_leave) - self.drop_target.connect("drop", self.on_drop) - self.textview.add_controller(self.drop_target) - # CSS - self._setup_css() + self.window.add_controller(self.drop_target) - self.drop_hint = Gtk.Label(label="📂 Drop files here to get started") - self.drop_hint.add_css_class("dim-label") - self._setup_dim_label_css() - - overlay = Gtk.Overlay() - overlay.set_child(self.textview) - overlay.add_overlay(self.drop_hint) - self.drop_hint.set_halign(Gtk.Align.CENTER) - self.drop_hint.set_valign(Gtk.Align.CENTER) - - self.scrolled = Gtk.ScrolledWindow() - self.scrolled.set_child(overlay) - self.vbox.append(self.scrolled) - self.scrolled.set_vexpand(True) - self.scrolled.set_hexpand(True) - - self.progress = Gtk.ProgressBar() - self.vbox.append(self.progress) - self.progress.set_hexpand(True) + css_provider = Gtk.CssProvider() + css_provider.load_from_resource("/net/jeena/recoder/style.css") + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) Notify.init("Recoder") - def _setup_css(self): - css = b""" - textview.drop-highlight { - background-color: @theme_selected_bg_color; - border: 2px solid @theme_selected_fg_color; - } - """ - style_provider = Gtk.CssProvider() - style_provider.load_from_data(css) - Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, - ) - - def _setup_dim_label_css(self): - css = b""" - .dim-label { - font-size: 16px; - color: @theme_unfocused_fg_color; - } - """ - style_provider = Gtk.CssProvider() - style_provider.load_from_data(css) - Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, - ) - def on_drop_enter(self, drop_target, x, y): - self.textview.add_css_class("drop-highlight") + self.overlay.add_css_class("drop-highlight") return True def on_drop_leave(self, drop_target): - self.textview.remove_css_class("drop-highlight") + self.overlay.remove_css_class("drop-highlight") return True def on_drop(self, drop_target, value, x, y): GLib.idle_add(partial(self.process_drop_value, value)) + self.overlay.remove_overlay(self.drop_hint) + self.progress.set_visible(True) + self.progress.set_fraction(0.0) # optionally reset + self.drop_hint.set_visible(False) return value def process_drop_value(self, value): @@ -129,10 +100,21 @@ class RecoderWindow(Adw.ApplicationWindow): if not paths: return False - self.textview.remove_css_class("drop-highlight") - self.drop_hint.set_visible(False) + # Clear previous listbox rows + children = self.listbox.get_first_child() + while children: + next_child = children.get_next_sibling() + self.listbox.remove(children) + children = next_child + self.file_rows.clear() + + # Add new files to listbox with custom FileEntryRow + for path in paths: + row = FileEntryRow(path) + self.listbox.append(row) + self.file_rows[path] = row + self.files_to_process = paths - self.textbuffer.set_text("\n".join(os.path.basename(p) for p in paths) + "\n") self.start_transcoding() return False @@ -147,6 +129,7 @@ class RecoderWindow(Adw.ApplicationWindow): self.files_to_process, progress_callback=self.update_progress, done_callback=self.notify_done, + file_done_callback=self.mark_file_done, ) self.transcoder.start() @@ -155,6 +138,11 @@ class RecoderWindow(Adw.ApplicationWindow): self.progress.set_text(text) self.progress.set_fraction(fraction) + def mark_file_done(self, filepath): + if filepath in self.file_rows: + row = self.file_rows[filepath] + GLib.idle_add(row.mark_done) + def notify_done(self): self.progress.set_show_text(True) self.progress.set_text("Transcoding Complete!") diff --git a/resources/net.jeena.Recoder.desktop b/src/resources/net.jeena.Recoder.desktop similarity index 100% rename from resources/net.jeena.Recoder.desktop rename to src/resources/net.jeena.Recoder.desktop diff --git a/resources/net.jeena.Recoder.png b/src/resources/net.jeena.Recoder.png similarity index 100% rename from resources/net.jeena.Recoder.png rename to src/resources/net.jeena.Recoder.png diff --git a/resources/recoder.png b/src/resources/recoder.png similarity index 100% rename from resources/recoder.png rename to src/resources/recoder.png diff --git a/src/resources/recoder.ui b/src/resources/recoder.ui new file mode 100644 index 0000000..c54c8ec --- /dev/null +++ b/src/resources/recoder.ui @@ -0,0 +1,61 @@ + + + + + + + Recoder + 700 + 400 + + + + + + + + + + vertical + 6 + true + + + true + true + + + true + true + + + none + true + + + + + + + 📂 Drop files here to get started + center + center + + + + + + + + true + False + + + + + + + + diff --git a/src/resources/resources.xml b/src/resources/resources.xml new file mode 100644 index 0000000..40892f6 --- /dev/null +++ b/src/resources/resources.xml @@ -0,0 +1,6 @@ + + + recoder.ui + style.css + + diff --git a/src/resources/style.css b/src/resources/style.css new file mode 100644 index 0000000..d6d1f4a --- /dev/null +++ b/src/resources/style.css @@ -0,0 +1,13 @@ +.drop-highlight { + background-color: @theme_selected_bg_color; + border: 2px solid @theme_selected_fg_color; +} + +.dim-label { + font-size: 16px; + color: @theme_unfocused_fg_color; +} + +.debug-row { + background: red; +} \ No newline at end of file