Make saving stuff reactive
This commit is contained in:
parent
33ca05a3cb
commit
758f017f22
3 changed files with 63 additions and 67 deletions
lazy_player
|
@ -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: ...
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue