Make saving stuff reactive

This commit is contained in:
Jan Hamal Dvořák 2025-03-11 17:41:24 +01:00
parent 33ca05a3cb
commit 758f017f22
3 changed files with 63 additions and 67 deletions

View file

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

View file

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

View file

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