from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, QSizePolicy, QSpacerItem, QSlider, QProgressBar) from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QTimer, pyqtProperty from PyQt6.QtGui import QFont, QPalette, QIcon, QPixmap, QPainter, QFontMetrics, QColor, QLinearGradient class ScrollingLabel(QLabel): """A label that smoothly scrolls text horizontally when it's too long to fit""" def __init__(self, text="", parent=None): super().__init__(parent) self.full_text = text self.scroll_offset = 0 self.text_width = 0 self.should_scroll = False self.is_scrolling = False self.scroll_speed = 30 # pixels per second # Animation timer self.scroll_timer = QTimer() self.scroll_timer.timeout.connect(self.update_scroll) # Pause timer for smooth start/stop self.pause_timer = QTimer() self.pause_timer.setSingleShot(True) self.pause_timer.timeout.connect(self.start_scroll_animation) # Set initial properties self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.update_text_metrics() def setText(self, text): """Override setText to handle scroll calculations""" self.full_text = text self.scroll_offset = 0 self.update_text_metrics() super().setText(text) def update_text_metrics(self): """Calculate if text needs scrolling and start animation if needed""" if not self.full_text: self.should_scroll = False self.stop_scrolling() return font_metrics = QFontMetrics(self.font()) self.text_width = font_metrics.horizontalAdvance(self.full_text) available_width = self.width() - 20 # Account for padding self.should_scroll = self.text_width > available_width and available_width > 0 if self.should_scroll and not self.is_scrolling: # Start scrolling after a pause self.pause_timer.start(1500) # 1.5 second pause before scrolling elif not self.should_scroll: self.stop_scrolling() def start_scroll_animation(self): """Start the continuous scrolling animation""" if self.should_scroll and not self.is_scrolling: self.is_scrolling = True self.scroll_timer.start(50) # Update every 50ms for smooth animation def stop_scrolling(self): """Stop scrolling and reset position""" self.scroll_timer.stop() self.pause_timer.stop() self.is_scrolling = False self.scroll_offset = 0 self.update() def update_scroll(self): """Update scroll position for animation""" if not self.should_scroll: self.stop_scrolling() return available_width = self.width() - 20 max_scroll = self.text_width - available_width + 30 # Extra padding at end # Move scroll position self.scroll_offset += 2 # 2 pixels per frame # Reset when we've scrolled past the end if self.scroll_offset > max_scroll: self.scroll_offset = -50 # Start from off-screen left self.update() def paintEvent(self, event): """Custom paint event to draw scrolling text""" if not self.should_scroll or not self.is_scrolling: # Use default painting for non-scrolling text super().paintEvent(event) return painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) # Set font and color from stylesheet painter.setFont(self.font()) # Get text color from current style painter.setPen(self.palette().color(QPalette.ColorRole.WindowText)) # Draw text at scroll offset position text_rect = self.rect() text_rect.adjust(10, 0, -10, 0) # Account for padding painter.drawText(text_rect.x() - self.scroll_offset, text_rect.y(), text_rect.width() + self.text_width, text_rect.height(), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self.full_text) def resizeEvent(self, event): """Handle resize to recalculate scrolling needs""" super().resizeEvent(event) self.update_text_metrics() def enterEvent(self, event): """Start scrolling on hover""" super().enterEvent(event) if self.should_scroll and not self.is_scrolling: self.start_scroll_animation() def leaveEvent(self, event): """Optionally stop scrolling when mouse leaves (can be customized)""" super().leaveEvent(event) # Note: We continue scrolling even after mouse leaves for better UX # You can uncomment the line below if you want it to stop on mouse leave # self.stop_scrolling() class SidebarButton(QPushButton): def __init__(self, text: str, icon_text: str = "", parent=None): super().__init__(parent) self.text = text self.icon_text = icon_text self.is_active = False self.setup_ui() def setup_ui(self): self.setFixedHeight(50) self.setFixedWidth(200) self.setCursor(Qt.CursorShape.PointingHandCursor) layout = QHBoxLayout(self) layout.setContentsMargins(20, 0, 20, 0) layout.setSpacing(15) # Icon label self.icon_label = QLabel(self.icon_text) self.icon_label.setFixedSize(24, 24) self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.icon_label.setStyleSheet(""" QLabel { color: #b3b3b3; font-size: 16px; font-weight: bold; border-radius: 12px; background: rgba(255, 255, 255, 0.1); } """) # Text label self.text_label = QLabel(self.text) self.text_label.setFont(QFont("Arial", 11, QFont.Weight.Medium)) layout.addWidget(self.icon_label) layout.addWidget(self.text_label) layout.addStretch() self.update_style() def set_active(self, active: bool): self.is_active = active self.update_style() def update_style(self): if self.is_active: self.setStyleSheet(""" SidebarButton { background: rgba(29, 185, 84, 0.15); border-left: 4px solid #1db954; border-radius: 12px; text-align: left; padding: 0px; } SidebarButton:hover { background: rgba(29, 185, 84, 0.25); transform: scale(1.02); } """) self.text_label.setStyleSheet("color: #1db954; font-weight: bold; background: transparent;") self.icon_label.setStyleSheet(""" QLabel { color: #1db954; font-size: 16px; font-weight: bold; border-radius: 14px; background: rgba(29, 185, 84, 0.25); } """) else: self.setStyleSheet(""" SidebarButton { background: transparent; border: none; border-radius: 12px; text-align: left; padding: 0px; } SidebarButton:hover { background: rgba(255, 255, 255, 0.08); border-left: 2px solid rgba(255, 255, 255, 0.3); } """) self.text_label.setStyleSheet("color: #b3b3b3; background: transparent;") self.icon_label.setStyleSheet(""" QLabel { color: #b3b3b3; font-size: 16px; font-weight: bold; border-radius: 14px; background: rgba(255, 255, 255, 0.08); } """) class StatusIndicator(QWidget): def __init__(self, service_name: str, parent=None): super().__init__(parent) self.service_name = service_name self.is_connected = False self.setup_ui() def setup_ui(self): self.setFixedHeight(35) # Ensure enough height layout = QHBoxLayout(self) layout.setContentsMargins(15, 8, 15, 8) layout.setSpacing(12) # Status dot with rounded background self.status_dot = QLabel("●") self.status_dot.setFixedSize(16, 16) self.status_dot.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_dot.setStyleSheet(""" QLabel { border-radius: 8px; font-size: 12px; font-weight: bold; } """) # Service name self.service_label = QLabel(self.service_name) self.service_label.setFont(QFont("Arial", 10, QFont.Weight.Medium)) self.service_label.setMinimumWidth(80) # Ensure text doesn't get cut off layout.addWidget(self.status_dot) layout.addWidget(self.service_label) layout.addStretch() self.update_status(False) def update_status(self, connected: bool): self.is_connected = connected if connected: self.status_dot.setStyleSheet(""" QLabel { color: #1db954; background: rgba(29, 185, 84, 0.15); border-radius: 8px; font-size: 12px; font-weight: bold; } """) self.service_label.setStyleSheet("color: #ffffff; font-weight: 500;") else: self.status_dot.setStyleSheet(""" QLabel { color: #e22134; background: rgba(226, 33, 52, 0.15); border-radius: 8px; font-size: 12px; font-weight: bold; } """) self.service_label.setStyleSheet("color: #b3b3b3; font-weight: 400;") class LoadingAnimation(QWidget): """Thin horizontal loading animation for media player with dual-mode capability""" def __init__(self, parent=None): super().__init__(parent) self.setFixedHeight(12) # Increased height for text overlay self._progress = 0.0 self._is_active = False self._mode = "indefinite" # "indefinite" or "determinate" self._determinate_progress = 0.0 # 0-100% for determinate mode # Animation setup for indefinite mode self.animation = QPropertyAnimation(self, b"progress") self.animation.setDuration(1200) # 1.2 second cycle self.animation.setStartValue(0.0) self.animation.setEndValue(1.0) self.animation.setLoopCount(-1) # Infinite loop self.animation.setEasingCurve(QEasingCurve.Type.InOutSine) # Progress value animation for smooth transitions in determinate mode self.progress_animation = QPropertyAnimation(self, b"determinate_progress") self.progress_animation.setDuration(300) # Smooth 300ms transitions self.progress_animation.setEasingCurve(QEasingCurve.Type.OutCubic) # Completion glow effect self._glow_opacity = 0.0 self.glow_animation = QPropertyAnimation(self, b"glow_opacity") self.glow_animation.setDuration(800) # Slower glow pulse self.glow_animation.setStartValue(0.0) self.glow_animation.setEndValue(1.0) self.glow_animation.setLoopCount(3) # Pulse 3 times self.glow_animation.setEasingCurve(QEasingCurve.Type.InOutSine) self.hide() # Start hidden @pyqtProperty(float) def progress(self): return self._progress @progress.setter def progress(self, value): self._progress = value self.update() @pyqtProperty(float) def determinate_progress(self): return self._determinate_progress @determinate_progress.setter def determinate_progress(self, value): self._determinate_progress = value self.update() @pyqtProperty(float) def glow_opacity(self): return self._glow_opacity @glow_opacity.setter def glow_opacity(self, value): self._glow_opacity = value self.update() def start_animation(self): """Start the indefinite loading animation""" self._is_active = True self._mode = "indefinite" self.show() self.animation.start() def set_progress(self, percentage): """Set determinate progress (0-100%) with smooth animation""" if not self._is_active: self._is_active = True self.show() # Switch to determinate mode if self._mode == "indefinite": self._mode = "determinate" self.animation.stop() # Stop indefinite animation # Animate to new progress value self.progress_animation.setStartValue(self._determinate_progress) self.progress_animation.setEndValue(percentage) self.progress_animation.start() # Trigger completion glow effect when reaching 100% if percentage >= 100 and self._determinate_progress < 100: self.glow_animation.start() def stop_animation(self): """Stop the loading animation""" self._is_active = False self._mode = "indefinite" self.animation.stop() self.progress_animation.stop() self.glow_animation.stop() self.hide() self._progress = 0.0 self._determinate_progress = 0.0 self._glow_opacity = 0.0 self.update() def paintEvent(self, event): """Custom paint event for dual-mode animation with text overlay""" if not self._is_active: return painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) width = self.width() height = self.height() progress_bar_height = 4 # Bottom 4px for progress bar text_height = height - progress_bar_height # Top area for text # Background for progress bar area progress_rect = self.rect() progress_rect.setTop(text_height) painter.fillRect(progress_rect, QColor(40, 40, 40)) if self._mode == "indefinite": # Indefinite mode: animated gradient wave gradient_width = width * 0.3 # 30% of total width center_x = self._progress * width for i in range(int(gradient_width)): alpha = max(0, 255 - (abs(i - gradient_width/2) * 8)) color = QColor(29, 185, 84, int(alpha)) # Spotify green with fade x = int(center_x - gradient_width/2 + i) if 0 <= x < width: painter.fillRect(x, text_height, 1, progress_bar_height, color) else: # determinate mode # Determinate mode: progress bar with percentage progress_width = (self._determinate_progress / 100.0) * width # Progress bar with gradient if progress_width > 0: progress_fill_rect = QRect(0, text_height, int(progress_width), progress_bar_height) # Create subtle gradient for progress bar gradient = QLinearGradient(0, text_height, progress_width, text_height) gradient.setColorAt(0, QColor(29, 185, 84)) # Spotify green gradient.setColorAt(1, QColor(30, 215, 96)) # Lighter green painter.fillRect(progress_fill_rect, gradient) # Add animated glow effect during completion if self._glow_opacity > 0: glow_alpha = int(120 * self._glow_opacity) # Max alpha of 120 glow_color = QColor(29, 185, 84, glow_alpha) # Expand glow slightly beyond progress bar for effect glow_rect = QRect(0, text_height - 1, width, progress_bar_height + 2) painter.fillRect(glow_rect, glow_color) # Percentage text overlay (elegant, small font) if text_height > 0 and self._determinate_progress > 0: font = QFont("Segoe UI", 7, QFont.Weight.Medium) # Small, elegant font painter.setFont(font) painter.setPen(QColor(180, 180, 180)) # Light gray text percentage_text = f"{int(self._determinate_progress)}%" text_rect = QRect(0, 0, width, text_height) painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, percentage_text) class MediaPlayer(QWidget): # Signals for media control play_pause_requested = pyqtSignal() stop_requested = pyqtSignal() volume_changed = pyqtSignal(float) # Volume as percentage (0.0 to 1.0) def __init__(self, parent=None): super().__init__(parent) self.is_playing = False self.is_expanded = False self.current_track = None self.setup_ui() def setup_ui(self): self.setFixedHeight(65) # Start collapsed with more breathing room self.setStyleSheet(""" MediaPlayer { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #1e1e1e, stop: 1 #141414); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; margin: 0 6px; } MediaPlayer:hover { border: 1px solid rgba(255, 255, 255, 0.12); background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #212121, stop: 1 #171717); } """) layout = QVBoxLayout(self) layout.setContentsMargins(16, 10, 16, 10) layout.setSpacing(10) # Loading animation at the top self.loading_animation = LoadingAnimation() layout.addWidget(self.loading_animation) # Always visible header with basic controls self.header = self.create_header() layout.addWidget(self.header) # Expandable content (hidden when collapsed) self.expanded_content = self.create_expanded_content() self.expanded_content.setVisible(False) layout.addWidget(self.expanded_content) # No track message (shown when no music) self.no_track_label = QLabel("No track playing") self.no_track_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.no_track_label.setStyleSheet(""" QLabel { color: #6a6a6a; font-size: 11px; font-weight: 400; padding: 12px; background: transparent; letter-spacing: 0.5px; } """) layout.addWidget(self.no_track_label) def create_header(self): header = QWidget() layout = QHBoxLayout(header) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(12) # Track info (expandable on click) - now with scrolling for long titles self.track_info = ScrollingLabel("No track") self.track_info.setStyleSheet(""" ScrollingLabel { color: #ffffff; font-size: 13px; font-weight: 600; background: transparent; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; letter-spacing: 0.2px; padding: 2px 0px; } ScrollingLabel:hover { color: #1ed760; } """) self.track_info.setCursor(Qt.CursorShape.PointingHandCursor) self.track_info.mousePressEvent = self.toggle_expansion # Play/pause button self.play_pause_btn = QPushButton("▶") self.play_pause_btn.setFixedSize(36, 36) self.play_pause_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #1ed760, stop: 1 #1db954); border: none; border-radius: 18px; color: #000000; font-size: 14px; font-weight: 900; font-family: Arial, sans-serif; } QPushButton:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #1fdf64, stop: 1 #1ed760); transform: scale(1.06); } QPushButton:pressed { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #1ca851, stop: 1 #169c46); transform: scale(0.98); } QPushButton:disabled { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #404040, stop: 1 #2a2a2a); color: #666666; } """) self.play_pause_btn.clicked.connect(self.on_play_pause_clicked) self.play_pause_btn.setEnabled(False) layout.addWidget(self.track_info) layout.addStretch() layout.addWidget(self.play_pause_btn) return header def create_expanded_content(self): content = QWidget() layout = QVBoxLayout(content) layout.setContentsMargins(0, 5, 0, 0) layout.setSpacing(8) # Artist and album info self.artist_album_label = QLabel("Unknown Artist • Unknown Album") self.artist_album_label.setStyleSheet(""" QLabel { color: #a7a7a7; font-size: 11px; font-weight: 400; background: transparent; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; letter-spacing: 0.3px; margin-top: 2px; } """) layout.addWidget(self.artist_album_label) # Control buttons controls_layout = QHBoxLayout() controls_layout.setContentsMargins(0, 0, 0, 0) controls_layout.setSpacing(10) # Stop button self.stop_btn = QPushButton("⏹") self.stop_btn.setFixedSize(28, 28) self.stop_btn.setStyleSheet(""" QPushButton { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgba(226, 33, 52, 0.15), stop: 1 rgba(180, 28, 42, 0.15)); border: 1px solid rgba(226, 33, 52, 0.3); border-radius: 14px; color: #e22134; font-size: 11px; font-weight: 700; font-family: Arial, sans-serif; } QPushButton:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgba(226, 33, 52, 0.25), stop: 1 rgba(180, 28, 42, 0.25)); border: 1px solid rgba(226, 33, 52, 0.5); transform: scale(1.05); } QPushButton:pressed { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 rgba(180, 28, 42, 0.3), stop: 1 rgba(150, 23, 35, 0.3)); transform: scale(0.95); } QPushButton:disabled { background: rgba(100, 100, 100, 0.1); border: 1px solid rgba(100, 100, 100, 0.2); color: #666666; } """) self.stop_btn.clicked.connect(self.on_stop_clicked) self.stop_btn.setEnabled(False) # Volume control volume_layout = QHBoxLayout() volume_layout.setSpacing(8) volume_icon = QLabel("♪") volume_icon.setStyleSheet(""" QLabel { color: #a7a7a7; font-size: 12px; font-weight: 600; font-family: Arial, sans-serif; padding: 0px 2px; } """) self.volume_slider = QSlider(Qt.Orientation.Horizontal) self.volume_slider.setRange(0, 100) self.volume_slider.setValue(70) # Default 70% volume self.volume_slider.setFixedWidth(65) self.volume_slider.setStyleSheet(""" QSlider::groove:horizontal { border: none; height: 4px; background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #383838, stop: 1 #2a2a2a); border-radius: 2px; } QSlider::handle:horizontal { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #e0e0e0); border: 1px solid rgba(255, 255, 255, 0.3); width: 12px; height: 12px; border-radius: 6px; margin: -4px 0; } QSlider::handle:horizontal:hover { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #1ed760, stop: 1 #1db954); border: 1px solid rgba(30, 215, 96, 0.5); transform: scale(1.1); } QSlider::sub-page:horizontal { background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #1ed760, stop: 1 #1db954); border-radius: 2px; } """) self.volume_slider.valueChanged.connect(self.on_volume_changed) volume_layout.addWidget(volume_icon) volume_layout.addWidget(self.volume_slider) controls_layout.addWidget(self.stop_btn) controls_layout.addStretch() controls_layout.addLayout(volume_layout) layout.addLayout(controls_layout) return content def toggle_expansion(self, event=None): """Toggle between collapsed and expanded view""" if not self.current_track: return self.is_expanded = not self.is_expanded if self.is_expanded: self.setFixedHeight(125) # Slightly taller for better spacing self.expanded_content.setVisible(True) self.no_track_label.setVisible(False) else: self.setFixedHeight(65) # Match the updated collapsed height self.expanded_content.setVisible(False) def set_track_info(self, track_result): """Update the media player with new track information""" self.current_track = track_result # Update track name track_name = getattr(track_result, 'title', None) or getattr(track_result, 'filename', 'Unknown Track') if hasattr(track_result, 'filename'): # Clean up filename for display import os track_name = os.path.splitext(os.path.basename(track_result.filename))[0] self.track_info.setText(track_name) # Update artist and album artist = getattr(track_result, 'artist', 'Unknown Artist') album = getattr(track_result, 'album', 'Unknown Album') self.artist_album_label.setText(f"{artist} • {album}") # Enable controls self.play_pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) # Set to playing state (show pause button since track just started) self.set_playing_state(True) # Hide loading animation now that track is ready self.hide_loading() # Hide no track message and show player self.no_track_label.setVisible(False) # Auto-expand when new track starts if not self.is_expanded: self.toggle_expansion() def set_playing_state(self, playing): """Update play/pause button state""" self.is_playing = playing if playing: self.play_pause_btn.setText("⏸") # Start scrolling animation when playing if self.track_info.should_scroll and not self.track_info.is_scrolling: self.track_info.start_scroll_animation() else: self.play_pause_btn.setText("▶") # Optionally stop scrolling when paused (can be customized) # self.track_info.stop_scrolling() def clear_track(self): """Clear current track and reset to no track state""" self.current_track = None self.is_playing = False # Stop any animations self.track_info.stop_scrolling() self.hide_loading() # Update UI self.track_info.setText("No track") self.artist_album_label.setText("Unknown Artist • Unknown Album") self.play_pause_btn.setText("▶") self.play_pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) # Show no track message self.no_track_label.setVisible(True) # Collapse view if self.is_expanded: self.toggle_expansion() def on_play_pause_clicked(self): """Handle play/pause button click""" self.play_pause_requested.emit() def on_stop_clicked(self): """Handle stop button click""" self.stop_requested.emit() def on_volume_changed(self, value): """Handle volume slider change""" volume = value / 100.0 # Convert to 0.0-1.0 self.volume_changed.emit(volume) def show_loading(self): """Show and start the loading animation""" self.loading_animation.start_animation() def hide_loading(self): """Hide and stop the loading animation""" self.loading_animation.stop_animation() def set_loading_progress(self, percentage): """Set loading progress percentage (0-100)""" self.loading_animation.set_progress(percentage) class ModernSidebar(QWidget): page_changed = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.current_page = "dashboard" self.buttons = {} self.setup_ui() def setup_ui(self): self.setFixedWidth(220) self.setStyleSheet(""" ModernSidebar { background: #121212; border-right: 1px solid #282828; } """) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Header header = self.create_header() layout.addWidget(header) # Navigation buttons nav_section = self.create_navigation() layout.addWidget(nav_section) # Spacer layout.addItem(QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)) # Media Player section self.media_player = MediaPlayer() layout.addWidget(self.media_player) # Small spacer between media player and status layout.addItem(QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)) # Status section status_section = self.create_status_section() layout.addWidget(status_section) def create_header(self): header = QWidget() header.setFixedHeight(85) header.setStyleSheet(""" QWidget { background: #121212; border-bottom: 1px solid #282828; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; } """) layout = QVBoxLayout(header) layout.setContentsMargins(20, 22, 20, 22) layout.setSpacing(3) # App name app_name = QLabel("SoulSync") app_name.setFont(QFont("Arial", 19, QFont.Weight.Bold)) app_name.setStyleSheet("color: #ffffff; letter-spacing: -0.5px;") # Subtitle subtitle = QLabel("Music Sync & Manager") subtitle.setFont(QFont("Arial", 10)) subtitle.setStyleSheet("color: #b3b3b3; opacity: 0.8;") layout.addWidget(app_name) layout.addWidget(subtitle) return header def create_navigation(self): nav_widget = QWidget() nav_widget.setStyleSheet(""" QWidget { background: #121212; border-radius: 8px; } """) layout = QVBoxLayout(nav_widget) layout.setContentsMargins(8, 20, 8, 20) layout.setSpacing(6) # Navigation buttons nav_items = [ ("dashboard", "Dashboard", "📊"), ("sync", "Playlist Sync", "🔄"), ("downloads", "Downloads", "📥"), ("artists", "Artists", "🎵"), ("settings", "Settings", "⚙️") ] for page_id, title, icon in nav_items: button = SidebarButton(title, icon) button.clicked.connect(lambda checked, pid=page_id: self.change_page(pid)) self.buttons[page_id] = button layout.addWidget(button) # Set dashboard as active by default self.buttons["dashboard"].set_active(True) return nav_widget def create_status_section(self): status_widget = QWidget() status_widget.setFixedHeight(140) # Increased height status_widget.setStyleSheet(""" QWidget { background: #181818; border-top: 1px solid #282828; border-top-left-radius: 8px; border-top-right-radius: 8px; } """) layout = QVBoxLayout(status_widget) layout.setContentsMargins(0, 18, 0, 18) # Better margins layout.setSpacing(6) # Tighter spacing between items # Status title status_title = QLabel("Connection Status") status_title.setFont(QFont("Arial", 11, QFont.Weight.Bold)) status_title.setStyleSheet("color: #ffffff; padding: 0 15px; margin-bottom: 5px;") layout.addWidget(status_title) # Status indicators self.spotify_status = StatusIndicator("Spotify") self.plex_status = StatusIndicator("Plex") self.soulseek_status = StatusIndicator("Soulseek") layout.addWidget(self.spotify_status) layout.addWidget(self.plex_status) layout.addWidget(self.soulseek_status) return status_widget def change_page(self, page_id: str): if page_id != self.current_page: # Update button states for btn_id, button in self.buttons.items(): button.set_active(btn_id == page_id) self.current_page = page_id self.page_changed.emit(page_id) def update_service_status(self, service: str, connected: bool): status_map = { "spotify": self.spotify_status, "plex": self.plex_status, "soulseek": self.soulseek_status } if service in status_map: status_map[service].update_status(connected)