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"""