import sys import os import ctypes from PyQt6.QtWidgets import ( QApplication, QMainWindow, QStackedWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QMessageBox, QFrame, QSlider, QTextEdit ) from PyQt6.QtCore import Qt, pyqtSignal, QObject, QTimer from PyQt6.QtGui import QFont, QIcon, QColor from vlc_player import VLCSyncPlayer from sync_client import SyncClientThread import datetime class VlcSyncApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("VideoSync — Watch Together") # Set window icon 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 to switch between Lobby and Room self.stacked_widget = QStackedWidget() self.setCentralWidget(self.stacked_widget) # Setup Views self.lobby_view = self.create_lobby_view() self.room_view = self.create_room_view() self.stacked_widget.addWidget(self.lobby_view) self.stacked_widget.addWidget(self.room_view) # 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 self.ignore_vlc_events = False self.last_reported_time_ms = 0 self.sync_client = SyncClientThread("wss://video-sync.peterstockings.com/ws") self.sync_client.connected.connect(self.on_ws_connected) self.sync_client.disconnected.connect(self.on_ws_disconnected) 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.on_users_updated) self.sync_client.chat_message.connect(self.on_chat_message) self.sync_client.system_message.connect(self.on_system_message) self.sync_client.sync_event.connect(self.on_sync_event) self.apply_stylesheet() def create_lobby_view(self): widget = QWidget() layout = QVBoxLayout(widget) layout.setAlignment(Qt.AlignmentFlag.AlignCenter) # Container for centering container = QFrame() container.setObjectName("lobbyCard") container.setFixedWidth(500) container_layout = QVBoxLayout(container) container_layout.setContentsMargins(30, 30, 30, 30) container_layout.setSpacing(15) # Brand title = QLabel("VideoSync") title.setObjectName("brandTitle") title.setAlignment(Qt.AlignmentFlag.AlignCenter) tagline = QLabel("Watch together, anywhere") tagline.setObjectName("brandTagline") tagline.setAlignment(Qt.AlignmentFlag.AlignCenter) container_layout.addWidget(title) container_layout.addWidget(tagline) container_layout.addSpacing(20) # Username self.username_input = QLineEdit() self.username_input.setPlaceholderText("Enter a display name") container_layout.addWidget(QLabel("YOUR NAME")) container_layout.addWidget(self.username_input) container_layout.addSpacing(20) # Actions Layout (Create vs Join) actions_layout = QHBoxLayout() actions_layout.setSpacing(20) # Create Room Panel create_panel = QVBoxLayout() create_panel.addWidget(QLabel("Create a Room")) self.create_file_btn = QPushButton("Choose Video File") self.create_file_btn.setObjectName("secondaryBtn") self.create_file_btn.clicked.connect(self.select_file) self.create_file_info = QLabel("") self.create_file_info.setObjectName("fileInfo") self.create_file_info.setWordWrap(True) self.create_file_info.hide() self.create_room_btn = QPushButton("Create Room") self.create_room_btn.setObjectName("primaryBtn") self.create_room_btn.setEnabled(False) self.create_room_btn.clicked.connect(self.create_room) create_panel.addWidget(self.create_file_btn) create_panel.addWidget(self.create_file_info) create_panel.addWidget(self.create_room_btn) # Join Room Panel join_panel = QVBoxLayout() join_panel.addWidget(QLabel("Join a Room")) self.room_code_input = QLineEdit() self.room_code_input.setPlaceholderText("e.g. ABC123") self.join_room_btn = QPushButton("Join Room") self.join_room_btn.setObjectName("secondaryBtn") self.join_room_btn.setEnabled(False) self.join_room_btn.clicked.connect(self.join_room) join_panel.addWidget(self.room_code_input) join_panel.addWidget(self.join_room_btn) actions_layout.addLayout(create_panel) # Divider divider = QLabel("OR") divider.setAlignment(Qt.AlignmentFlag.AlignCenter) divider.setObjectName("divider") actions_layout.addWidget(divider) actions_layout.addLayout(join_panel) container_layout.addLayout(actions_layout) layout.addWidget(container) # Signals to enable/disable buttons self.username_input.textChanged.connect(self.check_inputs) self.room_code_input.textChanged.connect(self.check_inputs) return widget def create_room_view(self): widget = QWidget() layout = QHBoxLayout(widget) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # --- Left Side: Video --- video_container = QWidget() video_layout = QVBoxLayout(video_container) video_layout.setContentsMargins(0, 0, 0, 0) video_layout.setSpacing(0) # Topbar self.topbar = QFrame() self.topbar.setObjectName("topbar") self.topbar.setFixedHeight(50) topbar_layout = QHBoxLayout(self.topbar) self.room_code_display = QLabel("Room: XXXX") self.copy_code_btn = QPushButton() self.copy_code_btn.setObjectName("iconBtn") self.copy_icon = QIcon(os.path.join(os.path.dirname(__file__), "copy.svg")) self.check_icon = QIcon(os.path.join(os.path.dirname(__file__), "check.svg")) self.copy_code_btn.setIcon(self.copy_icon) self.copy_code_btn.setFixedSize(30, 30) self.copy_code_btn.setToolTip("Copy Room Code") self.copy_code_btn.clicked.connect(self.copy_room_code) self.room_file_badge = QLabel("📄 No file") self.room_file_badge.setObjectName("fileBadge") self.leave_btn = QPushButton("Leave Room") self.leave_btn.setObjectName("dangerBtn") self.leave_btn.clicked.connect(self.leave_room) topbar_layout.addWidget(self.room_code_display) topbar_layout.addWidget(self.copy_code_btn) topbar_layout.addStretch() topbar_layout.addWidget(self.room_file_badge) topbar_layout.addWidget(self.leave_btn) # Video Frame Placeholder self.video_frame = QFrame() self.video_frame.setStyleSheet("background-color: black;") # Fix for Windows QWidgetWindow Error: # Force the frame to have its own native HWND so VLC can attach to it without complaining it's not a top-level window. self.video_frame.setAttribute(Qt.WidgetAttribute.WA_NativeWindow, True) # Controls Bar controls = QFrame() controls.setObjectName("controlsBar") controls.setFixedHeight(60) controls_layout = QHBoxLayout(controls) self.play_btn = QPushButton("▶") self.play_btn.setFixedSize(40, 40) self.play_btn.setObjectName("playBtn") # Time and SeekBar self.seekbar = QSlider(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.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.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) # Fullscreen self.fullscreen_btn = QPushButton("⛶") self.fullscreen_btn.setFixedSize(40, 40) self.fullscreen_btn.setObjectName("iconBtn") self.fullscreen_btn.clicked.connect(self.toggle_fullscreen) controls_layout.addWidget(self.play_btn) controls_layout.addWidget(self.seekbar, 1) # SeekBar gets the stretch controls_layout.addWidget(self.time_lbl) controls_layout.addSpacing(15) controls_layout.addWidget(self.vol_icon) controls_layout.addWidget(self.volume_slider) controls_layout.addSpacing(10) controls_layout.addWidget(self.fullscreen_btn) video_layout.addWidget(self.topbar) video_layout.addWidget(self.video_frame) video_layout.addWidget(controls) # --- Right Side: Chat --- self.chat_container = QFrame() self.chat_container.setObjectName("chatContainer") self.chat_container.setFixedWidth(320) chat_layout = QVBoxLayout(self.chat_container) chat_header = QLabel("Live Chat") chat_header.setObjectName("chatHeader") self.users_lbl = QLabel("0 watching") self.users_lbl.setObjectName("usersLbl") self.chat_messages = QTextEdit() self.chat_messages.setObjectName("chatMessages") self.chat_messages.setReadOnly(True) self.chat_messages.setHtml("Welcome to the room! 👋") chat_input_layout = QHBoxLayout() self.chat_input = QLineEdit() self.chat_input.setPlaceholderText("Send a message...") self.chat_send_btn = QPushButton("Send") chat_input_layout.addWidget(self.chat_input) chat_input_layout.addWidget(self.chat_send_btn) # Chat actions self.chat_send_btn.clicked.connect(self.send_chat) self.chat_input.returnPressed.connect(self.send_chat) chat_layout.addWidget(chat_header) chat_layout.addWidget(self.users_lbl) chat_layout.addWidget(self.chat_messages, 1) # stretch chat_layout.addLayout(chat_input_layout) layout.addWidget(video_container, 1) # stretch layout.addWidget(self.chat_container) # Instantiate the VLC Player Wrapper self.vlc_player = VLCSyncPlayer(self.video_frame) self.vlc_player.signals.time_changed.connect(self.on_vlc_time) self.vlc_player.signals.state_changed.connect(self.on_vlc_state) self.vlc_player.set_volume(self.volume_slider.value()) self.play_btn.clicked.connect(self.toggle_playback) return widget def toggle_fullscreen(self): if self.isFullScreen(): self.showNormal() self.fullscreen_btn.setText("⛶") self.chat_container.show() self.topbar.show() else: self.showFullScreen() self.fullscreen_btn.setText("🗗") self.chat_container.hide() self.topbar.hide() def toggle_playback(self): position_s = self.vlc_player.current_time_ms / 1000.0 if self.vlc_player.is_playing: self.vlc_player.pause() self.play_btn.setText("▶") self.sync_client.send_message({"type": "sync", "action": "pause", "position": position_s}) else: self.vlc_player.play() self.play_btn.setText("⏸") self.sync_client.send_message({"type": "sync", "action": "play", "position": position_s}) def on_vlc_time(self, time_ms: int): length_ms = self.vlc_player.get_length() if length_ms > 0: def fmt(ms): s = max(0, ms) // 1000 return f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}" if not self.seekbar.isSliderDown(): self.time_lbl.setText(f"{fmt(time_ms)} / {fmt(length_ms)}") progress = int((time_ms / length_ms) * 1000) self.seekbar.blockSignals(True) self.seekbar.setValue(progress) self.seekbar.blockSignals(False) if self.last_reported_time_ms is not None: diff = abs(time_ms - self.last_reported_time_ms) if diff > 2500 and not self.ignore_vlc_events: self.sync_client.send_message({"type": "sync", "action": "seek", "position": time_ms / 1000.0}) self.last_reported_time_ms = time_ms def on_vlc_state(self, playing: bool, time_ms: int): if self.ignore_vlc_events: return action = "play" if playing else "pause" self.sync_client.send_message({"type": "sync", "action": action, "position": time_ms / 1000.0}) def on_seekbar_dragged(self, value): length_ms = self.vlc_player.get_length() if length_ms > 0: target_ms = int((value / 1000.0) * length_ms) def fmt(ms): 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)}") def on_seekbar_released(self): length_ms = self.vlc_player.get_length() if length_ms > 0: target_ms = int((self.seekbar.value() / 1000.0) * length_ms) self.vlc_player.seek(target_ms) self.sync_client.send_message({"type": "sync", "action": "seek", "position": target_ms / 1000.0}) def on_volume_changed(self, value): self.vlc_player.set_volume(value) def select_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "Select Video File", "", "Video Files (*.mp4 *.mkv *.avi *.mov *.webm);;All Files (*)" ) 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) 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.show() self.check_inputs() def copy_room_code(self): if self.room_code: QApplication.clipboard().setText(self.room_code) self.copy_code_btn.setIcon(self.check_icon) # Show floating tooltip for feedback toast = QLabel("Copied!", self) toast.setStyleSheet("background-color: #4BB543; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;") toast.setWindowFlags(Qt.WindowType.ToolTip | Qt.WindowType.FramelessWindowHint) # Position the toast slightly below the button pos = self.copy_code_btn.mapToGlobal(self.copy_code_btn.rect().bottomLeft()) toast.move(pos.x(), pos.y() + 5) toast.show() def reset(): self.copy_code_btn.setIcon(self.copy_icon) toast.deleteLater() QTimer.singleShot(1500, reset) def check_inputs(self): has_name = len(self.username_input.text().strip()) > 0 has_file = self.local_file_path is not None has_code = len(self.room_code_input.text().strip()) >= 4 self.create_room_btn.setEnabled(has_name and has_file) self.join_room_btn.setEnabled(has_name and has_code) def create_room(self): self.username = self.username_input.text().strip() self.create_room_btn.setText("Connecting...") self.create_room_btn.setEnabled(False) self.join_room_btn.setEnabled(False) 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 } }) if self.sync_client.running and self.sync_client.ws: self.on_ws_connected() else: self.sync_client.start() def join_room(self): self.username = self.username_input.text().strip() self.room_code = self.room_code_input.text().strip().upper() self.join_room_btn.setText("Connecting...") self.join_room_btn.setEnabled(False) self.create_room_btn.setEnabled(False) self.pending_connect_action = lambda: self.sync_client.send_message({ "type": "join_room", "username": self.username, "code": self.room_code }) if self.sync_client.running and self.sync_client.ws: self.on_ws_connected() else: self.sync_client.start() def leave_room(self): self.vlc_player.stop() self.sync_client.stop() self.stacked_widget.setCurrentIndex(0) self.local_file_path = None self.create_file_info.hide() self.room_code = "" self.check_inputs() self.create_room_btn.setText("Create Room") self.join_room_btn.setText("Join Room") # --- 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: # We are already in a room and just reconnected 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_code_display.setText(f"Room: {self.room_code} (Reconnecting...)") self.on_system_message("⚠️ Connection lost. Trying to reconnect...") def on_room_error(self, msg: str): QMessageBox.critical(self, "Room Error", msg) self.create_room_btn.setText("Create Room") self.join_room_btn.setText("Join Room") self.check_inputs() self.sync_client.stop() def on_file_check_needed(self, msg: dict): # Defer execution to the PyQt Main Thread to avoid deadlocking the WebSocket thread 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 == req_name and self.local_file_size == req_size: self.sync_client.send_message({ "type": "confirm_join", "fileInfo": { "name": self.local_file_name, "size": self.local_file_size, "duration": 0 } }) else: QMessageBox.critical(self, "File Mismatch", "The selected file does not exactly match the room's required file.") self.sync_client.stop() self.join_room_btn.setText("Join Room") self.check_inputs() else: self.sync_client.stop() self.join_room_btn.setText("Join Room") self.check_inputs() 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.room_code_display.setText(f"Room: {self.room_code}") self.room_file_badge.setText(f"📄 {self.local_file_name}") self.create_room_btn.setText("Create Room") self.join_room_btn.setText("Join Room") self.chat_messages.setHtml("Welcome to the room! 👋") state = msg.get("state", {}) start_time_s = state.get("position", 0.0) if state else 0.0 if self.local_file_path: self.vlc_player.load_media(self.local_file_path, start_time_s) self.vlc_player.set_volume(self.volume_slider.value()) users = msg.get("users", []) if users: self.on_users_updated(users) if state: self.on_sync_event(state) chat_history = msg.get("chatHistory", []) if chat_history: self.chat_messages.setHtml("Welcome to the room! 👋") for chat in chat_history: self.on_chat_message(chat.get("username", "Unknown"), chat.get("message", ""), chat.get("timestamp", 0)) def on_room_rejoined(self, msg: dict): self.room_code_display.setText(f"Room: {self.room_code}") self.on_system_message("✅ Reconnected to the room.") users = msg.get("users", []) if users: self.on_users_updated(users) state = msg.get("state", {}) if state: # Don't reload media, just resync playback state self.on_sync_event(state) def on_chat_message(self, author: str, text: str, timestamp: int): try: dt = datetime.datetime.fromtimestamp(timestamp / 1000.0) except (ValueError, OSError, TypeError): dt = datetime.datetime.now() time_str = dt.strftime("%I:%M %p") new_msg = f"{author}: {text} {time_str}" self.chat_messages.append(new_msg) def on_system_message(self, text: str): new_msg = f"{text}" self.chat_messages.append(new_msg) def on_users_updated(self, users: list): self.users_lbl.setText(f"{len(users)} watching: {', '.join(users)}") def on_sync_event(self, msg: dict): self.ignore_vlc_events = True action = msg.get("action") # Handle full state sync vs event sync if not action: if msg.get("playing", False): action = "play" elif msg.get("playing") is False: action = "pause" position_s = msg.get("position", 0) position_ms = int(position_s * 1000) if action == "play": self.vlc_player.seek(position_ms) self.vlc_player.play() self.play_btn.setText("⏸") elif action == "pause": self.vlc_player.seek(position_ms) self.vlc_player.pause() self.play_btn.setText("▶") elif action == "seek": self.vlc_player.seek(position_ms) def clear_ignore(): self.ignore_vlc_events = False QTimer.singleShot(1500, clear_ignore) # System notification username = msg.get("username") if username and username != self.username: if action == "play": self.on_system_message(f"{username} pressed play") elif action == "pause": self.on_system_message(f"{username} paused") elif action == "seek": def fmt(s): return f"{int(s)//3600:02d}:{(int(s)%3600)//60:02d}:{int(s)%60:02d}" self.on_system_message(f"{username} seeked to {fmt(position_s)}") def _handle_seek_command(self, arg: str) -> bool: current_ms = self.vlc_player.current_time_ms length_ms = self.vlc_player.get_length() if length_ms <= 0: return False try: target_ms = 0 if arg.startswith('+') or arg.startswith('-'): # relative modifier = 1 if arg.startswith('+') else -1 num_str = arg[1:] if num_str.endswith('s'): val = float(num_str[:-1]) * 1000 elif num_str.endswith('m'): val = float(num_str[:-1]) * 60 * 1000 elif num_str.endswith('h'): val = float(num_str[:-1]) * 3600 * 1000 else: val = float(num_str) * 1000 # Default to seconds target_ms = current_ms + (val * modifier) elif ":" in arg: # absolute time like HH:MM:SS or MM:SS parts = arg.split(":") parts.reverse() # seconds, minutes, hours if len(parts) > 0: target_ms += float(parts[0]) * 1000 if len(parts) > 1: target_ms += float(parts[1]) * 60 * 1000 if len(parts) > 2: target_ms += float(parts[2]) * 3600 * 1000 else: # absolute seconds or something with a suffix but no + or - if arg.endswith('s'): target_ms = float(arg[:-1]) * 1000 elif arg.endswith('m'): target_ms = float(arg[:-1]) * 60 * 1000 elif arg.endswith('h'): target_ms = float(arg[:-1]) * 3600 * 1000 else: target_ms = float(arg) * 1000 target_ms = max(0, min(target_ms, length_ms)) self.vlc_player.seek(int(target_ms)) self.sync_client.send_message({"type": "sync", "action": "seek", "position": target_ms / 1000.0}) return True except ValueError: return False def send_chat(self): text = self.chat_input.text().strip() if not text: return if text.startswith("/"): parts = text.split() cmd = parts[0].lower() if cmd == "/play": self.chat_input.setText("") if not self.vlc_player.is_playing: self.toggle_playback() self.on_system_message(text) return elif cmd == "/pause": self.chat_input.setText("") if self.vlc_player.is_playing: self.toggle_playback() self.on_system_message(text) return elif cmd == "/seek": self.chat_input.setText("") if len(parts) > 1: if self._handle_seek_command(parts[1]): self.on_system_message(text) else: self.on_system_message("Invalid time format. Use: 1:23, +30s, -1m") else: self.on_system_message("Usage: /seek [time]") return elif cmd == "/help": self.chat_input.setText("") self.on_system_message("Available commands:
/play - Resume playback
/pause - Pause playback
/seek [time] - Seek to specific time (e.g., 1:23) or offset (e.g., +30s, -1m)
/help - Show this message") return self.sync_client.send_message({"type": "chat", "message": text}) self.chat_input.setText("") 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; } #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; color: #f1f1f1; } #chatMessages QScrollBar:vertical { border: none; background: transparent; width: 8px; margin: 0px; } #chatMessages QScrollBar::handle:vertical { background: #555; min-height: 20px; border-radius: 4px; } #chatMessages QScrollBar::handle:vertical:hover { background: #666; } #chatMessages QScrollBar::add-line:vertical, #chatMessages QScrollBar::sub-line:vertical { height: 0px; } #chatMessages QScrollBar::add-page:vertical, #chatMessages QScrollBar::sub-page:vertical { background: none; } """) if __name__ == "__main__": # Tell Windows this is a distinct app so the taskbar icon updates correctly if os.name == 'nt': myappid = 'vlcsync.desktopclient.app.1' try: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) except Exception: pass app = QApplication(sys.argv) # Set app-level icon 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())