From c4bef281f76bec47ae73d1dad6c4bfb24774b7c1 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Tue, 3 Mar 2026 23:53:27 +1100 Subject: [PATCH] Add video scrubbing and click to seek/change volume on sliders --- desktop-client/room_widget.py | 61 +++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/desktop-client/room_widget.py b/desktop-client/room_widget.py index 276dd24..7437925 100644 --- a/desktop-client/room_widget.py +++ b/desktop-client/room_widget.py @@ -2,7 +2,7 @@ import os import datetime from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QFrame, QSlider, QTextEdit, QApplication + QPushButton, QFrame, QSlider, QTextEdit, QApplication, QToolTip ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer from PyQt6.QtGui import QIcon @@ -10,6 +10,49 @@ import uuid from vlc_player import VLCSyncPlayer +class ClickableSlider(QSlider): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._get_tooltip_text = None + + def set_tooltip_provider(self, provider_func): + self._get_tooltip_text = provider_func + + def mousePressEvent(self, event): + super().mousePressEvent(event) + if event.button() == Qt.MouseButton.LeftButton: + val = 0 + if self.orientation() == Qt.Orientation.Horizontal: + if self.width() > 0: + val = self.minimum() + ((self.maximum() - self.minimum()) * event.pos().x()) / self.width() + else: + if self.height() > 0: + val = self.minimum() + ((self.maximum() - self.minimum()) * (self.height() - event.pos().y())) / self.height() + + val = max(self.minimum(), min(self.maximum(), int(val))) + self.setValue(val) + self.sliderMoved.emit(val) + + if self._get_tooltip_text: + text = self._get_tooltip_text(val) + if text: + QToolTip.showText(event.globalPosition().toPoint(), text, self) + + def mouseMoveEvent(self, event): + super().mouseMoveEvent(event) + if event.buttons() & Qt.MouseButton.LeftButton: + if self._get_tooltip_text: + text = self._get_tooltip_text(self.value()) + if text: + QToolTip.showText(event.globalPosition().toPoint(), text, self) + + def mouseReleaseEvent(self, event): + super().mouseReleaseEvent(event) + if event.button() == Qt.MouseButton.LeftButton: + # Explicitly emit sliderReleased on mouse release + # to ensure single clicks on the track also trigger the release logic + self.sliderReleased.emit() + class ExpectedVlcEvent: def __init__(self, action: str, req_id: str, target_val=None): self.action = action @@ -90,23 +133,25 @@ class RoomWidget(QWidget): self.play_btn.setObjectName("playBtn") # Time and SeekBar - self.seekbar = QSlider(Qt.Orientation.Horizontal) + self.seekbar = ClickableSlider(Qt.Orientation.Horizontal) self.seekbar.setRange(0, 1000) self.seekbar.setObjectName("seekBar") self.seekbar.sliderMoved.connect(self.on_seekbar_dragged) self.seekbar.sliderReleased.connect(self.on_seekbar_released) + self.seekbar.set_tooltip_provider(self.get_seekbar_tooltip) self.time_lbl = QLabel("00:00:00 / 00:00:00") # Volume self.vol_icon = QLabel("🔊") self.vol_icon.setObjectName("volIcon") - self.volume_slider = QSlider(Qt.Orientation.Horizontal) + self.volume_slider = ClickableSlider(Qt.Orientation.Horizontal) self.volume_slider.setRange(0, 100) self.volume_slider.setValue(100) self.volume_slider.setFixedWidth(100) self.volume_slider.setObjectName("volumeSlider") self.volume_slider.valueChanged.connect(self.on_volume_changed) + self.volume_slider.set_tooltip_provider(lambda v: f"{v}%") # Fullscreen self.fullscreen_btn = QPushButton("⛶") @@ -172,6 +217,14 @@ class RoomWidget(QWidget): self.play_btn.clicked.connect(self.toggle_playback) + def get_seekbar_tooltip(self, value): + length_ms = self.vlc_player.get_length() + if length_ms > 0: + target_ms = int((value / 1000.0) * length_ms) + s = max(0, target_ms) // 1000 + return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}" + return "" + def setup_room(self, room_code: str, username: str, file_name: str, file_path: str, start_time_s: float = 0.0): self.room_code = room_code self.username = username @@ -313,6 +366,8 @@ class RoomWidget(QWidget): s = max(0, ms) // 1000 return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}" self.time_lbl.setText(f"{fmt(target_ms)} / {fmt(length_ms)}") + # Scrub the video locally in real-time + self._tell_vlc_and_expect("seek", target_ms / 1000.0) def on_seekbar_released(self): length_ms = self.vlc_player.get_length()