Moving cancel button and restructuring

Especially the app state has been reinvented
This commit is contained in:
Jeena 2025-06-05 00:53:12 +09:00
parent c3749a476e
commit 1464273506
6 changed files with 186 additions and 151 deletions

107
src/recoder/app_state.py Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@
<property name="sensitive">False</property>
</object>
</child>
<child>
<child type="end">
<object class="GtkButton" id="btn_cancel">
<property name="label">Cancel</property>
<property name="visible">False</property>
@ -68,7 +68,6 @@
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="progress_bar">
<property name="hexpand">true</property>

View file

@ -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 {