import sys import os import ctypes from PyQt6.QtWidgets import ( QApplication, QMainWindow, QStackedWidget, QFileDialog, QMessageBox ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QIcon from sync_client import SyncClientThread from lobby_widget import LobbyWidget from room_widget import RoomWidget class VlcSyncApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("VideoSync — Watch Together") icon_path = os.path.join(os.path.dirname(__file__), "icon.png") if os.path.exists(icon_path): self.setWindowIcon(QIcon(icon_path)) self.setMinimumSize(900, 600) self.resize(1100, 700) # Main stacked widget self.stacked_widget = QStackedWidget() self.setCentralWidget(self.stacked_widget) # Setup Views self.lobby_widget = LobbyWidget() self.room_widget = RoomWidget() self.stacked_widget.addWidget(self.lobby_widget) self.stacked_widget.addWidget(self.room_widget) # Connect View Signals self.lobby_widget.create_requested.connect(self._on_lobby_create) self.lobby_widget.join_requested.connect(self._on_lobby_join) self.room_widget.leave_requested.connect(self._on_room_leave) self.room_widget.sync_action_requested.connect(self._on_room_sync_action) self.room_widget.chat_message_ready.connect(self._on_room_chat) # App State self.username = "" self.room_code = "" self.local_file_path = None self.local_file_name = None self.local_file_size = 0 self.pending_connect_action = None # Network Service self.sync_client = SyncClientThread("wss://video-sync.peterstockings.com/ws") self.sync_client.connected.connect(self.on_ws_connected) self.sync_client.connected.connect(lambda: self.room_widget.update_connection_status(True)) self.sync_client.disconnected.connect(self.on_ws_disconnected) self.sync_client.disconnected.connect(lambda: self.room_widget.update_connection_status(False)) self.sync_client.room_joined.connect(self.on_room_joined) self.sync_client.room_rejoined.connect(self.on_room_rejoined) self.sync_client.room_error.connect(self.on_room_error) self.sync_client.file_check_needed.connect(self.on_file_check_needed) self.sync_client.users_updated.connect(self.room_widget.update_users) self.sync_client.chat_message.connect(self.room_widget.add_chat_message) self.sync_client.system_message.connect(self.room_widget.add_system_message) self.sync_client.sync_event.connect(self.room_widget.handle_sync_event) self.sync_client.latency_updated.connect(self.room_widget.update_latency) self.apply_stylesheet() def _on_lobby_create(self, username, file_path, file_name, file_size): self.username = username self.local_file_path = file_path self.local_file_name = file_name self.local_file_size = file_size self.pending_connect_action = lambda: self.sync_client.send_message({ "type": "create_room", "username": self.username, "fileInfo": { "name": self.local_file_name, "size": self.local_file_size, "duration": 0 } }) self._ensure_connection() def _on_lobby_join(self, username, room_code): self.username = username self.room_code = room_code self.pending_connect_action = lambda: self.sync_client.send_message({ "type": "join_room", "username": self.username, "code": self.room_code }) self._ensure_connection() def _ensure_connection(self): if self.sync_client.running and self.sync_client.ws: self.on_ws_connected() else: self.sync_client.start() def _on_room_leave(self): self.room_widget.cleanup() self.sync_client.stop() self.stacked_widget.setCurrentIndex(0) self.room_code = "" self.local_file_path = None self.lobby_widget.clear_file() self.lobby_widget.reset_ui() def _on_room_sync_action(self, action, position_s, req_id): self.sync_client.send_message({"type": "sync", "action": action, "position": position_s, "req_id": req_id}) def _on_room_chat(self, text): self.sync_client.send_message({"type": "chat", "message": text}) # --- WebSocket Callbacks --- def on_ws_connected(self): if self.pending_connect_action: self.pending_connect_action() self.pending_connect_action = None elif self.stacked_widget.currentIndex() == 1 and self.room_code and self.username: self.sync_client.send_message({ "type": "rejoin_room", "username": self.username, "code": self.room_code }) def on_ws_disconnected(self): if self.stacked_widget.currentIndex() == 1: self.room_widget.set_room_code_display(f"{self.room_code} (Reconnecting...)") self.room_widget.add_system_message("⚠️ Connection lost. Trying to reconnect...") else: self.lobby_widget.reset_ui() self.pending_connect_action = None def on_room_error(self, msg: str): QMessageBox.critical(self, "Room Error", msg) self.lobby_widget.reset_ui() self.sync_client.stop() def on_file_check_needed(self, msg: dict): QTimer.singleShot(0, lambda: self._handle_file_check(msg)) def _handle_file_check(self, msg: dict): req_name = msg["fileInfo"].get("name", "Unknown") req_size = msg["fileInfo"].get("size", 0) QMessageBox.information(self, "File Required", f"To join this room, you need to select:\n\nName: {req_name}\nSize: {req_size / (1024*1024):.1f} MB") file_path, _ = QFileDialog.getOpenFileName( self, f"Select {req_name}", "", f"Required File ({req_name})" ) if 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) if self.local_file_name.lower() == req_name.lower() and int(self.local_file_size) == int(req_size): self.sync_client.send_message({ "type": "confirm_join", "fileInfo": { "name": self.local_file_name, "size": self.local_file_size, "duration": 0 } }) else: err_msg = f"Expected: {req_name} ({req_size} bytes)\nGot: {self.local_file_name} ({self.local_file_size} bytes)" QMessageBox.critical(self, "File Mismatch", f"The selected file does not exactly match the room's required file.\n\n{err_msg}") self.sync_client.stop() self.lobby_widget.reset_ui() else: self.sync_client.stop() self.lobby_widget.reset_ui() def on_room_joined(self, msg: dict): if "room" in msg: self.room_code = msg["room"]["code"] else: self.room_code = msg.get("code", "") self.stacked_widget.setCurrentIndex(1) self.lobby_widget.reset_ui() state = msg.get("state", {}) start_time_s = state.get("position", 0.0) if state else 0.0 self.room_widget.setup_room(self.room_code, self.username, self.local_file_name, self.local_file_path, start_time_s) chat_history = msg.get("chatHistory", []) if chat_history: for chat in chat_history: self.room_widget.add_chat_message(chat.get("username", "Unknown"), chat.get("message", ""), chat.get("timestamp", 0)) users = msg.get("users", []) if users: self.room_widget.update_users(users) if state: self.room_widget.handle_sync_event(state) self.room_widget.add_system_message("Welcome to the room! 👋") def on_room_rejoined(self, msg: dict): self.room_widget.set_room_code_display(self.room_code) chat_history = msg.get("chatHistory", []) if chat_history: self.room_widget.clear_chat() for chat in chat_history: self.room_widget.add_chat_message(chat.get("username", "Unknown"), chat.get("message", ""), chat.get("timestamp", 0)) self.room_widget.add_system_message("✅ Reconnected to the room.") users = msg.get("users", []) if users: self.room_widget.update_users(users) state = msg.get("state", {}) if state: self.room_widget.handle_sync_event(state) def apply_stylesheet(self): self.setStyleSheet(""" QWidget { background-color: #0f0f0f; color: #f1f1f1; font-family: 'Segoe UI', 'Roboto', sans-serif; font-size: 14px; } #lobbyCard { background-color: #1a1a1a; border: 1px solid #333; border-radius: 12px; } #brandTitle { font-size: 32px; font-weight: bold; color: #3ea6ff; } #brandTagline { font-size: 14px; color: #aaaaaa; } QLabel { color: #aaaaaa; font-weight: bold; font-size: 12px; background-color: transparent; } #fileInfo { background-color: rgba(62, 166, 255, 0.1); color: #3ea6ff; padding: 10px; border-radius: 6px; font-weight: normal; font-size: 13px; } QLineEdit { background-color: #272727; border: 1px solid #333; border-radius: 6px; padding: 10px; color: white; font-size: 14px; } QLineEdit:focus { border: 1px solid #3ea6ff; } QPushButton { border-radius: 6px; padding: 10px 16px; font-weight: bold; font-size: 14px; background-color: #272727; color: white; border: 1px solid #444; } QPushButton:hover { background-color: #333; } QPushButton:disabled { border-color: #222; color: #666; } #primaryBtn { background-color: #3ea6ff; color: black; border: none; } #primaryBtn:hover { background-color: #65b8ff; } #primaryBtn:disabled { background-color: #333; color: #666; } #iconBtn { background-color: transparent; border: none; padding: 5px; } #iconBtn:hover { background-color: rgba(255, 255, 255, 0.1); } #dangerBtn { background-color: transparent; color: #ff4e45; border: 1px solid #ff4e45; padding: 6px 12px; font-size: 12px; } #dangerBtn:hover { background-color: #ff4e45; color: white; } #divider { color: #555; } #topbar { background-color: #1a1a1a; border-bottom: 1px solid #333; } #fileBadge { background-color: #272727; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: normal; } #controlsBar { background-color: #1a1a1a; } #playBtn { background: transparent; color: white; font-size: 20px; border: none; } #playBtn:hover { background: #333; } #seekBar { border: none; background: transparent; } #seekBar::groove:horizontal { height: 4px; background: #444; border: none; border-radius: 2px; } #seekBar::sub-page:horizontal { background: #ff0000; height: 4px; border-radius: 2px; } #seekBar::add-page:horizontal { background: #444; height: 4px; border-radius: 2px; } #seekBar::handle:horizontal { background: #ff0000; width: 12px; height: 12px; margin: -4px 0; border-radius: 6px; } #seekBar::handle:horizontal:hover { background: #ff3333; width: 14px; height: 14px; margin: -5px 0; border-radius: 7px; } #volumeSlider { border: none; background: transparent; } #volumeSlider::groove:horizontal { height: 4px; background: #444; border: none; border-radius: 2px; } #volumeSlider::sub-page:horizontal { background: #fff; height: 4px; border-radius: 2px; } #volumeSlider::add-page:horizontal { background: #444; height: 4px; border-radius: 2px; } #volumeSlider::handle:horizontal { background: #fff; width: 10px; height: 10px; margin: -3px 0; border-radius: 5px; } #volumeSlider::handle:horizontal:hover { width: 12px; height: 12px; margin: -4px 0; border-radius: 6px; } #chatContainer { background-color: #111; border-left: 1px solid #333; } #chatHeader { font-size: 16px; color: white; padding: 10px 0; } #usersLbl { color: #3ea6ff; font-size: 12px; } #chatMessages { background-color: transparent; border: none; } #chatMessages QScrollBar:vertical { border: none; background: transparent; width: 8px; margin: 0px; } #chatMessages QScrollBar::handle:vertical { background: #333; min-height: 20px; border-radius: 4px; } #chatMessages QScrollBar::handle:vertical:hover { background: #444; } #chatMessages QScrollBar::add-line:vertical, #chatMessages QScrollBar::sub-line:vertical { height: 0px; } #chatMessages QScrollBar::add-page:vertical, #chatMessages QScrollBar::sub-page:vertical { background: none; } #bubble { border: none; } """) if __name__ == "__main__": import ctypes if os.name == 'nt': myappid = 'vlcsync.desktopclient.app.1' try: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) except Exception: pass app = QApplication(sys.argv) icon_path = os.path.join(os.path.dirname(__file__), "icon.png") if os.path.exists(icon_path): app.setWindowIcon(QIcon(icon_path)) window = VlcSyncApp() window.show() sys.exit(app.exec())