This commit is contained in:
Jan Hamal Dvořák 2025-03-11 20:13:15 +01:00
parent 3c7953d815
commit 5894e55c36
4 changed files with 137 additions and 42 deletions

View file

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

View file

@ -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;
}

View file

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

View file

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