From 5894e55c369fd5f5e5488a21af4b3c79d0ffb6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hamal=20Dvo=C5=99=C3=A1k?= <mordae@anilinux.org> Date: Tue, 11 Mar 2025 20:13:15 +0100 Subject: [PATCH] Tweak UI --- lazy_player/main_window.py | 32 ++++++++++-------- lazy_player/style.css | 64 +++++++++++++++++++++++++++++------- lazy_player/video_overlay.py | 50 +++++++++++++++++++++------- lazy_player/video_player.py | 33 ++++++++++++++++--- 4 files changed, 137 insertions(+), 42 deletions(-) diff --git a/lazy_player/main_window.py b/lazy_player/main_window.py index c4362af..25b769a 100644 --- a/lazy_player/main_window.py +++ b/lazy_player/main_window.py @@ -74,26 +74,32 @@ class MainWindow(Gtk.ApplicationWindow, Watcher): self.video_picture.set_hexpand(True) # Create main menu clock - self.clock = Gtk.Label() - self.clock.set_name("main-clock") - self.clock.set_halign(Gtk.Align.CENTER) - self.clock.set_valign(Gtk.Align.CENTER) + self.clock = Gtk.Label( + name="main-clock", + halign=Gtk.Align.START, + valign=Gtk.Align.START, + ) # Create image widget for thumbnail - self.thumbnail_picture = Gtk.Picture() - self.thumbnail_picture.set_name("thumbnail-picture") + self.thumbnail_picture = Gtk.Picture( + name="thumbnail-picture", + can_shrink=True, + keep_aspect_ratio=True, + ) self.thumbnail_picture.set_size_request(384, 216) - self.thumbnail_picture.set_can_shrink(True) - self.thumbnail_picture.set_keep_aspect_ratio(True) self.thumbnail_picture.set_resource(None) - # Create list model and view + # Create list & selection model for the file list view. self.list_model = FileListModel() - self.list_view = Gtk.ListView() - self.selection_model = Gtk.SingleSelection.new(self.list_model) + self.selection_model = Gtk.SingleSelection(model=self.list_model) self.selection_model.connect("selection-changed", self._on_selection_changed) - self.list_view.set_model(self.selection_model) - self.list_view.set_vexpand(True) + + # Create file list view. + self.list_view = Gtk.ListView( + name="file-list", + vexpand=True, + model=self.selection_model, + ) self.list_view.connect("activate", self._on_activate) # Factory for list items diff --git a/lazy_player/style.css b/lazy_player/style.css index 07a5fb6..795bded 100644 --- a/lazy_player/style.css +++ b/lazy_player/style.css @@ -1,9 +1,3 @@ -listview > row { - padding: 8px; - font-size: 24px; - font-family: monospace; -} - .file-icon { -gtk-icon-size: 24px; } @@ -20,6 +14,28 @@ listview > row { color: #0f0; } +#file-list { + margin: 8px; + margin-top: 122px; + border-radius: 4px; + box-shadow: rgba(32, 32, 32, 0.5) 0px 0px 4px; +} + +#file-list > row { + padding: 8px; + font-size: 24px; + font-family: monospace; +} + +#thumbnail-picture { + margin: 16px; + margin-top: 32px; + margin-right: 8px; + border-radius: 4px; + box-shadow: rgba(32, 32, 32, 0.5) 0px 0px 4px; + background: #444; +} + #overlay { background-color: black; } @@ -34,19 +50,43 @@ listview > row { margin: 32px; } -#thumbnail-picture { - margin: 8px; -} - #main-clock, #overlay-clock { color: white; font-size: 48px; font-family: monospace; padding: 12px; + box-shadow: rgba(32, 32, 32, 0.5) 0px 0px 4px; + background-color: rgba(32, 32, 32, 1); + border-bottom-right-radius: 8px; } #overlay-clock { - background-color: rgba(32, 32, 32, 0.5); - border-radius: 8px; + background-color: rgba(64, 64, 64, 0.5); + border-bottom-right-radius: 8px; +} + +#progressbar { + margin-left: 64px; + margin-right: 64px; + margin-bottom: 32px; + border-radius: 0px; + box-shadow: rgba(32, 32, 32, 0.5) 0px 0px 8px; + background: rgba(32, 32, 32, 0.5); + border: 0px; +} + +#progressbar trough { + min-height: 32px; + border-radius: 0px; + padding: 2px; + border: 2px solid #fff; + background: transparent; +} + +#progressbar progress { + border-radius: 0px; + min-height: 32px; + background: #fff; + border: 0px; } diff --git a/lazy_player/video_overlay.py b/lazy_player/video_overlay.py index 54695df..f51fd68 100644 --- a/lazy_player/video_overlay.py +++ b/lazy_player/video_overlay.py @@ -18,8 +18,8 @@ class VideoOverlay(Gtk.Overlay, Watcher): grid: Gtk.Grid grid_expiration: float - clock_box: Gtk.Box clock: Gtk.Label + progressbar: Gtk.ProgressBar now: float @@ -56,20 +56,36 @@ class VideoOverlay(Gtk.Overlay, Watcher): self.grid_expiration = 0.0 # Create grid boxes. - self.clock_box = Gtk.Box(hexpand=True, vexpand=True) - self.grid.attach(self.clock_box, 0, 0, 1, 1) + clock_box = Gtk.Box(hexpand=True, vexpand=True) + self.grid.attach(clock_box, 0, 0, 1, 1) self.grid.attach(Gtk.Box(), 1, 0, 2, 1) self.grid.attach(Gtk.Box(), 0, 1, 3, 1) - self.grid.attach(Gtk.Box(), 0, 2, 3, 1) + + progressbar_box = Gtk.Box( + name="progressbar-box", + hexpand=True, + vexpand=True, + ) + self.grid.attach(progressbar_box, 0, 2, 3, 1) # Add clock to the top-left grid box. - self.clock = Gtk.Label(hexpand=True, vexpand=True) - self.clock.set_name("overlay-clock") - self.clock.set_halign(Gtk.Align.CENTER) - self.clock.set_valign(Gtk.Align.START) - self.clock.set_text(datetime.now().strftime("%H:%M")) - self.clock_box.append(self.clock) + self.clock = Gtk.Label( + name="overlay-clock", + halign=Gtk.Align.START, + valign=Gtk.Align.START, + ) + clock_box.append(self.clock) + + # Create progressbar. + self.progressbar = Gtk.ProgressBar( + name="progressbar", + hexpand=True, + halign=Gtk.Align.FILL, + valign=Gtk.Align.END, + focusable=False, + ) + progressbar_box.append(self.progressbar) # Add children. self.set_child(self.player.picture) @@ -93,6 +109,12 @@ class VideoOverlay(Gtk.Overlay, Watcher): if self.message_expiration <= self.now: self.message.hide() + position = self.player.get_position() + duration = self.player.get_duration() + + if position is not None and duration is not None: + self.progressbar.set_fraction(position / duration) + return True def show_message(self, text: str, timeout: float = 1.0) -> None: @@ -106,8 +128,12 @@ class VideoOverlay(Gtk.Overlay, Watcher): is_playing = self.player.is_playing.value is_paused = self.player.is_paused.value + # Just to track other user interactions. + self.player.last_user_input.value + + self.grid.show() + if is_playing and is_paused: - self.grid.show() self.grid_expiration = 1e20 else: - self.grid_expiration = 0 + self.grid_expiration = self.now + 1.0 diff --git a/lazy_player/video_player.py b/lazy_player/video_player.py index ca71639..bd37c76 100644 --- a/lazy_player/video_player.py +++ b/lazy_player/video_player.py @@ -1,12 +1,15 @@ from __future__ import annotations from pathlib import Path +from time import time from gi.repository import GObject, Gst, Gtk from .reactive import Ref -DEFAULT_SEEK_FLAGS = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT +SEEK_FORWARD = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT | Gst.SeekFlags.SNAP_AFTER +SEEK_BACKWARD = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT | Gst.SeekFlags.SNAP_BEFORE +SEEK_ACCURATE = Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE class VideoPlayer(GObject.Object): @@ -19,6 +22,8 @@ class VideoPlayer(GObject.Object): is_playing: Ref[bool] is_paused: Ref[bool] + last_user_input: Ref[float] + def __init__(self, picture: Gtk.Picture): super().__init__() @@ -26,6 +31,7 @@ class VideoPlayer(GObject.Object): self.is_playing = Ref(False) self.is_paused = Ref(True) + self.last_user_input = Ref(time()) self.pipeline = Gst.Pipeline.new("video-player") @@ -75,12 +81,13 @@ class VideoPlayer(GObject.Object): if position: # Seek to saved position - self.pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, position) + self.pipeline.seek_simple(Gst.Format.TIME, SEEK_ACCURATE, position) # Start playing self.pipeline.set_state(Gst.State.PLAYING) self.is_playing.value = True self.is_paused.value = False + self.last_user_input.value = time() def stop(self) -> None: """Stop playback and release resources""" @@ -89,10 +96,15 @@ class VideoPlayer(GObject.Object): self.is_playing.value = True self.is_paused.value = False + self.last_user_input.value = time() def toggle_play_pause(self) -> None: """Toggle between play and pause states""" + + self.last_user_input.value = time() + _, state, _ = self.pipeline.get_state(0) + if state == Gst.State.PLAYING: self.pipeline.set_state(Gst.State.PAUSED) self.is_paused.value = True @@ -102,6 +114,9 @@ class VideoPlayer(GObject.Object): def seek_relative(self, offset: float) -> None: """Seek relative to current position by offset seconds""" + + self.last_user_input.value = time() + success, current = self.pipeline.query_position(Gst.Format.TIME) if not success: return @@ -110,17 +125,25 @@ class VideoPlayer(GObject.Object): if new_pos < 0: new_pos = 0 - self.pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, new_pos) + self.pipeline.seek_simple( + Gst.Format.TIME, + SEEK_FORWARD if offset >= 0 else SEEK_BACKWARD, + new_pos, + ) def seek_start(self): """Seek to the start of the video.""" - self.pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, 0) + + self.last_user_input.value = time() + self.pipeline.seek_simple(Gst.Format.TIME, SEEK_BACKWARD, 0) def seek_end(self): """Seek to the end of the video.""" + self.last_user_input.value = time() + if duration := self.get_duration(): - self.pipeline.seek_simple(Gst.Format.TIME, DEFAULT_SEEK_FLAGS, duration) + self.pipeline.seek_simple(Gst.Format.TIME, SEEK_FORWARD, duration) def get_position(self) -> int | None: """Get current playback position in nanoseconds"""