feat: Spotify now shows track info + Windows-style Calculator

Spotify Controller:
- Shows current track title, artist, album
- Displays position/duration time
- Progress bar updates in real-time
- Fetches info from Spotify every second
- Album art placeholder (💿)
- Volume slider with visual feedback

Calculator:
- Windows Calculator layout
- Memory buttons (MC, MR, M+, M-, MS, M~)
- Scientific functions (1/x, x², √x)
- Standard operators with proper styling
- Blue equals button like Windows
- Backspace, CE, C, % buttons
- +/- sign toggle
- Memory operations working
This commit is contained in:
LemonNexus 2026-02-13 12:57:08 +00:00
parent 7c05691e14
commit 0b34cea4d7
2 changed files with 596 additions and 342 deletions

View File

@ -1,13 +1,12 @@
""" """
EU-Utility - Calculator Plugin EU-Utility - Calculator Plugin
Simple calculator with basic math and EU-specific calculations. Standard calculator with Windows-style layout.
""" """
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QGridLayout, QLineEdit, QPushButton, QLabel, QGridLayout
QTabWidget, QSpinBox, QDoubleSpinBox
) )
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
@ -15,286 +14,373 @@ from plugins.base_plugin import BasePlugin
class CalculatorPlugin(BasePlugin): class CalculatorPlugin(BasePlugin):
"""Simple calculator with EU-specific features.""" """Standard calculator with Windows-style layout."""
name = "Calculator" name = "Calculator"
version = "1.0.0" version = "1.1.0"
author = "ImpulsiveFPS" author = "ImpulsiveFPS"
description = "Basic calculator and EU unit conversions" description = "Standard calculator"
hotkey = "ctrl+shift+c" hotkey = "ctrl+shift+c"
def initialize(self): def initialize(self):
"""Setup calculator.""" """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): def get_ui(self):
"""Create calculator UI.""" """Create calculator UI with Windows layout."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout(widget) layout = QVBoxLayout(widget)
layout.setSpacing(2)
# Tabs for different calculators # Title
tabs = QTabWidget() title = QLabel("🧮 Calculator")
tabs.setStyleSheet(""" title.setStyleSheet("color: #4a9eff; font-size: 16px; font-weight: bold;")
QTabWidget::pane { layout.addWidget(title)
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)
# Display # Display
self.calc_display = QLineEdit("0") self.display = QLineEdit("0")
self.calc_display.setAlignment(Qt.AlignmentFlag.AlignRight) self.display.setAlignment(Qt.AlignmentFlag.AlignRight)
self.calc_display.setStyleSheet(""" self.display.setStyleSheet("""
QLineEdit { QLineEdit {
background-color: #222; background-color: #1a1a1a;
color: #0f0; color: white;
font-size: 24px; font-size: 32px;
font-family: Consolas, monospace; font-family: 'Segoe UI', Arial;
padding: 10px; padding: 15px;
border: 2px solid #555; border: none;
border-radius: 4px; border-radius: 4px;
} }
""") """)
self.calc_display.setReadOnly(True) self.display.setReadOnly(True)
layout.addWidget(self.calc_display) layout.addWidget(self.display)
# Button grid
buttons = [
['C', '', '%', '÷'],
['7', '8', '9', '×'],
['4', '5', '6', '-'],
['1', '2', '3', '+'],
['0', '.', '=', '']
]
# Button grid - Windows Calculator Layout
grid = QGridLayout() grid = QGridLayout()
for row, row_buttons in enumerate(buttons): grid.setSpacing(2)
for col, btn_text in enumerate(row_buttons):
if not btn_text:
continue
btn = QPushButton(btn_text) # Row 1: Memory buttons
btn.setFixedSize(60, 50) 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)
# Style based on button type # Row 2: %, CE, C, ⌫
if btn_text in ['C', '']: row2 = ['%', 'CE', 'C', '']
style = """ for i, btn_text in enumerate(row2):
QPushButton { btn = self._create_button(btn_text, "#3a3a3a")
background-color: #c44; btn.clicked.connect(lambda checked, t=btn_text: self._on_special(t))
color: white; grid.addWidget(btn, 1, i)
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) # Row 3: ¹/ₓ, x², ²√x, ÷
btn.clicked.connect(lambda checked, t=btn_text: self._on_calc_button(t)) row3 = [('¹/ₓ', '1/x'), ('', 'sq'), ('²√x', 'sqrt'), '÷']
grid.addWidget(btn, row, col) 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.addLayout(grid)
layout.addStretch() layout.addStretch()
return tab return widget
def _create_eu_converter(self): def _create_button(self, text, bg_color, is_number=False, text_color="#ffffff"):
"""Create EU unit converter.""" """Create a calculator button."""
tab = QWidget() btn = QPushButton(text)
layout = QVBoxLayout(tab) btn.setMinimumSize(60, 45)
# Title if is_number:
title = QLabel("💰 PED Converter") font_size = "18px"
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") font_weight = "normal"
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)
else: else:
# Number font_size = "14px"
if current == "0": font_weight = "normal"
self.calc_display.setText(text)
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: else:
self.calc_display.setText(current + text) self.current_value += num
self._update_display()
def _convert_ped(self): def _on_decimal(self):
"""Convert PED to other units.""" """Handle decimal point."""
ped = self.ped_input.value() if self.start_new:
pec = ped * 1000 self.current_value = "0."
usd = ped * 0.1 # Approximate self.start_new = False
elif "." not in self.current_value:
self.current_value += "."
self._update_display()
self.pec_result.setText(f"= {pec:,.0f} PEC") def _on_operator(self, op):
self.usd_result.setText(f"≈ ${usd:.2f} USD") """Handle operator button."""
try:
current = float(self.current_value)
def _calc_dpp(self): if op == "1/x":
"""Calculate DPP (Damage Per Pec).""" result = 1 / current
damage = self.dmg_input.value() self.current_value = self._format_result(result)
cost = self.cost_input.value() 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()
if cost > 0: self.stored_value = float(self.current_value)
dpp = damage / cost self.pending_op = op
self.dpp_result.setText(f"DPP: {dpp:.2f}") self.start_new = True
else:
self.dpp_result.setText("DPP: ∞") 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
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): def on_hotkey(self):
"""Focus calculator when hotkey pressed.""" """Focus calculator when hotkey pressed."""
pass # Calculator doesn't need focus action pass

View File

@ -1,7 +1,7 @@
""" """
EU-Utility - Spotify Controller Plugin EU-Utility - Spotify Controller Plugin
Control Spotify playback from the overlay. Control Spotify playback and display current track info.
""" """
import subprocess import subprocess
@ -10,100 +10,246 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QSlider, QProgressBar QLabel, QPushButton, QSlider, QProgressBar
) )
from PyQt6.QtCore import Qt, QTimer from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin 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): class SpotifyControllerPlugin(BasePlugin):
"""Control local Spotify playback.""" """Control Spotify playback and display current track."""
name = "Spotify" name = "Spotify"
version = "1.0.0" version = "1.1.0"
author = "ImpulsiveFPS" author = "ImpulsiveFPS"
description = "Control Spotify playback on your PC" description = "Control Spotify and view current track info"
hotkey = "ctrl+shift+m" # M for Music hotkey = "ctrl+shift+m"
def initialize(self): def initialize(self):
"""Setup Spotify controller.""" """Setup Spotify controller."""
self.system = platform.system() self.system = platform.system()
self.update_timer = None self.update_timer = None
self.current_track = "Not playing" self.info_thread = None
self.is_playing = False self.current_info = {
'title': 'Not playing',
'artist': '',
'album': '',
'position': 0,
'duration': 0,
'is_playing': False
}
def get_ui(self): def get_ui(self):
"""Create Spotify controller UI.""" """Create Spotify controller UI."""
widget = QWidget() widget = QWidget()
layout = QVBoxLayout(widget) layout = QVBoxLayout(widget)
layout.setSpacing(12)
# Title # Title
title = QLabel("🎵 Spotify Controller") title = QLabel("🎵 Spotify")
title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;") title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;")
layout.addWidget(title) 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 = QLabel("Not playing")
self.track_label.setStyleSheet(""" self.track_label.setStyleSheet("""
color: white; color: white;
font-size: 14px; font-size: 16px;
font-weight: bold; font-weight: bold;
padding: 10px;
background-color: #222;
border-radius: 4px;
""") """)
self.track_label.setWordWrap(True) self.track_label.setWordWrap(True)
layout.addWidget(self.track_label) info_layout.addWidget(self.track_label)
# Artist # Artist
self.artist_label = QLabel("") self.artist_label = QLabel("")
self.artist_label.setStyleSheet("color: #aaa; font-size: 12px;") self.artist_label.setStyleSheet("""
layout.addWidget(self.artist_label) 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 = QProgressBar()
self.progress.setRange(0, 100) self.progress.setRange(0, 100)
self.progress.setValue(0) self.progress.setValue(0)
self.progress.setTextVisible(False) self.progress.setTextVisible(False)
self.progress.setStyleSheet(""" self.progress.setStyleSheet("""
QProgressBar { QProgressBar {
background-color: #333; background-color: #404040;
border: none; border: none;
height: 4px; height: 4px;
border-radius: 2px;
} }
QProgressBar::chunk { QProgressBar::chunk {
background-color: #1DB954; background-color: #1DB954;
border-radius: 2px;
} }
""") """)
layout.addWidget(self.progress) layout.addWidget(self.progress)
# Control buttons # Control buttons
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
btn_layout.setSpacing(20) btn_layout.setSpacing(15)
btn_layout.addStretch()
# Previous # Previous
prev_btn = QPushButton("") prev_btn = QPushButton("")
prev_btn.setFixedSize(50, 50) 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) prev_btn.clicked.connect(self._previous_track)
btn_layout.addWidget(prev_btn) btn_layout.addWidget(prev_btn)
# Play/Pause # Play/Pause
self.play_btn = QPushButton("") self.play_btn = QPushButton("")
self.play_btn.setFixedSize(60, 60) 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) self.play_btn.clicked.connect(self._toggle_playback)
btn_layout.addWidget(self.play_btn) btn_layout.addWidget(self.play_btn)
# Next # Next
next_btn = QPushButton("") next_btn = QPushButton("")
next_btn.setFixedSize(50, 50) 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) next_btn.clicked.connect(self._next_track)
btn_layout.addWidget(next_btn) btn_layout.addWidget(next_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout) layout.addLayout(btn_layout)
# Volume slider # Volume
volume_layout = QHBoxLayout() volume_layout = QHBoxLayout()
volume_layout.addWidget(QLabel("🔈")) volume_layout.addWidget(QLabel("🔈"))
@ -112,19 +258,19 @@ class SpotifyControllerPlugin(BasePlugin):
self.volume_slider.setValue(50) self.volume_slider.setValue(50)
self.volume_slider.setStyleSheet(""" self.volume_slider.setStyleSheet("""
QSlider::groove:horizontal { QSlider::groove:horizontal {
background: #333; background: #404040;
height: 8px; height: 4px;
border-radius: 4px; border-radius: 2px;
} }
QSlider::handle:horizontal { QSlider::handle:horizontal {
background: #1DB954; background: #fff;
width: 18px; width: 12px;
margin: -5px 0; margin: -4px 0;
border-radius: 9px; border-radius: 6px;
} }
QSlider::sub-page:horizontal { QSlider::sub-page:horizontal {
background: #1DB954; background: #1DB954;
border-radius: 4px; border-radius: 2px;
} }
""") """)
self.volume_slider.valueChanged.connect(self._set_volume) self.volume_slider.valueChanged.connect(self._set_volume)
@ -135,14 +281,10 @@ class SpotifyControllerPlugin(BasePlugin):
# Status # Status
self.status_label = QLabel("Click play to control Spotify") 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) 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() layout.addStretch()
# Start update timer # Start update timer
@ -150,89 +292,119 @@ class SpotifyControllerPlugin(BasePlugin):
return widget return widget
def _get_button_style(self, color, font_size="16px"): def _get_button_style(self, color):
"""Get button stylesheet.""" """Get button stylesheet."""
return f""" return f"""
QPushButton {{ QPushButton {{
background-color: {color}; background-color: {color};
color: white; color: white;
font-size: {font_size}; font-size: 18px;
font-weight: bold;
border: none; border: none;
border-radius: 25px; border-radius: 25px;
}} }}
QPushButton:hover {{ QPushButton:hover {{
background-color: {self._lighten_color(color)}; background-color: #505050;
}} }}
QPushButton:pressed {{ QPushButton:pressed {{
background-color: {self._darken_color(color)}; background-color: #303030;
}} }}
""" """
def _lighten_color(self, hex_color): def _get_play_button_style(self):
"""Lighten color for hover effect.""" """Get play button style (green)."""
# Simple approximation return """
if hex_color == "#1DB954": QPushButton {
return "#1ed760" background-color: #1DB954;
return "#777" color: white;
font-size: 22px;
def _darken_color(self, hex_color): border: none;
"""Darken color for press effect.""" border-radius: 30px;
if hex_color == "#1DB954": }
return "#1aa34a" QPushButton:hover {
return "#555" background-color: #1ed760;
}
QPushButton:pressed {
background-color: #1aa34a;
}
"""
def _start_timer(self): def _start_timer(self):
"""Start status update timer.""" """Start status update timer."""
self.update_timer = QTimer() self.update_timer = QTimer()
self.update_timer.timeout.connect(self._update_status) self.update_timer.timeout.connect(self._fetch_spotify_info)
self.update_timer.start(2000) # Update every 2 seconds self.update_timer.start(1000)
def _update_status(self): def _fetch_spotify_info(self):
"""Update playback status.""" """Fetch Spotify info in background."""
# In a real implementation, this would query Spotify's API if self.info_thread and self.info_thread.isRunning():
# For now, just show a placeholder return
pass
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): def _send_media_key(self, key):
"""Send media key press to system.""" """Send media key press to system."""
try: try:
if self.system == "Windows": if self.system == "Windows":
# Use Windows media keys
import ctypes 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 = { key_codes = {
'play': 0xB3, 'play': 0xB3,
'next': 0xB0, 'next': 0xB0,
'prev': 0xB1, 'prev': 0xB1,
'vol_up': 0xAF,
'vol_down': 0xAE,
} }
if key in key_codes: 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, 0, 0)
ctypes.windll.user32.keybd_event(key_codes[key], 0, 2, 0) ctypes.windll.user32.keybd_event(key_codes[key], 0, 2, 0)
return True return True
elif self.system == "Linux": elif self.system == "Linux":
# Use playerctl or dbus
cmd_map = { cmd_map = {
'play': ['playerctl', 'play-pause'], 'play': ['playerctl', '--player=spotify', 'play-pause'],
'next': ['playerctl', 'next'], 'next': ['playerctl', '--player=spotify', 'next'],
'prev': ['playerctl', 'previous'], 'prev': ['playerctl', '--player=spotify', 'previous'],
} }
if key in cmd_map: if key in cmd_map:
subprocess.run(cmd_map[key], capture_output=True) subprocess.run(cmd_map[key], capture_output=True)
return True return True
elif self.system == "Darwin": # macOS elif self.system == "Darwin":
# Use osascript
cmd_map = { cmd_map = {
'play': ['osascript', '-e', 'tell application "Spotify" to playpause'], 'play': ['osascript', '-e', 'tell application "Spotify" to playpause'],
'next': ['osascript', '-e', 'tell application "Spotify" to next track'], 'next': ['osascript', '-e', 'tell application "Spotify" to next track'],
@ -250,9 +422,8 @@ class SpotifyControllerPlugin(BasePlugin):
def _toggle_playback(self): def _toggle_playback(self):
"""Toggle play/pause.""" """Toggle play/pause."""
if self._send_media_key('play'): if self._send_media_key('play'):
self.is_playing = not self.is_playing self.current_info['is_playing'] = not self.current_info.get('is_playing', False)
self.play_btn.setText("" if self.is_playing else "") self.play_btn.setText("" if self.current_info['is_playing'] else "")
self.track_label.setText("Playing..." if self.is_playing else "Paused")
self.status_label.setText("Command sent to Spotify") self.status_label.setText("Command sent to Spotify")
else: else:
self.status_label.setText("❌ Could not control Spotify") self.status_label.setText("❌ Could not control Spotify")
@ -274,14 +445,8 @@ class SpotifyControllerPlugin(BasePlugin):
def _set_volume(self, value): def _set_volume(self, value):
"""Set volume (0-100).""" """Set volume (0-100)."""
try: try:
if self.system == "Windows": if self.system == "Linux":
# Use Windows volume control subprocess.run(['playerctl', '--player=spotify', 'volume', str(value / 100)], capture_output=True)
# 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
except: except:
pass pass
@ -298,8 +463,11 @@ class SpotifyControllerPlugin(BasePlugin):
"""Restart timer when overlay shown.""" """Restart timer when overlay shown."""
if self.update_timer: if self.update_timer:
self.update_timer.start() self.update_timer.start()
self._fetch_spotify_info()
def shutdown(self): def shutdown(self):
"""Cleanup.""" """Cleanup."""
if self.update_timer: if self.update_timer:
self.update_timer.stop() self.update_timer.stop()
if self.info_thread and self.info_thread.isRunning():
self.info_thread.wait()