mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1356 lines
50 KiB
1356 lines
50 KiB
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
|
QLabel, QFrame, QSizePolicy, QSpacerItem, QSlider, QProgressBar, QApplication)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect, QTimer, pyqtProperty
|
|
from PyQt6.QtGui import QFont, QPalette, QIcon, QPixmap, QPainter, QFontMetrics, QColor, QLinearGradient
|
|
from utils.logging_config import get_logger
|
|
|
|
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(52)
|
|
self.setFixedWidth(216) # Adjusted for new sidebar width
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(18, 0, 18, 0)
|
|
layout.setSpacing(16)
|
|
|
|
# Icon label with better styling
|
|
self.icon_label = QLabel(self.icon_text)
|
|
self.icon_label.setFixedSize(28, 28)
|
|
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.icon_label.setStyleSheet("""
|
|
QLabel {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
border-radius: 14px;
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(255, 255, 255, 0.08),
|
|
stop: 1 rgba(255, 255, 255, 0.04));
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
""")
|
|
|
|
# Text label with improved typography
|
|
self.text_label = QLabel(self.text)
|
|
self.text_label.setFont(QFont("SF Pro Text", 12, 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: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(29, 185, 84, 0.18),
|
|
stop: 0.5 rgba(29, 185, 84, 0.12),
|
|
stop: 1 rgba(29, 185, 84, 0.08));
|
|
border-left: 3px solid #1ed760;
|
|
border-radius: 16px;
|
|
text-align: left;
|
|
padding: 0px;
|
|
border: 1px solid rgba(29, 185, 84, 0.2);
|
|
}
|
|
SidebarButton:hover {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(29, 185, 84, 0.25),
|
|
stop: 0.5 rgba(29, 185, 84, 0.18),
|
|
stop: 1 rgba(29, 185, 84, 0.12));
|
|
border: 1px solid rgba(29, 185, 84, 0.3);
|
|
}
|
|
""")
|
|
self.text_label.setStyleSheet("""
|
|
color: #1ed760;
|
|
font-weight: 600;
|
|
background: transparent;
|
|
letter-spacing: 0.1px;
|
|
""")
|
|
self.icon_label.setStyleSheet("""
|
|
QLabel {
|
|
color: #1ed760;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
border-radius: 14px;
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(29, 185, 84, 0.25),
|
|
stop: 1 rgba(30, 215, 96, 0.2));
|
|
border: 1px solid rgba(29, 185, 84, 0.3);
|
|
}
|
|
""")
|
|
else:
|
|
self.setStyleSheet("""
|
|
SidebarButton {
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 16px;
|
|
text-align: left;
|
|
padding: 0px;
|
|
}
|
|
SidebarButton:hover {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(255, 255, 255, 0.06),
|
|
stop: 1 rgba(255, 255, 255, 0.03));
|
|
border-left: 2px solid rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
""")
|
|
self.text_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.8);
|
|
background: transparent;
|
|
letter-spacing: 0.1px;
|
|
""")
|
|
self.icon_label.setStyleSheet("""
|
|
QLabel {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
border-radius: 14px;
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(255, 255, 255, 0.08),
|
|
stop: 1 rgba(255, 255, 255, 0.04));
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
}
|
|
""")
|
|
|
|
class CryptoDonationWidget(QWidget):
|
|
"""Widget for displaying crypto donation addresses with collapsible section"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.addresses_visible = False
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
self.setStyleSheet("""
|
|
CryptoDonationWidget {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
stop: 0 transparent,
|
|
stop: 0.3 rgba(255, 255, 255, 0.02),
|
|
stop: 1 rgba(255, 255, 255, 0.04));
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-bottom-right-radius: 12px;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 15, 0, 15)
|
|
layout.setSpacing(8)
|
|
|
|
# Header with title and toggle button
|
|
header_layout = QHBoxLayout()
|
|
header_layout.setContentsMargins(20, 0, 20, 0)
|
|
header_layout.setSpacing(8)
|
|
|
|
# Donation title
|
|
donation_title = QLabel("Support Development")
|
|
donation_title.setFont(QFont("SF Pro Text", 10, QFont.Weight.Bold))
|
|
donation_title.setMinimumHeight(16)
|
|
donation_title.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.9);
|
|
margin-bottom: 5px;
|
|
letter-spacing: 0.2px;
|
|
font-weight: 600;
|
|
""")
|
|
|
|
# Toggle button
|
|
self.toggle_btn = QPushButton("Show")
|
|
self.toggle_btn.setFixedSize(40, 20)
|
|
self.toggle_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
self.toggle_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 10px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
font-size: 8px;
|
|
font-weight: 500;
|
|
}
|
|
QPushButton:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
""")
|
|
self.toggle_btn.clicked.connect(self.toggle_addresses)
|
|
|
|
header_layout.addWidget(donation_title)
|
|
header_layout.addStretch()
|
|
header_layout.addWidget(self.toggle_btn)
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# Container for donation options (initially hidden)
|
|
self.addresses_container = QWidget()
|
|
self.addresses_layout = QVBoxLayout(self.addresses_container)
|
|
self.addresses_layout.setContentsMargins(0, 0, 0, 0)
|
|
self.addresses_layout.setSpacing(8)
|
|
|
|
# Ko-fi option (first item)
|
|
kofi_item = self.create_kofi_item()
|
|
self.addresses_layout.addWidget(kofi_item)
|
|
|
|
# Crypto addresses
|
|
crypto_addresses = [
|
|
("BTC", "Bitcoin", "3JVWrRSkozAQSmw5DXYVxYKsM9bndPTqdS"),
|
|
("ETH", "Ethereum", "0x343fC48c2cd1C6332b0df9a58F86e6520a026AC5")
|
|
]
|
|
|
|
for symbol, name, address in crypto_addresses:
|
|
crypto_item = self.create_crypto_item(symbol, name, address)
|
|
self.addresses_layout.addWidget(crypto_item)
|
|
|
|
# Initially hide the addresses
|
|
self.addresses_container.hide()
|
|
layout.addWidget(self.addresses_container)
|
|
|
|
def toggle_addresses(self):
|
|
"""Toggle the visibility of crypto addresses"""
|
|
self.addresses_visible = not self.addresses_visible
|
|
|
|
if self.addresses_visible:
|
|
self.addresses_container.show()
|
|
self.toggle_btn.setText("Hide")
|
|
else:
|
|
self.addresses_container.hide()
|
|
self.toggle_btn.setText("Show")
|
|
|
|
def create_crypto_item(self, symbol: str, name: str, address: str):
|
|
"""Create a clickable crypto donation item"""
|
|
item = QFrame()
|
|
item.setFixedHeight(32)
|
|
item.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
item.setStyleSheet("""
|
|
QFrame {
|
|
background: transparent;
|
|
border-radius: 8px;
|
|
margin: 0 12px;
|
|
}
|
|
QFrame:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
""")
|
|
|
|
layout = QHBoxLayout(item)
|
|
layout.setContentsMargins(12, 4, 12, 4)
|
|
layout.setSpacing(6)
|
|
|
|
# Crypto name
|
|
name_label = QLabel(name)
|
|
name_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium))
|
|
name_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.8);
|
|
font-weight: 500;
|
|
""")
|
|
|
|
# Address (truncated)
|
|
address_short = f"{address[:6]}...{address[-4:]}"
|
|
address_label = QLabel(address_short)
|
|
address_label.setFont(QFont("SF Pro Text", 8))
|
|
address_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.5);
|
|
font-family: 'Courier New', monospace;
|
|
""")
|
|
|
|
layout.addWidget(name_label)
|
|
layout.addStretch()
|
|
layout.addWidget(address_label)
|
|
|
|
# Store full address for copying
|
|
item.full_address = address
|
|
item.crypto_name = name
|
|
item.mousePressEvent = lambda event: self.copy_address(address, name)
|
|
|
|
return item
|
|
|
|
def copy_address(self, address: str, crypto_name: str):
|
|
"""Copy crypto address to clipboard"""
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(address)
|
|
|
|
# Brief visual feedback (could add a tooltip or status message here)
|
|
print(f"Copied {crypto_name} address to clipboard: {address}")
|
|
|
|
def create_kofi_item(self):
|
|
"""Create a clickable Ko-fi donation item styled like crypto items"""
|
|
item = QFrame()
|
|
item.setFixedHeight(32)
|
|
item.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
item.setStyleSheet("""
|
|
QFrame {
|
|
background: transparent;
|
|
border-radius: 8px;
|
|
margin: 0 12px;
|
|
}
|
|
QFrame:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
""")
|
|
|
|
layout = QHBoxLayout(item)
|
|
layout.setContentsMargins(12, 4, 12, 4)
|
|
layout.setSpacing(6)
|
|
|
|
# Ko-fi name
|
|
name_label = QLabel("Ko-fi")
|
|
name_label.setFont(QFont("SF Pro Text", 9, QFont.Weight.Medium))
|
|
name_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.8);
|
|
font-weight: 500;
|
|
""")
|
|
|
|
# External link indicator (instead of address)
|
|
link_label = QLabel("Click to open")
|
|
link_label.setFont(QFont("SF Pro Text", 8))
|
|
link_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.5);
|
|
font-style: italic;
|
|
""")
|
|
|
|
layout.addWidget(name_label)
|
|
layout.addStretch()
|
|
layout.addWidget(link_label)
|
|
|
|
# Connect click event to open Ko-fi link
|
|
item.mousePressEvent = lambda event: self.open_kofi_link()
|
|
|
|
return item
|
|
|
|
def open_kofi_link(self):
|
|
"""Open Ko-fi link in the user's default web browser"""
|
|
import webbrowser
|
|
kofi_url = "https://ko-fi.com/boulderbadgedad"
|
|
webbrowser.open(kofi_url)
|
|
print(f"Opening Ko-fi link: {kofi_url}")
|
|
|
|
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(38) # Slightly taller for better proportions
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(20, 8, 20, 8)
|
|
layout.setSpacing(14)
|
|
|
|
# Status dot with more elegant design
|
|
self.status_dot = QLabel("●")
|
|
self.status_dot.setFixedSize(18, 18)
|
|
self.status_dot.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.status_dot.setStyleSheet("""
|
|
QLabel {
|
|
border-radius: 9px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
""")
|
|
|
|
# Service name with better typography
|
|
self.service_label = QLabel(self.service_name)
|
|
self.service_label.setFont(QFont("SF Pro Text", 10, QFont.Weight.Medium))
|
|
self.service_label.setMinimumWidth(85)
|
|
|
|
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: #1ed760;
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(29, 185, 84, 0.2),
|
|
stop: 1 rgba(30, 215, 96, 0.15));
|
|
border-radius: 9px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
border: 1px solid rgba(29, 185, 84, 0.3);
|
|
}
|
|
""")
|
|
self.service_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.95);
|
|
font-weight: 500;
|
|
letter-spacing: 0.1px;
|
|
""")
|
|
else:
|
|
self.status_dot.setStyleSheet("""
|
|
QLabel {
|
|
color: #ff6b6b;
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 rgba(255, 107, 107, 0.15),
|
|
stop: 1 rgba(255, 107, 107, 0.1));
|
|
border-radius: 9px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
border: 1px solid rgba(255, 107, 107, 0.2);
|
|
}
|
|
""")
|
|
self.service_label.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.5);
|
|
font-weight: 400;
|
|
letter-spacing: 0.1px;
|
|
""")
|
|
|
|
def update_name(self, new_name: str):
|
|
"""Update the service name displayed in the status indicator"""
|
|
self.service_name = new_name
|
|
self.service_label.setText(new_name)
|
|
|
|
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(85) # More space for better proportions
|
|
self.setStyleSheet("""
|
|
MediaPlayer {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
stop: 0 rgba(26, 26, 26, 0.95),
|
|
stop: 0.5 rgba(18, 18, 18, 0.98),
|
|
stop: 1 rgba(12, 12, 12, 1.0));
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
border-radius: 12px;
|
|
margin: 8px 10px;
|
|
}
|
|
MediaPlayer:hover {
|
|
border: 1px solid rgba(29, 185, 84, 0.2);
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
stop: 0 rgba(29, 185, 84, 0.08),
|
|
stop: 0.5 rgba(26, 26, 26, 0.95),
|
|
stop: 1 rgba(18, 18, 18, 1.0));
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(18, 12, 18, 12)
|
|
layout.setSpacing(12)
|
|
|
|
# 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("Start playing music to see controls")
|
|
self.no_track_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.no_track_label.setStyleSheet("""
|
|
QLabel {
|
|
color: #6a6a6a;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
padding: 20px 16px;
|
|
background: transparent;
|
|
letter-spacing: 0.2px;
|
|
font-family: 'Spotify Circular', -apple-system, sans-serif;
|
|
line-height: 1.4;
|
|
}
|
|
""")
|
|
layout.addWidget(self.no_track_label)
|
|
|
|
def create_header(self):
|
|
header = QWidget()
|
|
main_layout = QVBoxLayout(header)
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
main_layout.setSpacing(8)
|
|
|
|
# Top row: Track info and play button
|
|
top_row = QHBoxLayout()
|
|
top_row.setContentsMargins(0, 0, 0, 0)
|
|
top_row.setSpacing(14)
|
|
|
|
# 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: 14px;
|
|
font-weight: 700;
|
|
background: transparent;
|
|
font-family: 'Spotify Circular', 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
letter-spacing: -0.3px;
|
|
padding: 2px 0px;
|
|
line-height: 1.2;
|
|
}
|
|
ScrollingLabel:hover {
|
|
color: #1ed760;
|
|
text-decoration: underline;
|
|
}
|
|
""")
|
|
self.track_info.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
self.track_info.mousePressEvent = self.toggle_expansion
|
|
|
|
# Play/pause button - more Spotify-like
|
|
self.play_pause_btn = QPushButton("▷")
|
|
self.play_pause_btn.setFixedSize(40, 40)
|
|
self.play_pause_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #1ed760;
|
|
border: none;
|
|
border-radius: 20px;
|
|
color: #000000;
|
|
font-size: 16px;
|
|
font-weight: 900;
|
|
font-family: 'Arial', sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #1fdf64;
|
|
}
|
|
QPushButton:pressed {
|
|
background: #1ca851;
|
|
}
|
|
QPushButton:disabled {
|
|
background: #535353;
|
|
color: #b3b3b3;
|
|
}
|
|
""")
|
|
self.play_pause_btn.clicked.connect(self.on_play_pause_clicked)
|
|
self.play_pause_btn.setEnabled(False)
|
|
|
|
top_row.addWidget(self.track_info)
|
|
top_row.addStretch()
|
|
top_row.addWidget(self.play_pause_btn)
|
|
|
|
# Bottom row: Artist info (always visible in collapsed mode)
|
|
self.artist_info = QLabel("Unknown Artist")
|
|
self.artist_info.setStyleSheet("""
|
|
QLabel {
|
|
color: #b3b3b3;
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
background: transparent;
|
|
font-family: 'Spotify Circular', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
letter-spacing: 0.1px;
|
|
margin-top: 1px;
|
|
}
|
|
""")
|
|
|
|
main_layout.addLayout(top_row)
|
|
main_layout.addWidget(self.artist_info)
|
|
|
|
return header
|
|
|
|
def create_expanded_content(self):
|
|
content = QWidget()
|
|
layout = QVBoxLayout(content)
|
|
layout.setContentsMargins(0, 2, 0, 0)
|
|
layout.setSpacing(4)
|
|
|
|
# Album info
|
|
self.album_label = QLabel("Unknown Album")
|
|
self.album_label.setStyleSheet("""
|
|
QLabel {
|
|
color: #a7a7a7;
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
background: transparent;
|
|
font-family: 'Spotify Circular', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
letter-spacing: 0.1px;
|
|
}
|
|
""")
|
|
layout.addWidget(self.album_label)
|
|
|
|
# Control buttons - more Spotify-like
|
|
controls_layout = QHBoxLayout()
|
|
controls_layout.setContentsMargins(0, 1, 0, 0)
|
|
controls_layout.setSpacing(6)
|
|
|
|
# Volume control (Spotify style - more prominent)
|
|
volume_layout = QHBoxLayout()
|
|
volume_layout.setSpacing(10)
|
|
|
|
volume_icon = QLabel("🔊")
|
|
volume_icon.setStyleSheet("""
|
|
QLabel {
|
|
color: #b3b3b3;
|
|
font-size: 13px;
|
|
font-weight: 400;
|
|
padding: 0px;
|
|
}
|
|
""")
|
|
|
|
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(80)
|
|
self.volume_slider.setFixedHeight(20)
|
|
self.volume_slider.setStyleSheet("""
|
|
QSlider::groove:horizontal {
|
|
border: none;
|
|
height: 3px;
|
|
background: #4f4f4f;
|
|
border-radius: 1px;
|
|
}
|
|
QSlider::handle:horizontal {
|
|
background: #ffffff;
|
|
border: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 6px;
|
|
margin: -4px 0;
|
|
}
|
|
QSlider::handle:horizontal:hover {
|
|
background: #1ed760;
|
|
}
|
|
QSlider::sub-page:horizontal {
|
|
background: #1ed760;
|
|
border-radius: 1px;
|
|
}
|
|
""")
|
|
self.volume_slider.valueChanged.connect(self.on_volume_changed)
|
|
|
|
# Stop button - more visible Spotify style
|
|
self.stop_btn = QPushButton("⏹")
|
|
self.stop_btn.setFixedSize(32, 32)
|
|
self.stop_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid #b3b3b3;
|
|
border-radius: 16px;
|
|
color: #ffffff;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
QPushButton:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border: 1px solid #ffffff;
|
|
color: #ffffff;
|
|
}
|
|
QPushButton:pressed {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
}
|
|
QPushButton:disabled {
|
|
background: transparent;
|
|
border: 1px solid #2a2a2a;
|
|
color: #535353;
|
|
}
|
|
""")
|
|
self.stop_btn.clicked.connect(self.on_stop_clicked)
|
|
self.stop_btn.setEnabled(False)
|
|
|
|
volume_layout.addWidget(volume_icon)
|
|
volume_layout.addWidget(self.volume_slider)
|
|
|
|
controls_layout.addLayout(volume_layout)
|
|
controls_layout.addStretch()
|
|
controls_layout.addWidget(self.stop_btn)
|
|
|
|
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(145) # More space for the new layout
|
|
self.expanded_content.setVisible(True)
|
|
self.no_track_label.setVisible(False)
|
|
else:
|
|
self.setFixedHeight(85) # 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 info
|
|
artist = getattr(track_result, 'artist', 'Unknown Artist')
|
|
album = getattr(track_result, 'album', 'Unknown Album')
|
|
|
|
# Update the separate artist and album labels
|
|
self.artist_info.setText(artist)
|
|
self.album_label.setText(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_info.setText("Unknown Artist")
|
|
self.album_label.setText("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(240) # Slightly wider for better proportions
|
|
self.setStyleSheet("""
|
|
ModernSidebar {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1,
|
|
stop: 0 #0d1117,
|
|
stop: 0.3 #121212,
|
|
stop: 1 #0a0a0a);
|
|
border-right: 1px solid rgba(29, 185, 84, 0.1);
|
|
border-top-right-radius: 12px;
|
|
border-bottom-right-radius: 12px;
|
|
}
|
|
""")
|
|
|
|
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 crypto
|
|
layout.addItem(QSpacerItem(20, 8, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed))
|
|
|
|
# Crypto Donation section
|
|
crypto_section = CryptoDonationWidget()
|
|
layout.addWidget(crypto_section)
|
|
|
|
# Version info section
|
|
version_section = self.create_version_section()
|
|
layout.addWidget(version_section)
|
|
|
|
# Small spacer between version and status
|
|
layout.addItem(QSpacerItem(20, 8, 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(95)
|
|
header.setStyleSheet("""
|
|
QWidget {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
stop: 0 rgba(29, 185, 84, 0.08),
|
|
stop: 0.4 rgba(29, 185, 84, 0.03),
|
|
stop: 1 transparent);
|
|
border-bottom: 1px solid rgba(29, 185, 84, 0.15);
|
|
border-top-right-radius: 12px;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(header)
|
|
layout.setContentsMargins(24, 24, 24, 20)
|
|
layout.setSpacing(4)
|
|
|
|
# App name with gradient text effect
|
|
app_name = QLabel("SoulSync")
|
|
app_name.setFont(QFont("SF Pro Display", 20, QFont.Weight.Bold))
|
|
app_name.setStyleSheet("""
|
|
color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
|
|
stop: 0 #ffffff,
|
|
stop: 0.6 #1ed760,
|
|
stop: 1 #1db954);
|
|
letter-spacing: -0.8px;
|
|
font-weight: 700;
|
|
""")
|
|
|
|
# Subtitle with better typography
|
|
subtitle = QLabel("Music Sync & Manager")
|
|
subtitle.setFont(QFont("SF Pro Text", 10, QFont.Weight.Medium))
|
|
subtitle.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.65);
|
|
letter-spacing: 0.2px;
|
|
font-weight: 500;
|
|
margin-top: 2px;
|
|
""")
|
|
|
|
layout.addWidget(app_name)
|
|
layout.addWidget(subtitle)
|
|
|
|
return header
|
|
|
|
def create_navigation(self):
|
|
nav_widget = QWidget()
|
|
nav_widget.setStyleSheet("""
|
|
QWidget {
|
|
background: transparent;
|
|
border-radius: 12px;
|
|
}
|
|
""")
|
|
layout = QVBoxLayout(nav_widget)
|
|
layout.setContentsMargins(12, 25, 12, 25)
|
|
layout.setSpacing(8)
|
|
|
|
# Navigation buttons
|
|
nav_items = [
|
|
("dashboard", "Dashboard", "📊"),
|
|
("sync", "Sync", "🔄"),
|
|
("downloads", "Search", "📥"),
|
|
("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_version_section(self):
|
|
version_widget = QWidget()
|
|
version_widget.setFixedHeight(45)
|
|
version_widget.setStyleSheet("""
|
|
QWidget {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
stop: 0 transparent,
|
|
stop: 0.3 rgba(255, 255, 255, 0.02),
|
|
stop: 1 rgba(255, 255, 255, 0.04));
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 8px;
|
|
margin: 0 10px;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(version_widget)
|
|
layout.setContentsMargins(20, 12, 20, 12)
|
|
layout.setSpacing(0)
|
|
|
|
# Version button (clickable)
|
|
self.version_button = QPushButton("v.0.6")
|
|
self.version_button.setFont(QFont("SF Pro Text", 10, QFont.Weight.Medium))
|
|
self.version_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
self.version_button.setStyleSheet("""
|
|
QPushButton {
|
|
color: rgba(255, 255, 255, 0.6);
|
|
letter-spacing: 0.1px;
|
|
font-weight: 500;
|
|
background: transparent;
|
|
border: none;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #1ed760;
|
|
background: rgba(29, 185, 84, 0.1);
|
|
border: 1px solid rgba(29, 185, 84, 0.2);
|
|
}
|
|
QPushButton:pressed {
|
|
background: rgba(29, 185, 84, 0.15);
|
|
}
|
|
""")
|
|
self.version_button.clicked.connect(self.show_version_info)
|
|
layout.addWidget(self.version_button)
|
|
|
|
return version_widget
|
|
|
|
def create_status_section(self):
|
|
status_widget = QWidget()
|
|
status_widget.setFixedHeight(150) # Slightly taller for better proportions
|
|
status_widget.setStyleSheet("""
|
|
QWidget {
|
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
|
stop: 0 transparent,
|
|
stop: 0.3 rgba(255, 255, 255, 0.02),
|
|
stop: 1 rgba(255, 255, 255, 0.04));
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-bottom-right-radius: 12px;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(status_widget)
|
|
layout.setContentsMargins(0, 20, 0, 20)
|
|
layout.setSpacing(8)
|
|
|
|
# Status title with better typography
|
|
status_title = QLabel("Service Status")
|
|
status_title.setFont(QFont("SF Pro Text", 11, QFont.Weight.Bold))
|
|
status_title.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 0.9);
|
|
padding: 0 20px;
|
|
margin-bottom: 8px;
|
|
letter-spacing: 0.2px;
|
|
font-weight: 600;
|
|
""")
|
|
layout.addWidget(status_title)
|
|
|
|
# Status indicators
|
|
self.spotify_status = StatusIndicator("Spotify")
|
|
|
|
# Dynamic media server status - determine which server is active
|
|
from config.settings import config_manager
|
|
active_server = config_manager.get_active_media_server()
|
|
server_name = "Plex" if active_server == "plex" else "Jellyfin"
|
|
self.media_server_status = StatusIndicator(server_name)
|
|
|
|
self.soulseek_status = StatusIndicator("Soulseek")
|
|
|
|
layout.addWidget(self.spotify_status)
|
|
layout.addWidget(self.media_server_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.media_server_status,
|
|
"jellyfin": self.media_server_status,
|
|
"soulseek": self.soulseek_status
|
|
}
|
|
|
|
if service in status_map:
|
|
status_map[service].update_status(connected)
|
|
|
|
def update_media_server_name(self, server_type: str):
|
|
"""Update the media server status indicator name"""
|
|
server_name = "Plex" if server_type == "plex" else "Jellyfin"
|
|
if hasattr(self, 'media_server_status'):
|
|
self.media_server_status.update_name(server_name)
|
|
|
|
def show_version_info(self):
|
|
"""Show the version information modal"""
|
|
try:
|
|
from ui.components.version_info_modal import VersionInfoModal
|
|
modal = VersionInfoModal(self)
|
|
modal.exec()
|
|
except Exception as e:
|
|
logger = get_logger("sidebar")
|
|
logger.error(f"Error showing version info modal: {e}") |