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
-
+
-