Persist desktop client preferences

This commit is contained in:
Peter Stockings
2026-10-05 16:06:59 +11:00
parent 75362bfcba
commit 836034533e
4 changed files with 110 additions and 7 deletions
+8
View File
@@ -1,5 +1,13 @@
# VideoSync desktop client
## Saved preferences
The client remembers your display name, volume, window geometry, chat sidebar
visibility and width, highlight panel visibility, and last selected media folder.
Preferences are stored locally using Qt's platform settings store. File dialogs
for both creating and joining rooms start in the remembered folder. The server
address remains fixed. Detached chat opens docked on the next launch.
## Chat
Chat follows new messages while you are near the bottom. Scroll up to read older
+5 -1
View File
@@ -24,6 +24,7 @@ class LobbyWidget(QWidget):
# Signals to communicate to VlcSyncApp
create_requested = pyqtSignal(str, str, str, object) # username, path, filename, size
join_requested = pyqtSignal(str, str) # username, room_code
file_selected = pyqtSignal(str)
def __init__(self):
super().__init__()
@@ -32,6 +33,7 @@ class LobbyWidget(QWidget):
self.local_file_path = None
self.local_file_name = None
self.local_file_size = 0
self.media_folder = ""
self._setup_ui()
@@ -126,6 +128,8 @@ class LobbyWidget(QWidget):
self.local_file_path = file_path
self.local_file_name = os.path.basename(file_path)
self.local_file_size = os.path.getsize(file_path)
self.media_folder = os.path.dirname(file_path)
self.file_selected.emit(file_path)
size_mb = self.local_file_size / (1024 * 1024)
self.create_file_info.setText(f"{self.local_file_name}\n{size_mb:.1f} MB")
@@ -134,7 +138,7 @@ class LobbyWidget(QWidget):
def select_file(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Select Video File", "", "Video Files (*.mp4 *.mkv *.avi *.mov *.webm);;All Files (*)"
self, "Select Video File", self.media_folder, "Video Files (*.mp4 *.mkv *.avi *.mov *.webm);;All Files (*)"
)
if file_path:
self._set_local_file(file_path)
+48 -3
View File
@@ -4,7 +4,7 @@ import ctypes
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QStackedWidget, QFileDialog, QMessageBox
)
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtCore import Qt, QTimer, QSettings
from PyQt6.QtGui import QIcon
from sync_client import SyncClientThread
@@ -12,8 +12,9 @@ from lobby_widget import LobbyWidget
from room_widget import RoomWidget
class VlcSyncApp(QMainWindow):
def __init__(self):
def __init__(self, settings=None):
super().__init__()
self.settings = settings if settings is not None else QSettings("VideoSync", "DesktopClient")
self.setWindowTitle("VideoSync — Watch Together")
icon_path = os.path.join(os.path.dirname(__file__), "icon.png")
@@ -72,6 +73,49 @@ class VlcSyncApp(QMainWindow):
self.sync_client.latency_updated.connect(self.room_widget.update_latency)
self.apply_stylesheet()
self._restore_preferences()
self.lobby_widget.username_input.textChanged.connect(
lambda name: self.settings.setValue("username", name))
self.lobby_widget.file_selected.connect(self._remember_media_folder)
self.room_widget.volume_slider.valueChanged.connect(
lambda volume: self.settings.setValue("volume", volume))
def _restore_preferences(self):
self.lobby_widget.username_input.setText(self.settings.value("username", "", type=str))
folder = self.settings.value("media_folder", "", type=str)
self.lobby_widget.media_folder = folder if os.path.isdir(folder) else ""
volume = self.settings.value("volume", 100, type=int)
self.room_widget.volume_slider.setValue(max(0, min(100, volume)))
geometry = self.settings.value("window_geometry")
if geometry is not None:
self.restoreGeometry(geometry)
width = max(200, min(800, self.settings.value("chat_width", 320, type=int)))
self.room_widget._saved_chat_width = width
self.room_widget.splitter.setSizes([max(100, self.width() - width), width])
self.room_widget.chat_container.setVisible(self.settings.value("chat_visible", True, type=bool))
if not self.settings.value("highlights_visible", True, type=bool):
self.room_widget.tags_list.hide()
self.room_widget.tags_header_btn.setText("▶ Highlights")
def _remember_media_folder(self, path):
folder = os.path.dirname(path)
self.lobby_widget.media_folder = folder
self.settings.setValue("media_folder", folder)
def closeEvent(self, event):
room = self.room_widget
self.settings.setValue("window_geometry", self.saveGeometry())
width = (room.chat_container.width() if not room.popout_window and
not room.chat_container.isHidden() else getattr(room, "_saved_chat_width", 320))
self.settings.setValue("chat_width", width)
self.settings.setValue("chat_visible", not room.chat_container.isHidden())
self.settings.setValue("highlights_visible", not room.tags_list.isHidden())
self.settings.sync()
if room.popout_window:
room.popout_window.close()
self.sync_client.stop()
room.cleanup()
super().closeEvent(event)
def _on_lobby_create(self, username, file_path, file_name, file_size):
self.username = username
@@ -164,10 +208,11 @@ class VlcSyncApp(QMainWindow):
ext = os.path.splitext(req_name)[1].lower()
file_filter = f"Video Files (*{ext});;All Files (*)" if ext else "All Files (*)"
file_path, _ = QFileDialog.getOpenFileName(
self, f"Select {req_name}", "", file_filter
self, f"Select {req_name}", self.lobby_widget.media_folder, file_filter
)
if file_path:
self._remember_media_folder(file_path)
self.local_file_path = file_path
self.local_file_name = os.path.basename(file_path)
self.local_file_size = os.path.getsize(file_path)
+46
View File
@@ -0,0 +1,46 @@
from unittest.mock import Mock
from PyQt6.QtCore import QSettings
import room_widget
from main import VlcSyncApp
from test_chat import PlayerSignals
def test_preferences_survive_restart(qtbot, monkeypatch, tmp_path):
monkeypatch.setattr(room_widget, "VLCSyncPlayer",
lambda frame: Mock(signals=PlayerSignals()))
path = str(tmp_path / "preferences.ini")
settings = QSettings(path, QSettings.Format.IniFormat)
first = VlcSyncApp(settings)
qtbot.addWidget(first)
first.show()
first.lobby_widget.username_input.setText("Alex")
first.room_widget.volume_slider.setValue(37)
first._remember_media_folder(str(tmp_path / "movie.mkv"))
first.room_widget.chat_container.hide()
first.room_widget.tags_list.hide()
first.resize(900, 650)
first.close()
second = VlcSyncApp(QSettings(path, QSettings.Format.IniFormat))
qtbot.addWidget(second)
assert second.lobby_widget.username_input.text() == "Alex"
assert second.room_widget.volume_slider.value() == 37
assert second.lobby_widget.media_folder == str(tmp_path)
assert second.room_widget.chat_container.isHidden()
assert second.room_widget.tags_list.isHidden()
assert second.size() == first.size()
assert second.sync_client.url == "wss://video-sync.peterstockings.com/ws"
def test_missing_folder_and_volume_bounds(qtbot, monkeypatch, tmp_path):
monkeypatch.setattr(room_widget, "VLCSyncPlayer",
lambda frame: Mock(signals=PlayerSignals()))
settings = QSettings(str(tmp_path / "preferences.ini"), QSettings.Format.IniFormat)
settings.setValue("media_folder", str(tmp_path / "missing"))
settings.setValue("volume", 200)
window = VlcSyncApp(settings)
qtbot.addWidget(window)
assert window.lobby_widget.media_folder == ""
assert window.room_widget.volume_slider.value() == 100