diff --git a/lazy_player/file_model.py b/lazy_player/file_model.py index 3054a2c..0dffe59 100644 --- a/lazy_player/file_model.py +++ b/lazy_player/file_model.py @@ -8,75 +8,66 @@ from typing import Optional, overload from gi.repository import Gio, GObject +from .reactive import Ref, Watcher + class FileType(Enum): DIRECTORY = auto() VIDEO = auto() -class FileItem(GObject.Object): +class FileItem(GObject.Object, Watcher): file_type: FileType full_path: Path - thumbnail: bytes - attempted_thumbnail: bool - _has_thumbnail: bool + thumbnail: Ref[bytes] + attempted_thumbnail: Ref[bool] + + saved_position: Ref[int] + saved_duration: Ref[int] + saved_subtitle_track: Ref[int] + saved_audio_track: Ref[int] __gtype_name__ = "FileItem" def __init__(self, name: str, file_type: FileType, full_path: Path): super().__init__() + self.name = name self.file_type = file_type self.full_path = full_path - self.thumbnail = b"" - self.attempted_thumbnail = False - self._has_thumbnail = False - @GObject.Property(type=GObject.TYPE_UINT64) - def saved_position(self) -> int: - return self._load_attribute("position", 0) + self.thumbnail = Ref(b"") + self.attempted_thumbnail = Ref(False) - @saved_position.setter - def set_saved_position(self, value: int): - self._save_attribute("position", value if value else None) - self.notify("saved-position") + self.saved_position = Ref(self._load_attribute("position", 0)) + self.saved_duration = Ref(self._load_attribute("duration", 1)) + self.saved_subtitle_track = Ref(self._load_attribute("subtitle_track", -2)) + self.saved_audio_track = Ref(self._load_attribute("audio_track", 0)) - @GObject.Property(type=GObject.TYPE_UINT64) - def saved_duration(self): - return self._load_attribute("duration", 1) or 1 + self.watch_all() - @saved_duration.setter - def set_saved_duration(self, value: int): - self._save_attribute("duration", value if value > 0 else None) - self.notify("saved-duration") + def _watch_saved_position(self): + saved_position = self.saved_position.value + self._save_attribute("position", saved_position if saved_position > 0 else None) - @GObject.Property(type=int) - def saved_subtitle_track(self): - return self._load_attribute("subtitle_track", -2) + def _watch_saved_duration(self): + saved_duration = self.saved_duration.value + self._save_attribute("duration", saved_duration if saved_duration > 0 else None) - @saved_subtitle_track.setter - def set_saved_subtitle_track(self, value: int): - self._save_attribute("subtitle_track", value if value >= -1 else None) - self.notify("saved-subtitle-track") + def _watch_saved_subtitle_track(self): + saved_subtitle_track = self.saved_subtitle_track.value + self._save_attribute( + "subtitle_track", + saved_subtitle_track if saved_subtitle_track > -2 else None, + ) - @GObject.Property(type=int) - def saved_audio_track(self): - return self._load_attribute("audio_track", 0) - - @saved_audio_track.setter - def set_saved_audio_track(self, value: int): - self._save_attribute("audio_track", value if value > 0 else None) - self.notify("saved-audio-track") - - @GObject.Property(type=bool, default=False) - def has_thumbnail(self): - return self._has_thumbnail - - @has_thumbnail.setter - def set_has_thumbnail(self, value: bool): - self._has_thumbnail = value - self.notify("has-thumbnail") + def _watch_saved_audio_track(self): + saved_audio_track = self.saved_audio_track.value + self._save_attribute( + "audio_track", + saved_audio_track if saved_audio_track > -1 else None, + ) @overload def _load_attribute(self, name: str, dfl: str) -> str: ... diff --git a/lazy_player/main_window.py b/lazy_player/main_window.py index f39aae7..bb4337c 100644 --- a/lazy_player/main_window.py +++ b/lazy_player/main_window.py @@ -129,7 +129,6 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): # Setup video player self.video_player = VideoPlayer(self.video_widget) - self.watch(self._sync_overlay_grid) # Add both main menu and overlay to stack self.stack.add_named(main_box, "menu") @@ -189,8 +188,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): self._navigate_to(file_item.full_path) return - position = file_item.saved_position - duration = file_item.saved_duration + position = file_item.saved_position.value + duration = file_item.saved_duration.value if (position / duration) >= 0.99: position = 0 @@ -199,8 +198,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): self.video_player.play( file_item.full_path, position, - file_item.saved_subtitle_track, - file_item.saved_audio_track, + file_item.saved_subtitle_track.value, + file_item.saved_audio_track.value, ) self.last_position_save = self.now @@ -225,6 +224,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): self.set_child(self.stack) + self.watch_all() + @property def now(self) -> float: frame_clock = self.get_frame_clock() @@ -266,8 +267,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): icon.set_from_icon_name("folder-symbolic") icon.set_css_classes(["file-icon"]) else: - position = item.saved_position - duration = item.saved_duration + position = item.saved_position.value + duration = item.saved_duration.value if position == 0: icon.set_from_icon_name("media-playback-start-symbolic") @@ -323,8 +324,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): if file_item := self.selection: # Update thumbnail if available - if file_item.thumbnail: - gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail)) + if file_item.thumbnail.value: + gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value)) texture = Gdk.Texture.new_from_bytes(gbytes) self.thumbnail_image.set_paintable(texture) else: @@ -387,7 +388,7 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): file_item = self.selection if file_item is not None: - file_item.saved_subtitle_track = index - 1 + file_item.saved_subtitle_track.value = index - 1 else: self.show_overlay_text("No subtitles available") @@ -400,7 +401,7 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): self.show_overlay_text(f"Audio #{index} ({lang})") file_item = self.selection if file_item is not None: - file_item.saved_audio_track = index - 1 + file_item.saved_audio_track.value = index - 1 else: self.show_overlay_text("No audio tracks available") @@ -416,15 +417,15 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): if file_item is None or file_item.file_type == FileType.DIRECTORY: return - position = file_item.saved_position - duration = file_item.saved_duration + position = file_item.saved_position.value + duration = file_item.saved_duration.value # If position exists and is >= 99% through, clear it if position > 0 and (position / duration) >= 0.99: - file_item.saved_position = 0 + file_item.saved_position.value = 0 else: # Otherwise mark as complete - file_item.saved_position = duration + file_item.saved_position.value = duration # Force the list to update the changed item self.list_model.items_changed(self.selection_model.get_selected(), 1, 1) @@ -479,23 +480,22 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): position = self.video_player.get_position() if position is not None: - file_item.saved_position = position + file_item.saved_position.value = position duration = self.video_player.get_duration() if duration is not None: - file_item.saved_duration = duration + file_item.saved_duration.value = duration def show_overlay_text(self, text: str, timeout_seconds: float = 1.0) -> None: """Show text in a centered overlay that disappears after timeout""" + self.overlay_label.set_text(text) self.overlay_label.set_visible(True) # Set absolute time when overlay should hide self.overlay_hide_time = self.now + timeout_seconds - def _sync_overlay_grid(self): - """Update grid visibility based on player state.""" - + def _watch_player_state(self): is_playing = self.video_player.is_playing.value is_paused = self.video_player.is_paused.value self.grid_overlay.set_visible(is_playing and is_paused) @@ -523,8 +523,8 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): if file_item := self.selection: self.thumbnailer.generate_thumbnail(file_item) - if file_item.thumbnail and not self.thumbnail_image.get_paintable(): - gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail)) + if file_item.thumbnail.value and not self.thumbnail_image.get_paintable(): + gbytes = GLib.Bytes.new(cast(Any, file_item.thumbnail.value)) texture = Gdk.Texture.new_from_bytes(gbytes) self.thumbnail_image.set_paintable(texture) diff --git a/lazy_player/reactive.py b/lazy_player/reactive.py index aae3b70..a9ea19c 100644 --- a/lazy_player/reactive.py +++ b/lazy_player/reactive.py @@ -81,6 +81,11 @@ class Computed(Ref[T]): class Watcher: _watches: set[Computed[Any]] + def watch_all(self, prefix: str = "_watch_"): + for method in dir(self): + if method.startswith(prefix): + self.watch(getattr(self, method)) + def watch(self, handler: Callable[[], Any]): if not hasattr(self, "_watches"): self._watches = set()