diff --git a/projects/EU-Utility/plugins/calculator/plugin.py b/projects/EU-Utility/plugins/calculator/plugin.py index 7b053bd..2514914 100644 --- a/projects/EU-Utility/plugins/calculator/plugin.py +++ b/projects/EU-Utility/plugins/calculator/plugin.py @@ -1,13 +1,12 @@ """ EU-Utility - Calculator Plugin -Simple calculator with basic math and EU-specific calculations. +Standard calculator with Windows-style layout. """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, QGridLayout, - QTabWidget, QSpinBox, QDoubleSpinBox + QLineEdit, QPushButton, QLabel, QGridLayout ) from PyQt6.QtCore import Qt @@ -15,286 +14,373 @@ from plugins.base_plugin import BasePlugin class CalculatorPlugin(BasePlugin): - """Simple calculator with EU-specific features.""" + """Standard calculator with Windows-style layout.""" name = "Calculator" - version = "1.0.0" + version = "1.1.0" author = "ImpulsiveFPS" - description = "Basic calculator and EU unit conversions" + description = "Standard calculator" hotkey = "ctrl+shift+c" def initialize(self): """Setup calculator.""" - self.current_input = "" + self.current_value = "0" + self.stored_value = None + self.pending_op = None + self.memory = 0 + self.start_new = True def get_ui(self): - """Create calculator UI.""" + """Create calculator UI with Windows layout.""" widget = QWidget() layout = QVBoxLayout(widget) + layout.setSpacing(2) - # Tabs for different calculators - tabs = QTabWidget() - tabs.setStyleSheet(""" - QTabWidget::pane { - background-color: #333; - border: 1px solid #555; - } - QTabBar::tab { - background-color: #444; - color: #aaa; - padding: 8px 16px; - border: none; - } - QTabBar::tab:selected { - background-color: #4a9eff; - color: white; - } - """) - - # Basic Calculator Tab - basic_tab = self._create_basic_calculator() - tabs.addTab(basic_tab, "Basic") - - # EU Converter Tab - eu_tab = self._create_eu_converter() - tabs.addTab(eu_tab, "EU Units") - - layout.addWidget(tabs) - return widget - - def _create_basic_calculator(self): - """Create basic calculator.""" - tab = QWidget() - layout = QVBoxLayout(tab) + # Title + title = QLabel("🧮 Calculator") + title.setStyleSheet("color: #4a9eff; font-size: 16px; font-weight: bold;") + layout.addWidget(title) # Display - self.calc_display = QLineEdit("0") - self.calc_display.setAlignment(Qt.AlignmentFlag.AlignRight) - self.calc_display.setStyleSheet(""" + self.display = QLineEdit("0") + self.display.setAlignment(Qt.AlignmentFlag.AlignRight) + self.display.setStyleSheet(""" QLineEdit { - background-color: #222; - color: #0f0; - font-size: 24px; - font-family: Consolas, monospace; - padding: 10px; - border: 2px solid #555; + background-color: #1a1a1a; + color: white; + font-size: 32px; + font-family: 'Segoe UI', Arial; + padding: 15px; + border: none; border-radius: 4px; } """) - self.calc_display.setReadOnly(True) - layout.addWidget(self.calc_display) - - # Button grid - buttons = [ - ['C', '⌫', '%', '÷'], - ['7', '8', '9', '×'], - ['4', '5', '6', '-'], - ['1', '2', '3', '+'], - ['0', '.', '=', ''] - ] + self.display.setReadOnly(True) + layout.addWidget(self.display) + # Button grid - Windows Calculator Layout grid = QGridLayout() - for row, row_buttons in enumerate(buttons): - for col, btn_text in enumerate(row_buttons): - if not btn_text: - continue - - btn = QPushButton(btn_text) - btn.setFixedSize(60, 50) - - # Style based on button type - if btn_text in ['C', '⌫']: - style = """ - QPushButton { - background-color: #c44; - color: white; - font-size: 16px; - font-weight: bold; - border: none; - border-radius: 4px; - } - QPushButton:hover { background-color: #d55; } - """ - elif btn_text in ['÷', '×', '-', '+', '=', '%']: - style = """ - QPushButton { - background-color: #4a9eff; - color: white; - font-size: 18px; - font-weight: bold; - border: none; - border-radius: 4px; - } - QPushButton:hover { background-color: #5aafff; } - """ - else: - style = """ - QPushButton { - background-color: #444; - color: white; - font-size: 18px; - border: none; - border-radius: 4px; - } - QPushButton:hover { background-color: #555; } - """ - - btn.setStyleSheet(style) - btn.clicked.connect(lambda checked, t=btn_text: self._on_calc_button(t)) - grid.addWidget(btn, row, col) + grid.setSpacing(2) + + # Row 1: Memory buttons + mem_buttons = ['MC', 'MR', 'M+', 'M-', 'MS', 'M~'] + for i, btn_text in enumerate(mem_buttons): + btn = self._create_button(btn_text, "#3a3a3a") + btn.clicked.connect(lambda checked, t=btn_text: self._on_memory(t)) + grid.addWidget(btn, 0, i) + + # Row 2: %, CE, C, ⌫ + row2 = ['%', 'CE', 'C', '⌫'] + for i, btn_text in enumerate(row2): + btn = self._create_button(btn_text, "#3a3a3a") + btn.clicked.connect(lambda checked, t=btn_text: self._on_special(t)) + grid.addWidget(btn, 1, i) + + # Row 3: ¹/ₓ, x², ²√x, ÷ + row3 = [('¹/ₓ', '1/x'), ('x²', 'sq'), ('²√x', 'sqrt'), '÷'] + for i, item in enumerate(row3): + if isinstance(item, tuple): + text, op = item + else: + text = op = item + btn = self._create_button(text, "#3a3a3a") + btn.clicked.connect(lambda checked, o=op: self._on_operator(o)) + grid.addWidget(btn, 2, i) + + # Row 4: 7, 8, 9, × + row4 = ['7', '8', '9', '×'] + for i, btn_text in enumerate(row4): + btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['×']) + if btn_text == '×': + btn.clicked.connect(lambda checked: self._on_operator('*')) + else: + btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t)) + grid.addWidget(btn, 3, i) + + # Row 5: 4, 5, 6, - + row5 = ['4', '5', '6', '-'] + for i, btn_text in enumerate(row5): + btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['-']) + if btn_text == '-': + btn.clicked.connect(lambda checked: self._on_operator('-')) + else: + btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t)) + grid.addWidget(btn, 4, i) + + # Row 6: 1, 2, 3, + + row6 = ['1', '2', '3', '+'] + for i, btn_text in enumerate(row6): + btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['+']) + if btn_text == '+': + btn.clicked.connect(lambda checked: self._on_operator('+')) + else: + btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t)) + grid.addWidget(btn, 5, i) + + # Row 7: +/-, 0, ., = + row7 = [('±', '+/-'), '0', '.', '='] + for i, item in enumerate(row7): + if isinstance(item, tuple): + text, val = item + else: + text = val = item + + if val == '=': + btn = self._create_button(text, "#0078d4", text_color="white") # Blue equals + else: + btn = self._create_button(text, "#2a2a2a", is_number=True) + + if val == '+/-': + btn.clicked.connect(self._on_negate) + elif val == '.': + btn.clicked.connect(self._on_decimal) + elif val == '=': + btn.clicked.connect(self._on_equals) + else: + btn.clicked.connect(lambda checked, t=val: self._on_number(t)) + + grid.addWidget(btn, 6, i) + + # Set column stretch + for i in range(4): + grid.setColumnStretch(i, 1) + + # Set row stretch + for i in range(7): + grid.setRowStretch(i, 1) layout.addLayout(grid) layout.addStretch() - return tab + return widget - def _create_eu_converter(self): - """Create EU unit converter.""" - tab = QWidget() - layout = QVBoxLayout(tab) + def _create_button(self, text, bg_color, is_number=False, text_color="#ffffff"): + """Create a calculator button.""" + btn = QPushButton(text) + btn.setMinimumSize(60, 45) - # Title - title = QLabel("💰 PED Converter") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # PED input - ped_layout = QHBoxLayout() - ped_layout.addWidget(QLabel("PED:")) - - self.ped_input = QDoubleSpinBox() - self.ped_input.setRange(0, 1000000) - self.ped_input.setDecimals(4) - self.ped_input.setValue(1.0000) - self.ped_input.setStyleSheet(""" - QDoubleSpinBox { - background-color: #333; - color: #0f0; - font-size: 16px; - padding: 5px; - border: 1px solid #555; - } - """) - self.ped_input.valueChanged.connect(self._convert_ped) - ped_layout.addWidget(self.ped_input) - layout.addLayout(ped_layout) - - # Results - self.pec_result = QLabel("= 1,000 PEC") - self.pec_result.setStyleSheet("color: #aaa; font-size: 14px;") - layout.addWidget(self.pec_result) - - self.usd_result = QLabel("≈ $0.10 USD") - self.usd_result.setStyleSheet("color: #aaa; font-size: 14px;") - layout.addWidget(self.usd_result) - - # Divider - line = QLabel("─" * 40) - line.setStyleSheet("color: #555;") - layout.addWidget(line) - - # DPP Calculator - dpp_title = QLabel("⚔️ DPP Calculator") - dpp_title.setStyleSheet("color: white; font-size: 16px; font-weight: bold; margin-top: 10px;") - layout.addWidget(dpp_title) - - # Damage - dmg_layout = QHBoxLayout() - dmg_layout.addWidget(QLabel("Damage:")) - self.dmg_input = QDoubleSpinBox() - self.dmg_input.setRange(0.01, 10000) - self.dmg_input.setDecimals(2) - self.dmg_input.setValue(50.0) - self.dmg_input.setStyleSheet(""" - QDoubleSpinBox { - background-color: #333; - color: white; - font-size: 14px; - padding: 3px; - border: 1px solid #555; - } - """) - self.dmg_input.valueChanged.connect(self._calc_dpp) - dmg_layout.addWidget(self.dmg_input) - layout.addLayout(dmg_layout) - - # Cost - cost_layout = QHBoxLayout() - cost_layout.addWidget(QLabel("Cost (PEC):")) - self.cost_input = QDoubleSpinBox() - self.cost_input.setRange(0.01, 10000) - self.cost_input.setDecimals(4) - self.cost_input.setValue(2.5) - self.cost_input.setStyleSheet(""" - QDoubleSpinBox { - background-color: #333; - color: white; - font-size: 14px; - padding: 3px; - border: 1px solid #555; - } - """) - self.cost_input.valueChanged.connect(self._calc_dpp) - cost_layout.addWidget(self.cost_input) - layout.addLayout(cost_layout) - - # DPP Result - self.dpp_result = QLabel("DPP: 20.00") - self.dpp_result.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold; margin-top: 5px;") - layout.addWidget(self.dpp_result) - - layout.addStretch() - return tab - - def _on_calc_button(self, text): - """Handle calculator button press.""" - current = self.calc_display.text() - - if text == 'C': - self.calc_display.setText("0") - elif text == '⌫': - self.calc_display.setText(current[:-1] if len(current) > 1 else "0") - elif text == '=': - try: - # Replace display symbols with Python operators - expr = current.replace('×', '*').replace('÷', '/') - result = eval(expr) - self.calc_display.setText(str(result)[:12]) - except: - self.calc_display.setText("Error") - elif text in ['+', '-', '×', '÷', '%']: - if current[-1] not in '+-×÷%': - self.calc_display.setText(current + text) - elif text == '.': - if '.' not in current.split('[-+×÷]')[-1]: - self.calc_display.setText(current + text) + if is_number: + font_size = "18px" + font_weight = "normal" else: - # Number - if current == "0": - self.calc_display.setText(text) + font_size = "14px" + font_weight = "normal" + + btn.setStyleSheet(f""" + QPushButton {{ + background-color: {bg_color}; + color: {text_color}; + font-size: {font_size}; + font-weight: {font_weight}; + border: none; + border-radius: 4px; + }} + QPushButton:hover {{ + background-color: {self._lighten(bg_color)}; + }} + QPushButton:pressed {{ + background-color: {self._darken(bg_color)}; + }} + """) + + return btn + + def _lighten(self, color): + """Lighten a hex color slightly.""" + # Simple approximation - increase each component + if color == "#2a2a2a": + return "#3a3a3a" + elif color == "#3a3a3a": + return "#4a4a4a" + elif color == "#0078d4": + return "#1084e0" + return color + + def _darken(self, color): + """Darken a hex color slightly.""" + if color == "#2a2a2a": + return "#1a1a1a" + elif color == "#3a3a3a": + return "#2a2a2a" + elif color == "#0078d4": + return "#006cbd" + return color + + def _on_number(self, num): + """Handle number button press.""" + if self.start_new: + self.current_value = num + self.start_new = False + else: + if self.current_value == "0": + self.current_value = num else: - self.calc_display.setText(current + text) + self.current_value += num + self._update_display() - def _convert_ped(self): - """Convert PED to other units.""" - ped = self.ped_input.value() - pec = ped * 1000 - usd = ped * 0.1 # Approximate - - self.pec_result.setText(f"= {pec:,.0f} PEC") - self.usd_result.setText(f"≈ ${usd:.2f} USD") + def _on_decimal(self): + """Handle decimal point.""" + if self.start_new: + self.current_value = "0." + self.start_new = False + elif "." not in self.current_value: + self.current_value += "." + self._update_display() - def _calc_dpp(self): - """Calculate DPP (Damage Per Pec).""" - damage = self.dmg_input.value() - cost = self.cost_input.value() + def _on_operator(self, op): + """Handle operator button.""" + try: + current = float(self.current_value) + + if op == "1/x": + result = 1 / current + self.current_value = self._format_result(result) + self.start_new = True + elif op == "sq": + result = current ** 2 + self.current_value = self._format_result(result) + self.start_new = True + elif op == "sqrt": + import math + result = math.sqrt(current) + self.current_value = self._format_result(result) + self.start_new = True + else: + # Binary operators + if self.pending_op and not self.start_new: + self._calculate() + + self.stored_value = float(self.current_value) + self.pending_op = op + self.start_new = True + + self._update_display() + except Exception: + self.current_value = "Error" + self._update_display() + self.start_new = True + + def _on_special(self, op): + """Handle special buttons (%, CE, C, backspace).""" + if op == 'C': + # Clear all + self.current_value = "0" + self.stored_value = None + self.pending_op = None + self.start_new = True + elif op == 'CE': + # Clear entry + self.current_value = "0" + self.start_new = True + elif op == '⌫': + # Backspace + if len(self.current_value) > 1: + self.current_value = self.current_value[:-1] + else: + self.current_value = "0" + elif op == '%': + # Percent + try: + result = float(self.current_value) / 100 + self.current_value = self._format_result(result) + self.start_new = True + except: + self.current_value = "Error" + self.start_new = True - if cost > 0: - dpp = damage / cost - self.dpp_result.setText(f"DPP: {dpp:.2f}") - else: - self.dpp_result.setText("DPP: ∞") + self._update_display() + + def _on_memory(self, op): + """Handle memory operations.""" + try: + current = float(self.current_value) + + if op == 'MC': + self.memory = 0 + elif op == 'MR': + self.current_value = self._format_result(self.memory) + self.start_new = True + elif op == 'M+': + self.memory += current + elif op == 'M-': + self.memory -= current + elif op == 'MS': + self.memory = current + elif op == 'M~': + # Memory clear (same as MC) + self.memory = 0 + + self._update_display() + except: + pass + + def _on_negate(self): + """Toggle sign.""" + try: + current = float(self.current_value) + result = -current + self.current_value = self._format_result(result) + self._update_display() + except: + pass + + def _on_equals(self): + """Calculate result.""" + self._calculate() + self.pending_op = None + self.stored_value = None + self.start_new = True + + def _calculate(self): + """Perform pending calculation.""" + if self.pending_op and self.stored_value is not None: + try: + current = float(self.current_value) + + if self.pending_op == '+': + result = self.stored_value + current + elif self.pending_op == '-': + result = self.stored_value - current + elif self.pending_op == '*': + result = self.stored_value * current + elif self.pending_op == '÷': + if current != 0: + result = self.stored_value / current + else: + result = "Error" + else: + return + + self.current_value = self._format_result(result) + self._update_display() + except: + self.current_value = "Error" + self._update_display() + + def _format_result(self, result): + """Format calculation result.""" + if isinstance(result, str): + return result + + # Check if it's essentially an integer + if result == int(result): + return str(int(result)) + + # Format with reasonable precision + formatted = f"{result:.10f}" + # Remove trailing zeros + formatted = formatted.rstrip('0').rstrip('.') + + # Limit length + if len(formatted) > 12: + formatted = f"{result:.6e}" + + return formatted + + def _update_display(self): + """Update the display.""" + self.display.setText(self.current_value) def on_hotkey(self): """Focus calculator when hotkey pressed.""" - pass # Calculator doesn't need focus action + pass diff --git a/projects/EU-Utility/plugins/spotify_controller/plugin.py b/projects/EU-Utility/plugins/spotify_controller/plugin.py index bad3523..f70a2ed 100644 --- a/projects/EU-Utility/plugins/spotify_controller/plugin.py +++ b/projects/EU-Utility/plugins/spotify_controller/plugin.py @@ -1,7 +1,7 @@ """ EU-Utility - Spotify Controller Plugin -Control Spotify playback from the overlay. +Control Spotify playback and display current track info. """ import subprocess @@ -10,100 +10,246 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QProgressBar ) -from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal from plugins.base_plugin import BasePlugin +class SpotifyInfoThread(QThread): + """Background thread to fetch Spotify info.""" + info_ready = pyqtSignal(dict) + error = pyqtSignal(str) + + def __init__(self, system): + super().__init__() + self.system = system + + def run(self): + """Fetch Spotify info.""" + try: + if self.system == "Linux": + result = subprocess.run( + ['playerctl', '--player=spotify', 'metadata', '--format', + '{{title}}|{{artist}}|{{album}}|{{position}}|{{mpris:length}}|{{status}}'], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + parts = result.stdout.strip().split('|') + if len(parts) >= 6: + self.info_ready.emit({ + 'title': parts[0] or 'Unknown', + 'artist': parts[1] or 'Unknown Artist', + 'album': parts[2] or '', + 'position': self._parse_time(parts[3]), + 'duration': self._parse_time(parts[4]), + 'is_playing': parts[5] == 'Playing' + }) + return + + elif self.system == "Darwin": + script = ''' + tell application "Spotify" + if player state is playing then + return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Playing" + else + return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Paused" + end if + end tell + ''' + result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + parts = result.stdout.strip().split('|') + if len(parts) >= 6: + self.info_ready.emit({ + 'title': parts[0] or 'Unknown', + 'artist': parts[1] or 'Unknown Artist', + 'album': parts[2] or '', + 'position': float(parts[3]) if parts[3] else 0, + 'duration': float(parts[4]) if parts[4] else 0, + 'is_playing': parts[5] == 'Playing' + }) + return + + # Default/empty response + self.info_ready.emit({ + 'title': 'Not playing', + 'artist': '', + 'album': '', + 'position': 0, + 'duration': 0, + 'is_playing': False + }) + + except Exception as e: + self.error.emit(str(e)) + self.info_ready.emit({ + 'title': 'Not playing', + 'artist': '', + 'album': '', + 'position': 0, + 'duration': 0, + 'is_playing': False + }) + + def _parse_time(self, time_str): + """Parse time string to seconds.""" + try: + return int(time_str) / 1000000 + except: + return 0 + + class SpotifyControllerPlugin(BasePlugin): - """Control local Spotify playback.""" + """Control Spotify playback and display current track.""" name = "Spotify" - version = "1.0.0" + version = "1.1.0" author = "ImpulsiveFPS" - description = "Control Spotify playback on your PC" - hotkey = "ctrl+shift+m" # M for Music + description = "Control Spotify and view current track info" + hotkey = "ctrl+shift+m" def initialize(self): """Setup Spotify controller.""" self.system = platform.system() self.update_timer = None - self.current_track = "Not playing" - self.is_playing = False + self.info_thread = None + self.current_info = { + 'title': 'Not playing', + 'artist': '', + 'album': '', + 'position': 0, + 'duration': 0, + 'is_playing': False + } def get_ui(self): """Create Spotify controller UI.""" widget = QWidget() layout = QVBoxLayout(widget) + layout.setSpacing(12) # Title - title = QLabel("🎵 Spotify Controller") + title = QLabel("🎵 Spotify") title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;") layout.addWidget(title) - # Track info + # Album Art Placeholder + self.album_art = QLabel("💿") + self.album_art.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.album_art.setStyleSheet(""" + QLabel { + background-color: #282828; + border-radius: 8px; + font-size: 64px; + padding: 20px; + min-height: 120px; + } + """) + layout.addWidget(self.album_art) + + # Track info container + info_container = QWidget() + info_container.setStyleSheet(""" + QWidget { + background-color: #1a1a1a; + border-radius: 8px; + padding: 12px; + } + """) + info_layout = QVBoxLayout(info_container) + info_layout.setSpacing(4) + + # Track title self.track_label = QLabel("Not playing") self.track_label.setStyleSheet(""" color: white; - font-size: 14px; + font-size: 16px; font-weight: bold; - padding: 10px; - background-color: #222; - border-radius: 4px; """) self.track_label.setWordWrap(True) - layout.addWidget(self.track_label) + info_layout.addWidget(self.track_label) # Artist self.artist_label = QLabel("") - self.artist_label.setStyleSheet("color: #aaa; font-size: 12px;") - layout.addWidget(self.artist_label) + self.artist_label.setStyleSheet(""" + color: #b3b3b3; + font-size: 13px; + """) + info_layout.addWidget(self.artist_label) - # Progress bar (decorative - doesn't show actual progress) + # Album + self.album_label = QLabel("") + self.album_label.setStyleSheet(""" + color: #666; + font-size: 11px; + """) + info_layout.addWidget(self.album_label) + + layout.addWidget(info_container) + + # Time info + time_layout = QHBoxLayout() + self.position_label = QLabel("0:00") + self.position_label.setStyleSheet("color: #888; font-size: 11px;") + time_layout.addWidget(self.position_label) + + time_layout.addStretch() + + self.duration_label = QLabel("0:00") + self.duration_label.setStyleSheet("color: #888; font-size: 11px;") + time_layout.addWidget(self.duration_label) + + layout.addLayout(time_layout) + + # Progress bar self.progress = QProgressBar() self.progress.setRange(0, 100) self.progress.setValue(0) self.progress.setTextVisible(False) self.progress.setStyleSheet(""" QProgressBar { - background-color: #333; + background-color: #404040; border: none; height: 4px; + border-radius: 2px; } QProgressBar::chunk { background-color: #1DB954; + border-radius: 2px; } """) layout.addWidget(self.progress) # Control buttons btn_layout = QHBoxLayout() - btn_layout.setSpacing(20) + btn_layout.setSpacing(15) + btn_layout.addStretch() # Previous prev_btn = QPushButton("⏮") prev_btn.setFixedSize(50, 50) - prev_btn.setStyleSheet(self._get_button_style("#666")) + prev_btn.setStyleSheet(self._get_button_style("#404040")) prev_btn.clicked.connect(self._previous_track) btn_layout.addWidget(prev_btn) # Play/Pause self.play_btn = QPushButton("▶") self.play_btn.setFixedSize(60, 60) - self.play_btn.setStyleSheet(self._get_button_style("#1DB954", "20px")) + self.play_btn.setStyleSheet(self._get_play_button_style()) self.play_btn.clicked.connect(self._toggle_playback) btn_layout.addWidget(self.play_btn) # Next next_btn = QPushButton("⏭") next_btn.setFixedSize(50, 50) - next_btn.setStyleSheet(self._get_button_style("#666")) + next_btn.setStyleSheet(self._get_button_style("#404040")) next_btn.clicked.connect(self._next_track) btn_layout.addWidget(next_btn) + btn_layout.addStretch() layout.addLayout(btn_layout) - # Volume slider + # Volume volume_layout = QHBoxLayout() volume_layout.addWidget(QLabel("🔈")) @@ -112,19 +258,19 @@ class SpotifyControllerPlugin(BasePlugin): self.volume_slider.setValue(50) self.volume_slider.setStyleSheet(""" QSlider::groove:horizontal { - background: #333; - height: 8px; - border-radius: 4px; + background: #404040; + height: 4px; + border-radius: 2px; } QSlider::handle:horizontal { - background: #1DB954; - width: 18px; - margin: -5px 0; - border-radius: 9px; + background: #fff; + width: 12px; + margin: -4px 0; + border-radius: 6px; } QSlider::sub-page:horizontal { background: #1DB954; - border-radius: 4px; + border-radius: 2px; } """) self.volume_slider.valueChanged.connect(self._set_volume) @@ -135,14 +281,10 @@ class SpotifyControllerPlugin(BasePlugin): # Status self.status_label = QLabel("Click play to control Spotify") - self.status_label.setStyleSheet("color: #666; font-size: 11px; margin-top: 10px;") + self.status_label.setStyleSheet("color: #666; font-size: 10px;") + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.status_label) - # Install tip - tip = QLabel("💡 Tip: Ensure Spotify is running") - tip.setStyleSheet("color: #444; font-size: 10px;") - layout.addWidget(tip) - layout.addStretch() # Start update timer @@ -150,89 +292,119 @@ class SpotifyControllerPlugin(BasePlugin): return widget - def _get_button_style(self, color, font_size="16px"): + def _get_button_style(self, color): """Get button stylesheet.""" return f""" QPushButton {{ background-color: {color}; color: white; - font-size: {font_size}; - font-weight: bold; + font-size: 18px; border: none; border-radius: 25px; }} QPushButton:hover {{ - background-color: {self._lighten_color(color)}; + background-color: #505050; }} QPushButton:pressed {{ - background-color: {self._darken_color(color)}; + background-color: #303030; }} """ - def _lighten_color(self, hex_color): - """Lighten color for hover effect.""" - # Simple approximation - if hex_color == "#1DB954": - return "#1ed760" - return "#777" - - def _darken_color(self, hex_color): - """Darken color for press effect.""" - if hex_color == "#1DB954": - return "#1aa34a" - return "#555" + def _get_play_button_style(self): + """Get play button style (green).""" + return """ + QPushButton { + background-color: #1DB954; + color: white; + font-size: 22px; + border: none; + border-radius: 30px; + } + QPushButton:hover { + background-color: #1ed760; + } + QPushButton:pressed { + background-color: #1aa34a; + } + """ def _start_timer(self): """Start status update timer.""" self.update_timer = QTimer() - self.update_timer.timeout.connect(self._update_status) - self.update_timer.start(2000) # Update every 2 seconds + self.update_timer.timeout.connect(self._fetch_spotify_info) + self.update_timer.start(1000) - def _update_status(self): - """Update playback status.""" - # In a real implementation, this would query Spotify's API - # For now, just show a placeholder - pass + def _fetch_spotify_info(self): + """Fetch Spotify info in background.""" + if self.info_thread and self.info_thread.isRunning(): + return + + self.info_thread = SpotifyInfoThread(self.system) + self.info_thread.info_ready.connect(self._update_ui) + self.info_thread.start() + + def _update_ui(self, info): + """Update UI with Spotify info.""" + self.current_info = info + + # Update track info + self.track_label.setText(info.get('title', 'Unknown')) + self.artist_label.setText(info.get('artist', '')) + self.album_label.setText(info.get('album', '')) + + # Update play button + is_playing = info.get('is_playing', False) + self.play_btn.setText("⏸" if is_playing else "▶") + + # Update time + position = info.get('position', 0) + duration = info.get('duration', 0) + + self.position_label.setText(self._format_time(position)) + self.duration_label.setText(self._format_time(duration)) + + # Update progress bar + if duration > 0: + progress = int((position / duration) * 100) + self.progress.setValue(progress) + else: + self.progress.setValue(0) + + def _format_time(self, seconds): + """Format seconds to mm:ss.""" + try: + minutes = int(seconds) // 60 + secs = int(seconds) % 60 + return f"{minutes}:{secs:02d}" + except: + return "0:00" def _send_media_key(self, key): """Send media key press to system.""" try: if self.system == "Windows": - # Use Windows media keys import ctypes - # VK_MEDIA_PLAY_PAUSE = 0xB3 - # VK_MEDIA_NEXT_TRACK = 0xB0 - # VK_MEDIA_PREV_TRACK = 0xB1 - # VK_VOLUME_UP = 0xAF - # VK_VOLUME_DOWN = 0xAE - # VK_VOLUME_MUTE = 0xAD - key_codes = { 'play': 0xB3, 'next': 0xB0, 'prev': 0xB1, - 'vol_up': 0xAF, - 'vol_down': 0xAE, } - if key in key_codes: ctypes.windll.user32.keybd_event(key_codes[key], 0, 0, 0) ctypes.windll.user32.keybd_event(key_codes[key], 0, 2, 0) return True elif self.system == "Linux": - # Use playerctl or dbus cmd_map = { - 'play': ['playerctl', 'play-pause'], - 'next': ['playerctl', 'next'], - 'prev': ['playerctl', 'previous'], + 'play': ['playerctl', '--player=spotify', 'play-pause'], + 'next': ['playerctl', '--player=spotify', 'next'], + 'prev': ['playerctl', '--player=spotify', 'previous'], } if key in cmd_map: subprocess.run(cmd_map[key], capture_output=True) return True - elif self.system == "Darwin": # macOS - # Use osascript + elif self.system == "Darwin": cmd_map = { 'play': ['osascript', '-e', 'tell application "Spotify" to playpause'], 'next': ['osascript', '-e', 'tell application "Spotify" to next track'], @@ -250,9 +422,8 @@ class SpotifyControllerPlugin(BasePlugin): def _toggle_playback(self): """Toggle play/pause.""" if self._send_media_key('play'): - self.is_playing = not self.is_playing - self.play_btn.setText("⏸" if self.is_playing else "▶") - self.track_label.setText("Playing..." if self.is_playing else "Paused") + self.current_info['is_playing'] = not self.current_info.get('is_playing', False) + self.play_btn.setText("⏸" if self.current_info['is_playing'] else "▶") self.status_label.setText("Command sent to Spotify") else: self.status_label.setText("❌ Could not control Spotify") @@ -274,14 +445,8 @@ class SpotifyControllerPlugin(BasePlugin): def _set_volume(self, value): """Set volume (0-100).""" try: - if self.system == "Windows": - # Use Windows volume control - # This controls system volume, not Spotify specifically - pass # Would need pycaw library for precise control - elif self.system == "Linux": - subprocess.run(['playerctl', 'volume', str(value / 100)], capture_output=True) - elif self.system == "Darwin": - pass # Would need specific implementation + if self.system == "Linux": + subprocess.run(['playerctl', '--player=spotify', 'volume', str(value / 100)], capture_output=True) except: pass @@ -298,8 +463,11 @@ class SpotifyControllerPlugin(BasePlugin): """Restart timer when overlay shown.""" if self.update_timer: self.update_timer.start() + self._fetch_spotify_info() def shutdown(self): """Cleanup.""" if self.update_timer: self.update_timer.stop() + if self.info_thread and self.info_thread.isRunning(): + self.info_thread.wait()