Persist desktop client preferences
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# VideoSync desktop client
|
# 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
|
||||||
|
|
||||||
Chat follows new messages while you are near the bottom. Scroll up to read older
|
Chat follows new messages while you are near the bottom. Scroll up to read older
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class RoomCodeInput(QLineEdit):
|
|||||||
class LobbyWidget(QWidget):
|
class LobbyWidget(QWidget):
|
||||||
# Signals to communicate to VlcSyncApp
|
# Signals to communicate to VlcSyncApp
|
||||||
create_requested = pyqtSignal(str, str, str, object) # username, path, filename, size
|
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):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -31,7 +32,8 @@ class LobbyWidget(QWidget):
|
|||||||
|
|
||||||
self.local_file_path = None
|
self.local_file_path = None
|
||||||
self.local_file_name = None
|
self.local_file_name = None
|
||||||
self.local_file_size = 0
|
self.local_file_size = 0
|
||||||
|
self.media_folder = ""
|
||||||
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
|
||||||
@@ -125,7 +127,9 @@ class LobbyWidget(QWidget):
|
|||||||
def _set_local_file(self, file_path: str):
|
def _set_local_file(self, file_path: str):
|
||||||
self.local_file_path = file_path
|
self.local_file_path = file_path
|
||||||
self.local_file_name = os.path.basename(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)
|
size_mb = self.local_file_size / (1024 * 1024)
|
||||||
self.create_file_info.setText(f"{self.local_file_name}\n{size_mb:.1f} MB")
|
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):
|
def select_file(self):
|
||||||
file_path, _ = QFileDialog.getOpenFileName(
|
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:
|
if file_path:
|
||||||
self._set_local_file(file_path)
|
self._set_local_file(file_path)
|
||||||
|
|||||||
+48
-3
@@ -4,7 +4,7 @@ import ctypes
|
|||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QStackedWidget, QFileDialog, QMessageBox
|
QApplication, QMainWindow, QStackedWidget, QFileDialog, QMessageBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer, QSettings
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
|
|
||||||
from sync_client import SyncClientThread
|
from sync_client import SyncClientThread
|
||||||
@@ -12,8 +12,9 @@ from lobby_widget import LobbyWidget
|
|||||||
from room_widget import RoomWidget
|
from room_widget import RoomWidget
|
||||||
|
|
||||||
class VlcSyncApp(QMainWindow):
|
class VlcSyncApp(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self, settings=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.settings = settings if settings is not None else QSettings("VideoSync", "DesktopClient")
|
||||||
self.setWindowTitle("VideoSync — Watch Together")
|
self.setWindowTitle("VideoSync — Watch Together")
|
||||||
|
|
||||||
icon_path = os.path.join(os.path.dirname(__file__), "icon.png")
|
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.sync_client.latency_updated.connect(self.room_widget.update_latency)
|
||||||
|
|
||||||
self.apply_stylesheet()
|
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):
|
def _on_lobby_create(self, username, file_path, file_name, file_size):
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -164,10 +208,11 @@ class VlcSyncApp(QMainWindow):
|
|||||||
ext = os.path.splitext(req_name)[1].lower()
|
ext = os.path.splitext(req_name)[1].lower()
|
||||||
file_filter = f"Video Files (*{ext});;All Files (*)" if ext else "All Files (*)"
|
file_filter = f"Video Files (*{ext});;All Files (*)" if ext else "All Files (*)"
|
||||||
file_path, _ = QFileDialog.getOpenFileName(
|
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:
|
if file_path:
|
||||||
|
self._remember_media_folder(file_path)
|
||||||
self.local_file_path = file_path
|
self.local_file_path = file_path
|
||||||
self.local_file_name = os.path.basename(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)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user