Moving cancel button and restructuring
Especially the app state has been reinvented
This commit is contained in:
parent
c3749a476e
commit
1464273506
6 changed files with 186 additions and 151 deletions
107
src/recoder/app_state.py
Normal file
107
src/recoder/app_state.py
Normal 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
|
56
src/recoder/drop_handler.py
Normal file
56
src/recoder/drop_handler.py
Normal 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
|
|
@ -12,133 +12,8 @@ from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Notify
|
||||||
from recoder.transcoder import Transcoder, BatchStatus
|
from recoder.transcoder import Transcoder, BatchStatus
|
||||||
from recoder.utils import extract_video_files, notify_done, play_complete_sound
|
from recoder.utils import extract_video_files, notify_done, play_complete_sound
|
||||||
from recoder.file_entry_row import FileEntryRow
|
from recoder.file_entry_row import FileEntryRow
|
||||||
|
from recoder.drop_handler import DropHandler
|
||||||
|
from recoder.app_state import AppState, AppStateManager, UIStateManager
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template(resource_path="/net/jeena/recoder/recoder_window.ui")
|
@Gtk.Template(resource_path="/net/jeena/recoder/recoder_window.ui")
|
||||||
|
@ -161,15 +36,14 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
self.file_rows = {}
|
self.file_rows = {}
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
|
|
||||||
self.drop_handler = DropHandler(self.overlay, self.drop_hint, self.progress_bar, self.process_drop_value)
|
self.app_state_manager = AppStateManager()
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.drop_handler = DropHandler(self, self.app_state_manager)
|
||||||
|
self.ui_manager = UIStateManager(self, self.app_state_manager)
|
||||||
self.ui_manager = UIStateManager(self)
|
|
||||||
|
|
||||||
self.btn_transcode.connect("clicked", self.on_transcode_clicked)
|
self.btn_transcode.connect("clicked", self.on_transcode_clicked)
|
||||||
self.btn_cancel.connect("clicked", self.on_cancel_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 = Gtk.CssProvider()
|
||||||
css_provider.load_from_resource("/net/jeena/recoder/style.css")
|
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_rows[file_item.file] = row
|
||||||
|
|
||||||
self.file_items_to_process = file_items
|
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
|
return False
|
||||||
|
|
||||||
def clear_listbox(self):
|
def clear_listbox(self):
|
||||||
|
@ -218,50 +92,45 @@ class RecoderWindow(Adw.ApplicationWindow):
|
||||||
if not self.file_items_to_process:
|
if not self.file_items_to_process:
|
||||||
return
|
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 = Transcoder(self.file_items_to_process)
|
||||||
self.transcoder.connect("notify::batch-progress", self.on_transcoder_progress)
|
self.transcoder.connect("notify::batch-progress", self.on_transcoder_progress)
|
||||||
self.transcoder.connect("notify::batch-status", self.on_transcoder_status)
|
self.transcoder.connect("notify::batch-status", self.on_transcoder_status)
|
||||||
self.transcoder.start()
|
self.transcoder.start()
|
||||||
|
|
||||||
self.ui_manager.set_state(AppState.TRANSCODING)
|
self.app_state_manager.state = AppState.TRANSCODING
|
||||||
|
|
||||||
def pause_transcoding(self):
|
def pause_transcoding(self):
|
||||||
if self.transcoder:
|
if self.transcoder:
|
||||||
self.transcoder.pause()
|
self.transcoder.pause()
|
||||||
self.is_paused = True
|
self.is_paused = True
|
||||||
self.ui_manager.set_state(AppState.PAUSED)
|
self.app_state_manager.state = AppState.PAUSED
|
||||||
|
|
||||||
def resume_transcoding(self):
|
def resume_transcoding(self):
|
||||||
if self.transcoder:
|
if self.transcoder:
|
||||||
self.transcoder.resume()
|
self.transcoder.resume()
|
||||||
self.is_paused = False
|
self.is_paused = False
|
||||||
self.ui_manager.set_state(AppState.TRANSCODING)
|
self.app_state_manager.state = AppState.TRANSCODING
|
||||||
|
|
||||||
def on_cancel_clicked(self, button):
|
def on_cancel_clicked(self, button):
|
||||||
if self.transcoder and self.transcoder.is_processing:
|
if self.transcoder and self.transcoder.is_processing:
|
||||||
self.transcoder.stop()
|
self.transcoder.stop()
|
||||||
self.transcoder = None
|
self.transcoder = None
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.app_state_manager.state = AppState.STOPPED
|
||||||
self.ui_manager.set_state(AppState.STOPPED)
|
|
||||||
|
|
||||||
def on_transcoder_progress(self, transcoder, param):
|
def on_transcoder_progress(self, transcoder, param):
|
||||||
self.progress_bar.set_fraction(transcoder.batch_progress / 100.0)
|
self.progress_bar.set_fraction(transcoder.batch_progress / 100.0)
|
||||||
self.progress_bar.set_visible(True)
|
|
||||||
|
|
||||||
def on_transcoder_status(self, transcoder, param):
|
def on_transcoder_status(self, transcoder, param):
|
||||||
if transcoder.batch_status == BatchStatus.DONE:
|
if transcoder.batch_status == BatchStatus.DONE:
|
||||||
play_complete_sound()
|
play_complete_sound()
|
||||||
notify_done("Recoder", "Transcoding finished!")
|
notify_done("Recoder", "Transcoding finished!")
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.app_state_manager.state = AppState.DONE
|
||||||
self.ui_manager.set_state(AppState.DONE)
|
|
||||||
|
|
||||||
elif transcoder.batch_status == BatchStatus.STOPPED:
|
elif transcoder.batch_status == BatchStatus.STOPPED:
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.app_state_manager.state = AppState.STOPPED
|
||||||
self.ui_manager.set_state(AppState.STOPPED)
|
|
||||||
|
|
||||||
elif transcoder.batch_status == BatchStatus.ERROR:
|
elif transcoder.batch_status == BatchStatus.ERROR:
|
||||||
notify_done("Recoder", "An error occurred during transcoding.")
|
notify_done("Recoder", "An error occurred during transcoding.")
|
||||||
self.add_controller(self.drop_handler.controller())
|
self.app_state_manager.state = AppState.ERROR
|
||||||
self.ui_manager.set_state(AppState.ERROR)
|
|
||||||
|
|
|
@ -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, "status", new_status)
|
||||||
GLib.idle_add(file_item.set_property, "progress", 100 if success else 0)
|
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
|
self.batch_status = BatchStatus.ERROR
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<property name="sensitive">False</property>
|
<property name="sensitive">False</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child type="end">
|
||||||
<object class="GtkButton" id="btn_cancel">
|
<object class="GtkButton" id="btn_cancel">
|
||||||
<property name="label">Cancel</property>
|
<property name="label">Cancel</property>
|
||||||
<property name="visible">False</property>
|
<property name="visible">False</property>
|
||||||
|
@ -68,7 +68,6 @@
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkProgressBar" id="progress_bar">
|
<object class="GtkProgressBar" id="progress_bar">
|
||||||
<property name="hexpand">true</property>
|
<property name="hexpand">true</property>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
.drop-highlight {
|
.drop-highlight {
|
||||||
background-color: @theme_selected_bg_color;
|
background-color: @theme_selected_bg_color; /* good: uses theme color */
|
||||||
border: 2px solid @theme_selected_fg_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 {
|
.dim-label {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue