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.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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue