From 836034533e8084902030fc6dc36e621558b9b394 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Mon, 5 Oct 2026 16:06:59 +1100 Subject: [PATCH] Persist desktop client preferences --- desktop-client/README.md | 8 +++++ desktop-client/lobby_widget.py | 12 ++++--- desktop-client/main.py | 51 ++++++++++++++++++++++++++++-- desktop-client/test_preferences.py | 46 +++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 desktop-client/test_preferences.py diff --git a/desktop-client/README.md b/desktop-client/README.md index d5c2462..c5abe00 100644 --- a/desktop-client/README.md +++ b/desktop-client/README.md @@ -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 diff --git a/desktop-client/lobby_widget.py b/desktop-client/lobby_widget.py index d5ef9ca..9c04ca9 100644 --- a/desktop-client/lobby_widget.py +++ b/desktop-client/lobby_widget.py @@ -23,7 +23,8 @@ class RoomCodeInput(QLineEdit): 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 + join_requested = pyqtSignal(str, str) # username, room_code + file_selected = pyqtSignal(str) def __init__(self): super().__init__() @@ -31,7 +32,8 @@ class LobbyWidget(QWidget): self.local_file_path = None self.local_file_name = None - self.local_file_size = 0 + self.local_file_size = 0 + self.media_folder = "" self._setup_ui() @@ -125,7 +127,9 @@ class LobbyWidget(QWidget): def _set_local_file(self, file_path: str): 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.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) diff --git a/desktop-client/main.py b/desktop-client/main.py index ed95f0a..b0409dd 100644 --- a/desktop-client/main.py +++ b/desktop-client/main.py @@ -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) diff --git a/desktop-client/test_preferences.py b/desktop-client/test_preferences.py new file mode 100644 index 0000000..a3889dc --- /dev/null +++ b/desktop-client/test_preferences.py @@ -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