From 1464273506d294d56f3fb50461688463d4c29724 Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 5 Jun 2025 00:53:12 +0900 Subject: [PATCH] Moving cancel button and restructuring Especially the app state has been reinvented --- src/recoder/app_state.py | 107 +++++++++++++++++++++ src/recoder/drop_handler.py | 56 +++++++++++ src/recoder/recoder_window.py | 161 +++----------------------------- src/recoder/transcoder.py | 3 +- src/resources/recoder_window.ui | 3 +- src/resources/style.css | 7 +- 6 files changed, 186 insertions(+), 151 deletions(-) create mode 100644 src/recoder/app_state.py create mode 100644 src/recoder/drop_handler.py diff --git a/src/recoder/app_state.py b/src/recoder/app_state.py new file mode 100644 index 0000000..80a86cf --- /dev/null +++ b/src/recoder/app_state.py @@ -0,0 +1,107 @@ +import gi +gi.require_version("GObject", "2.0") +from gi.repository import GObject + + +class AppState(GObject.GEnum): + IDLE = 0 + FILES_LOADED = 1 + TRANSCODING = 2 + PAUSED = 3 + DONE = 4 + STOPPED = 5 + ERROR = 6 + + +class AppStateManager(GObject.GObject): + state = GObject.Property(type=AppState, default=AppState.IDLE) + + +class UIStateManager: + def __init__(self, window, app_state_manager): + self.window = window + self.app_state_manager = app_state_manager + self.app_state_manager.connect("notify::state", self.on_state_changed) + + self.handlers = { + AppState.IDLE: self._handle_idle, + AppState.FILES_LOADED: self._handle_files_loaded, + AppState.TRANSCODING: self._handle_transcoding, + AppState.PAUSED: self._handle_paused, + AppState.DONE: self._handle_done, + AppState.STOPPED: self._handle_idle, # Reset UI on STOPPED + AppState.ERROR: self._handle_error, + } + + def on_state_changed(self, obj, pspec): + self.set_state(self.app_state_manager.state) + + def set_state(self, state: AppState): + handler = self.handlers.get(state) + if handler: + handler() + + def _handle_idle(self): + w = self.window + w.clear_listbox() + w.file_rows.clear() + w.file_items_to_process = [] + w.is_paused = False + w.progress_bar.set_visible(False) + w.progress_bar.set_fraction(0.0) + w.progress_bar.set_text("") + w.drop_hint.set_visible(True) + if w.drop_hint.get_parent() != w.overlay: + w.overlay.add_overlay(w.drop_hint) + w.btn_transcode.set_visible(False) + w.btn_cancel.set_visible(False) + + def _handle_files_loaded(self): + w = self.window + w.drop_hint.set_visible(False) + w.progress_bar.set_visible(False) + w.btn_transcode.set_visible(True) + w.btn_transcode.set_sensitive(True) + w.btn_transcode.set_label("Start Transcoding") + w.btn_transcode.add_css_class("suggested-action") + w.btn_cancel.set_visible(True) + w.is_paused = False + + def _handle_transcoding(self): + w = self.window + w.drop_hint.set_visible(False) + w.progress_bar.set_visible(True) + w.btn_transcode.set_visible(True) + w.btn_transcode.set_sensitive(True) + w.btn_transcode.set_label("Pause") + w.btn_transcode.remove_css_class("suggested-action") + w.btn_cancel.set_visible(True) + w.is_paused = False + + def _handle_paused(self): + w = self.window + w.drop_hint.set_visible(False) + w.progress_bar.set_visible(True) + w.btn_transcode.set_visible(True) + w.btn_transcode.set_sensitive(True) + w.btn_transcode.set_label("Resume") + w.btn_cancel.set_visible(True) + w.is_paused = True + + def _handle_done(self): + w = self.window + w.drop_hint.set_visible(False) + w.progress_bar.set_visible(True) + w.progress_bar.set_fraction(1.0) + w.btn_transcode.set_visible(False) + w.btn_transcode.remove_css_class("suggested-action") + w.btn_cancel.set_visible(True) + w.is_paused = False + + def _handle_error(self): + w = self.window + w.drop_hint.set_visible(False) + w.progress_bar.set_visible(False) + w.btn_transcode.set_visible(False) + w.btn_cancel.set_visible(True) + w.is_paused = False diff --git a/src/recoder/drop_handler.py b/src/recoder/drop_handler.py new file mode 100644 index 0000000..51288a2 --- /dev/null +++ b/src/recoder/drop_handler.py @@ -0,0 +1,56 @@ +import gi +from gi.repository import Gtk, Gdk, Gio, GLib +from functools import partial +from recoder.app_state import AppState + +class DropHandler: + def __init__(self, w, app_state_manager): + self.w = w + self.app_state_manager = app_state_manager + self._accepting = self._compute_accept() + self.app_state_manager.connect("notify::state", self.on_state_changed) + + self.drop_target = Gtk.DropTarget.new(Gio.File, Gdk.DragAction.COPY) + 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.w.overlay.add_controller(self.drop_target) + + def _compute_accept(self): + return self.app_state_manager.state not in { + AppState.TRANSCODING, + AppState.PAUSED, + AppState.FILES_LOADED, + } + + def on_state_changed(self, *_): + accepting = self._compute_accept() + if accepting != self._accepting: + self._accepting = accepting + if accepting: + self.w.overlay.add_controller(self.drop_target) + else: + self.w.overlay.remove_controller(self.drop_target) + + + def on_drop_enter(self, *_): + if not self._accepting: + return False + self.w.overlay.add_css_class("drop-highlight") + return True + + def on_drop_leave(self, *_): + self.w.overlay.remove_css_class("drop-highlight") + return True + + def on_drop(self, _, value, __, ___): + if not self._accepting: + return False + if self.w.drop_hint.get_parent(): + self.w.overlay.remove_overlay(self.w.drop_hint) + self.w.drop_hint.set_visible(False) + self.w.progress_bar.set_visible(True) + self.w.progress_bar.set_fraction(0.0) + GLib.idle_add(partial(self.w.process_drop_value, value)) + return True diff --git a/src/recoder/recoder_window.py b/src/recoder/recoder_window.py index ab76401..25b0d69 100644 --- a/src/recoder/recoder_window.py +++ b/src/recoder/recoder_window.py @@ -12,133 +12,8 @@ from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Notify from recoder.transcoder import Transcoder, BatchStatus from recoder.utils import extract_video_files, notify_done, play_complete_sound from recoder.file_entry_row import FileEntryRow - - -class DropHandler: - def __init__(self, overlay, drop_hint, progress_bar, on_files_dropped): - self.overlay = overlay - self.drop_hint = drop_hint - self.progress_bar = progress_bar - self.on_files_dropped = on_files_dropped - - self.drop_target = Gtk.DropTarget.new(Gio.File, Gdk.DragAction.COPY) - 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) - - def controller(self): - return self.drop_target - - def on_drop_enter(self, *_): - self.overlay.add_css_class("drop-highlight") - return True - - def on_drop_leave(self, *_): - self.overlay.remove_css_class("drop-highlight") - return True - - def on_drop(self, _, value, __, ___): - self.overlay.remove_overlay(self.drop_hint) - self.drop_hint.set_visible(False) - self.progress_bar.set_visible(True) - self.progress_bar.set_fraction(0.0) - GLib.idle_add(partial(self.on_files_dropped, value)) - return value - - -class AppState(Enum): - IDLE = auto() # Nothing loaded, waiting for drop - FILES_LOADED = auto() # Files dropped, ready to start transcoding - TRANSCODING = auto() # Transcoding in progress - PAUSED = auto() # Transcoding paused - DONE = auto() # Transcoding finished - STOPPED = auto() # Transcoding canceled - ERROR = auto() # Error occurred - - -class UIStateManager: - def __init__(self, window): - self.window = window - self.handlers = { - AppState.IDLE: self._handle_idle, - AppState.FILES_LOADED: self._handle_files_loaded, - AppState.TRANSCODING: self._handle_transcoding, - AppState.PAUSED: self._handle_paused, - AppState.DONE: self._handle_done, - AppState.STOPPED: lambda: self.set_state(AppState.IDLE), - AppState.ERROR: self._handle_error, - } - - def set_state(self, state: AppState): - handler = self.handlers.get(state) - if handler: - handler() - - def _handle_idle(self): - w = self.window - w.clear_listbox() - w.file_rows.clear() - w.file_items_to_process = [] - w.is_paused = False - w.progress_bar.set_visible(False) - w.progress_bar.set_fraction(0.0) - w.progress_bar.set_text("") - w.drop_hint.set_visible(True) - if w.drop_hint.get_parent() != w.overlay: - w.overlay.add_overlay(w.drop_hint) - w.btn_transcode.set_visible(False) - w.btn_cancel.set_visible(False) - - def _handle_files_loaded(self): - w = self.window - w.drop_hint.set_visible(False) - w.progress_bar.set_visible(False) - w.btn_transcode.set_visible(True) - w.btn_transcode.set_sensitive(True) - w.btn_transcode.set_label("Start Transcoding") - w.btn_transcode.add_css_class("suggested-action") - w.btn_cancel.set_visible(False) - w.is_paused = False - - def _handle_transcoding(self): - w = self.window - w.drop_hint.set_visible(False) - w.progress_bar.set_visible(True) - w.btn_transcode.set_visible(True) - w.btn_transcode.set_sensitive(True) - w.btn_transcode.set_label("Pause") - w.btn_transcode.remove_css_class("suggested-action") - w.btn_cancel.set_visible(False) - w.is_paused = False - - def _handle_paused(self): - w = self.window - w.drop_hint.set_visible(False) - w.progress_bar.set_visible(True) - w.btn_transcode.set_visible(True) - w.btn_transcode.set_sensitive(True) - w.btn_transcode.set_label("Resume") - w.btn_cancel.set_visible(True) - w.is_paused = True - - def _handle_done(self): - w = self.window - w.drop_hint.set_visible(False) - w.progress_bar.set_visible(True) - w.progress_bar.set_fraction(1.0) - w.btn_transcode.set_visible(False) - w.btn_transcode.remove_css_class("suggested-action") - w.btn_cancel.set_visible(True) - w.is_paused = False - - def _handle_error(self): - w = self.window - w.drop_hint.set_visible(False) - w.progress_bar.set_visible(False) - w.btn_transcode.set_visible(False) - w.btn_cancel.set_visible(True) - w.is_paused = False - +from recoder.drop_handler import DropHandler +from recoder.app_state import AppState, AppStateManager, UIStateManager @Gtk.Template(resource_path="/net/jeena/recoder/recoder_window.ui") @@ -161,15 +36,14 @@ class RecoderWindow(Adw.ApplicationWindow): self.file_rows = {} self.is_paused = False - self.drop_handler = DropHandler(self.overlay, self.drop_hint, self.progress_bar, self.process_drop_value) - self.add_controller(self.drop_handler.controller()) - - self.ui_manager = UIStateManager(self) + self.app_state_manager = AppStateManager() + self.drop_handler = DropHandler(self, self.app_state_manager) + self.ui_manager = UIStateManager(self, self.app_state_manager) self.btn_transcode.connect("clicked", self.on_transcode_clicked) self.btn_cancel.connect("clicked", self.on_cancel_clicked) - self.ui_manager.set_state(AppState.IDLE) + self.app_state_manager.state = AppState.IDLE css_provider = Gtk.CssProvider() css_provider.load_from_resource("/net/jeena/recoder/style.css") @@ -195,7 +69,7 @@ class RecoderWindow(Adw.ApplicationWindow): self.file_rows[file_item.file] = row self.file_items_to_process = file_items - self.ui_manager.set_state(AppState.FILES_LOADED) + self.app_state_manager.state = AppState.FILES_LOADED return False def clear_listbox(self): @@ -218,50 +92,45 @@ class RecoderWindow(Adw.ApplicationWindow): if not self.file_items_to_process: return - self.remove_controller(self.drop_handler.controller()) + # no need to remove drop_controller here self.transcoder = Transcoder(self.file_items_to_process) self.transcoder.connect("notify::batch-progress", self.on_transcoder_progress) self.transcoder.connect("notify::batch-status", self.on_transcoder_status) self.transcoder.start() - self.ui_manager.set_state(AppState.TRANSCODING) + self.app_state_manager.state = AppState.TRANSCODING def pause_transcoding(self): if self.transcoder: self.transcoder.pause() self.is_paused = True - self.ui_manager.set_state(AppState.PAUSED) + self.app_state_manager.state = AppState.PAUSED def resume_transcoding(self): if self.transcoder: self.transcoder.resume() self.is_paused = False - self.ui_manager.set_state(AppState.TRANSCODING) + self.app_state_manager.state = AppState.TRANSCODING def on_cancel_clicked(self, button): if self.transcoder and self.transcoder.is_processing: self.transcoder.stop() self.transcoder = None - self.add_controller(self.drop_handler.controller()) - self.ui_manager.set_state(AppState.STOPPED) + self.app_state_manager.state = AppState.STOPPED def on_transcoder_progress(self, transcoder, param): self.progress_bar.set_fraction(transcoder.batch_progress / 100.0) - self.progress_bar.set_visible(True) def on_transcoder_status(self, transcoder, param): if transcoder.batch_status == BatchStatus.DONE: play_complete_sound() notify_done("Recoder", "Transcoding finished!") - self.add_controller(self.drop_handler.controller()) - self.ui_manager.set_state(AppState.DONE) + self.app_state_manager.state = AppState.DONE elif transcoder.batch_status == BatchStatus.STOPPED: - self.add_controller(self.drop_handler.controller()) - self.ui_manager.set_state(AppState.STOPPED) + self.app_state_manager.state = AppState.STOPPED elif transcoder.batch_status == BatchStatus.ERROR: notify_done("Recoder", "An error occurred during transcoding.") - self.add_controller(self.drop_handler.controller()) - self.ui_manager.set_state(AppState.ERROR) + self.app_state_manager.state = AppState.ERROR diff --git a/src/recoder/transcoder.py b/src/recoder/transcoder.py index 05a513d..4765e6e 100644 --- a/src/recoder/transcoder.py +++ b/src/recoder/transcoder.py @@ -83,7 +83,8 @@ class Transcoder(GObject.GObject): GLib.idle_add(file_item.set_property, "status", new_status) GLib.idle_add(file_item.set_property, "progress", 100 if success else 0) - if not success: + # Fix: Only set ERROR if not stopped by user + if not success and not self._stop_requested: self.batch_status = BatchStatus.ERROR break diff --git a/src/resources/recoder_window.ui b/src/resources/recoder_window.ui index 4e3f5f6..0d60a61 100644 --- a/src/resources/recoder_window.ui +++ b/src/resources/recoder_window.ui @@ -18,7 +18,7 @@ False - + Cancel False @@ -68,7 +68,6 @@ - true diff --git a/src/resources/style.css b/src/resources/style.css index 5d995f3..04c5d66 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -1,6 +1,9 @@ .drop-highlight { - background-color: @theme_selected_bg_color; - border: 2px solid @theme_selected_fg_color; + background-color: @theme_selected_bg_color; /* good: uses theme color */ + border: 2px solid @theme_selected_fg_color; /* good contrast */ + border-radius: 6px; /* soften edges */ + box-shadow: 0 0 8px @theme_selected_bg_color; /* subtle glow */ + transition: background-color 150ms ease, box-shadow 150ms ease; /* smooth */ } .dim-label {