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) self.video_picture.set_hexpand(True)
# Create main menu clock # Create main menu clock
self.clock = Gtk.Label() self.clock = Gtk.Label(
self.clock.set_name("main-clock") name="main-clock",
self.clock.set_halign(Gtk.Align.CENTER) halign=Gtk.Align.START,
self.clock.set_valign(Gtk.Align.CENTER) valign=Gtk.Align.START,
)
# Create image widget for thumbnail # Create image widget for thumbnail
self.thumbnail_picture = Gtk.Picture() self.thumbnail_picture = Gtk.Picture(
self.thumbnail_picture.set_name("thumbnail-picture") name="thumbnail-picture",
can_shrink=True,
keep_aspect_ratio=True,
)
self.thumbnail_picture.set_size_request(384, 216) 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) 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_model = FileListModel()
self.list_view = Gtk.ListView() self.selection_model = Gtk.SingleSelection(model=self.list_model)
self.selection_model = Gtk.SingleSelection.new(self.list_model)
self.selection_model.connect("selection-changed", self._on_selection_changed) 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) self.list_view.connect("activate", self._on_activate)
# Factory for list items # Factory for list items

View file

@ -1,9 +1,3 @@
listview > row {
padding: 8px;
font-size: 24px;
font-family: monospace;
}
.file-icon { .file-icon {
-gtk-icon-size: 24px; -gtk-icon-size: 24px;
} }
@ -20,6 +14,28 @@ listview > row {
color: #0f0; 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 { #overlay {
background-color: black; background-color: black;
} }
@ -34,19 +50,43 @@ listview > row {
margin: 32px; margin: 32px;
} }
#thumbnail-picture {
margin: 8px;
}
#main-clock, #main-clock,
#overlay-clock { #overlay-clock {
color: white; color: white;
font-size: 48px; font-size: 48px;
font-family: monospace; font-family: monospace;
padding: 12px; 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 { #overlay-clock {
background-color: rgba(32, 32, 32, 0.5); background-color: rgba(64, 64, 64, 0.5);
border-radius: 8px; 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: Gtk.Grid
grid_expiration: float grid_expiration: float
clock_box: Gtk.Box
clock: Gtk.Label clock: Gtk.Label
progressbar: Gtk.ProgressBar
now: float now: float
@ -56,20 +56,36 @@ class VideoOverlay(Gtk.Overlay, Watcher):
self.grid_expiration = 0.0 self.grid_expiration = 0.0
# Create grid boxes. # Create grid boxes.
self.clock_box = Gtk.Box(hexpand=True, vexpand=True) clock_box = Gtk.Box(hexpand=True, vexpand=True)
self.grid.attach(self.clock_box, 0, 0, 1, 1) self.grid.attach(clock_box, 0, 0, 1, 1)
self.grid.attach(Gtk.Box(), 1, 0, 2, 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, 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. # Add clock to the top-left grid box.
self.clock = Gtk.Label(hexpand=True, vexpand=True) self.clock = Gtk.Label(
self.clock.set_name("overlay-clock") name="overlay-clock",
self.clock.set_halign(Gtk.Align.CENTER) halign=Gtk.Align.START,
self.clock.set_valign(Gtk.Align.START) valign=Gtk.Align.START,
self.clock.set_text(datetime.now().strftime("%H:%M")) )
self.clock_box.append(self.clock) 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. # Add children.
self.set_child(self.player.picture) self.set_child(self.player.picture)
@ -93,6 +109,12 @@ class VideoOverlay(Gtk.Overlay, Watcher):
if self.message_expiration <= self.now: if self.message_expiration <= self.now:
self.message.hide() 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 return True
def show_message(self, text: str, timeout: float = 1.0) -> None: 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_playing = self.player.is_playing.value
is_paused = self.player.is_paused.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: if is_playing and is_paused:
self.grid.show()
self.grid_expiration = 1e20 self.grid_expiration = 1e20
else: else:
self.grid_expiration = 0 self.grid_expiration = self.now + 1.0

View file

@ -1,12 +1,15 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from time import time
from gi.repository import GObject, Gst, Gtk from gi.repository import GObject, Gst, Gtk
from .reactive import Ref 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): class VideoPlayer(GObject.Object):
@ -19,6 +22,8 @@ class VideoPlayer(GObject.Object):
is_playing: Ref[bool] is_playing: Ref[bool]
is_paused: Ref[bool] is_paused: Ref[bool]
last_user_input: Ref[float]
def __init__(self, picture: Gtk.Picture): def __init__(self, picture: Gtk.Picture):
super().__init__() super().__init__()
@ -26,6 +31,7 @@ class VideoPlayer(GObject.Object):
self.is_playing = Ref(False) self.is_playing = Ref(False)
self.is_paused = Ref(True) self.is_paused = Ref(True)
self.last_user_input = Ref(time())
self.pipeline = Gst.Pipeline.new("video-player") self.pipeline = Gst.Pipeline.new("video-player")
@ -75,12 +81,13 @@ class VideoPlayer(GObject.Object):
if position: if position:
# Seek to saved 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 # Start playing
self.pipeline.set_state(Gst.State.PLAYING) self.pipeline.set_state(Gst.State.PLAYING)
self.is_playing.value = True self.is_playing.value = True
self.is_paused.value = False self.is_paused.value = False
self.last_user_input.value = time()
def stop(self) -> None: def stop(self) -> None:
"""Stop playback and release resources""" """Stop playback and release resources"""
@ -89,10 +96,15 @@ class VideoPlayer(GObject.Object):
self.is_playing.value = True self.is_playing.value = True
self.is_paused.value = False self.is_paused.value = False
self.last_user_input.value = time()
def toggle_play_pause(self) -> None: def toggle_play_pause(self) -> None:
"""Toggle between play and pause states""" """Toggle between play and pause states"""
self.last_user_input.value = time()
_, state, _ = self.pipeline.get_state(0) _, state, _ = self.pipeline.get_state(0)
if state == Gst.State.PLAYING: if state == Gst.State.PLAYING:
self.pipeline.set_state(Gst.State.PAUSED) self.pipeline.set_state(Gst.State.PAUSED)
self.is_paused.value = True self.is_paused.value = True
@ -102,6 +114,9 @@ class VideoPlayer(GObject.Object):
def seek_relative(self, offset: float) -> None: def seek_relative(self, offset: float) -> None:
"""Seek relative to current position by offset seconds""" """Seek relative to current position by offset seconds"""
self.last_user_input.value = time()
success, current = self.pipeline.query_position(Gst.Format.TIME) success, current = self.pipeline.query_position(Gst.Format.TIME)
if not success: if not success:
return return
@ -110,17 +125,25 @@ class VideoPlayer(GObject.Object):
if new_pos < 0: if new_pos < 0:
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): def seek_start(self):
"""Seek to the start of the video.""" """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): def seek_end(self):
"""Seek to the end of the video.""" """Seek to the end of the video."""
self.last_user_input.value = time()
if duration := self.get_duration(): 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: def get_position(self) -> int | None:
"""Get current playback position in nanoseconds""" """Get current playback position in nanoseconds"""