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:
parent
7c05691e14
commit
0b34cea4d7
|
|
@ -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:
|
# Row 1: Memory buttons
|
||||||
continue
|
mem_buttons = ['MC', 'MR', 'M+', 'M-', 'MS', 'M~']
|
||||||
|
for i, btn_text in enumerate(mem_buttons):
|
||||||
btn = QPushButton(btn_text)
|
btn = self._create_button(btn_text, "#3a3a3a")
|
||||||
btn.setFixedSize(60, 50)
|
btn.clicked.connect(lambda checked, t=btn_text: self._on_memory(t))
|
||||||
|
grid.addWidget(btn, 0, i)
|
||||||
# Style based on button type
|
|
||||||
if btn_text in ['C', '⌫']:
|
# Row 2: %, CE, C, ⌫
|
||||||
style = """
|
row2 = ['%', 'CE', 'C', '⌫']
|
||||||
QPushButton {
|
for i, btn_text in enumerate(row2):
|
||||||
background-color: #c44;
|
btn = self._create_button(btn_text, "#3a3a3a")
|
||||||
color: white;
|
btn.clicked.connect(lambda checked, t=btn_text: self._on_special(t))
|
||||||
font-size: 16px;
|
grid.addWidget(btn, 1, i)
|
||||||
font-weight: bold;
|
|
||||||
border: none;
|
# Row 3: ¹/ₓ, x², ²√x, ÷
|
||||||
border-radius: 4px;
|
row3 = [('¹/ₓ', '1/x'), ('x²', 'sq'), ('²√x', 'sqrt'), '÷']
|
||||||
}
|
for i, item in enumerate(row3):
|
||||||
QPushButton:hover { background-color: #d55; }
|
if isinstance(item, tuple):
|
||||||
"""
|
text, op = item
|
||||||
elif btn_text in ['÷', '×', '-', '+', '=', '%']:
|
else:
|
||||||
style = """
|
text = op = item
|
||||||
QPushButton {
|
btn = self._create_button(text, "#3a3a3a")
|
||||||
background-color: #4a9eff;
|
btn.clicked.connect(lambda checked, o=op: self._on_operator(o))
|
||||||
color: white;
|
grid.addWidget(btn, 2, i)
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
# Row 4: 7, 8, 9, ×
|
||||||
border: none;
|
row4 = ['7', '8', '9', '×']
|
||||||
border-radius: 4px;
|
for i, btn_text in enumerate(row4):
|
||||||
}
|
btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['×'])
|
||||||
QPushButton:hover { background-color: #5aafff; }
|
if btn_text == '×':
|
||||||
"""
|
btn.clicked.connect(lambda checked: self._on_operator('*'))
|
||||||
else:
|
else:
|
||||||
style = """
|
btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t))
|
||||||
QPushButton {
|
grid.addWidget(btn, 3, i)
|
||||||
background-color: #444;
|
|
||||||
color: white;
|
# Row 5: 4, 5, 6, -
|
||||||
font-size: 18px;
|
row5 = ['4', '5', '6', '-']
|
||||||
border: none;
|
for i, btn_text in enumerate(row5):
|
||||||
border-radius: 4px;
|
btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['-'])
|
||||||
}
|
if btn_text == '-':
|
||||||
QPushButton:hover { background-color: #555; }
|
btn.clicked.connect(lambda checked: self._on_operator('-'))
|
||||||
"""
|
else:
|
||||||
|
btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t))
|
||||||
btn.setStyleSheet(style)
|
grid.addWidget(btn, 4, i)
|
||||||
btn.clicked.connect(lambda checked, t=btn_text: self._on_calc_button(t))
|
|
||||||
grid.addWidget(btn, row, col)
|
# 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.pec_result.setText(f"= {pec:,.0f} PEC")
|
self.current_value += "."
|
||||||
self.usd_result.setText(f"≈ ${usd:.2f} USD")
|
self._update_display()
|
||||||
|
|
||||||
def _calc_dpp(self):
|
def _on_operator(self, op):
|
||||||
"""Calculate DPP (Damage Per Pec)."""
|
"""Handle operator button."""
|
||||||
damage = self.dmg_input.value()
|
try:
|
||||||
cost = self.cost_input.value()
|
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:
|
self._update_display()
|
||||||
dpp = damage / cost
|
|
||||||
self.dpp_result.setText(f"DPP: {dpp:.2f}")
|
def _on_memory(self, op):
|
||||||
else:
|
"""Handle memory operations."""
|
||||||
self.dpp_result.setText("DPP: ∞")
|
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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue