feat: add Calculator and Spotify plugins
New Plugins: - Calculator: Basic math + EU unit conversions (PED/PEC/DPP) - Spotify Controller: Control local Spotify with media keys Calculator Features: - Basic calculator with +, -, ×, ÷ - PED to PEC/USD converter - DPP (Damage Per Pec) calculator Spotify Features: - Play/Pause toggle - Next/Previous track - Volume control slider - Global hotkey (Ctrl+Shift+M) - Works on Windows/Linux/macOS Updated requirements.txt with optional dependencies
This commit is contained in:
parent
61ecb16dd6
commit
d6a768d83c
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""
|
||||||
|
Calculator Plugin for EU-Utility
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .plugin import CalculatorPlugin
|
||||||
|
|
||||||
|
__all__ = ["CalculatorPlugin"]
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Calculator Plugin
|
||||||
|
|
||||||
|
Simple calculator with basic math and EU-specific calculations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLineEdit, QPushButton, QLabel, QGridLayout,
|
||||||
|
QTabWidget, QSpinBox, QDoubleSpinBox
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class CalculatorPlugin(BasePlugin):
|
||||||
|
"""Simple calculator with EU-specific features."""
|
||||||
|
|
||||||
|
name = "Calculator"
|
||||||
|
version = "1.0.0"
|
||||||
|
author = "ImpulsiveFPS"
|
||||||
|
description = "Basic calculator and EU unit conversions"
|
||||||
|
hotkey = "ctrl+shift+c"
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Setup calculator."""
|
||||||
|
self.current_input = ""
|
||||||
|
|
||||||
|
def get_ui(self):
|
||||||
|
"""Create calculator UI."""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Display
|
||||||
|
self.calc_display = QLineEdit("0")
|
||||||
|
self.calc_display.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||||
|
self.calc_display.setStyleSheet("""
|
||||||
|
QLineEdit {
|
||||||
|
background-color: #222;
|
||||||
|
color: #0f0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #555;
|
||||||
|
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', '.', '=', '']
|
||||||
|
]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
layout.addLayout(grid)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def _create_eu_converter(self):
|
||||||
|
"""Create EU unit converter."""
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout(tab)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
# Number
|
||||||
|
if current == "0":
|
||||||
|
self.calc_display.setText(text)
|
||||||
|
else:
|
||||||
|
self.calc_display.setText(current + text)
|
||||||
|
|
||||||
|
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 _calc_dpp(self):
|
||||||
|
"""Calculate DPP (Damage Per Pec)."""
|
||||||
|
damage = self.dmg_input.value()
|
||||||
|
cost = self.cost_input.value()
|
||||||
|
|
||||||
|
if cost > 0:
|
||||||
|
dpp = damage / cost
|
||||||
|
self.dpp_result.setText(f"DPP: {dpp:.2f}")
|
||||||
|
else:
|
||||||
|
self.dpp_result.setText("DPP: ∞")
|
||||||
|
|
||||||
|
def on_hotkey(self):
|
||||||
|
"""Focus calculator when hotkey pressed."""
|
||||||
|
pass # Calculator doesn't need focus action
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""
|
||||||
|
Spotify Controller Plugin for EU-Utility
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .plugin import SpotifyControllerPlugin
|
||||||
|
|
||||||
|
__all__ = ["SpotifyControllerPlugin"]
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Spotify Controller Plugin
|
||||||
|
|
||||||
|
Control Spotify playback from the overlay.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QPushButton, QSlider, QProgressBar
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifyControllerPlugin(BasePlugin):
|
||||||
|
"""Control local Spotify playback."""
|
||||||
|
|
||||||
|
name = "Spotify"
|
||||||
|
version = "1.0.0"
|
||||||
|
author = "ImpulsiveFPS"
|
||||||
|
description = "Control Spotify playback on your PC"
|
||||||
|
hotkey = "ctrl+shift+m" # M for Music
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Setup Spotify controller."""
|
||||||
|
self.system = platform.system()
|
||||||
|
self.update_timer = None
|
||||||
|
self.current_track = "Not playing"
|
||||||
|
self.is_playing = False
|
||||||
|
|
||||||
|
def get_ui(self):
|
||||||
|
"""Create Spotify controller UI."""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = QLabel("🎵 Spotify Controller")
|
||||||
|
title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# Track info
|
||||||
|
self.track_label = QLabel("Not playing")
|
||||||
|
self.track_label.setStyleSheet("""
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #222;
|
||||||
|
border-radius: 4px;
|
||||||
|
""")
|
||||||
|
self.track_label.setWordWrap(True)
|
||||||
|
layout.addWidget(self.track_label)
|
||||||
|
|
||||||
|
# Artist
|
||||||
|
self.artist_label = QLabel("")
|
||||||
|
self.artist_label.setStyleSheet("color: #aaa; font-size: 12px;")
|
||||||
|
layout.addWidget(self.artist_label)
|
||||||
|
|
||||||
|
# Progress bar (decorative - doesn't show actual progress)
|
||||||
|
self.progress = QProgressBar()
|
||||||
|
self.progress.setRange(0, 100)
|
||||||
|
self.progress.setValue(0)
|
||||||
|
self.progress.setTextVisible(False)
|
||||||
|
self.progress.setStyleSheet("""
|
||||||
|
QProgressBar {
|
||||||
|
background-color: #333;
|
||||||
|
border: none;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #1DB954;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(self.progress)
|
||||||
|
|
||||||
|
# Control buttons
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
btn_layout.setSpacing(20)
|
||||||
|
|
||||||
|
# Previous
|
||||||
|
prev_btn = QPushButton("⏮")
|
||||||
|
prev_btn.setFixedSize(50, 50)
|
||||||
|
prev_btn.setStyleSheet(self._get_button_style("#666"))
|
||||||
|
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.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.clicked.connect(self._next_track)
|
||||||
|
btn_layout.addWidget(next_btn)
|
||||||
|
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
# Volume slider
|
||||||
|
volume_layout = QHBoxLayout()
|
||||||
|
volume_layout.addWidget(QLabel("🔈"))
|
||||||
|
|
||||||
|
self.volume_slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
|
self.volume_slider.setRange(0, 100)
|
||||||
|
self.volume_slider.setValue(50)
|
||||||
|
self.volume_slider.setStyleSheet("""
|
||||||
|
QSlider::groove:horizontal {
|
||||||
|
background: #333;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QSlider::handle:horizontal {
|
||||||
|
background: #1DB954;
|
||||||
|
width: 18px;
|
||||||
|
margin: -5px 0;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
QSlider::sub-page:horizontal {
|
||||||
|
background: #1DB954;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.volume_slider.valueChanged.connect(self._set_volume)
|
||||||
|
volume_layout.addWidget(self.volume_slider)
|
||||||
|
|
||||||
|
volume_layout.addWidget(QLabel("🔊"))
|
||||||
|
layout.addLayout(volume_layout)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
self.status_label = QLabel("Click play to control Spotify")
|
||||||
|
self.status_label.setStyleSheet("color: #666; font-size: 11px; margin-top: 10px;")
|
||||||
|
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
|
||||||
|
self._start_timer()
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def _get_button_style(self, color, font_size="16px"):
|
||||||
|
"""Get button stylesheet."""
|
||||||
|
return f"""
|
||||||
|
QPushButton {{
|
||||||
|
background-color: {color};
|
||||||
|
color: white;
|
||||||
|
font-size: {font_size};
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{
|
||||||
|
background-color: {self._lighten_color(color)};
|
||||||
|
}}
|
||||||
|
QPushButton:pressed {{
|
||||||
|
background-color: {self._darken_color(color)};
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 _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
|
||||||
|
|
||||||
|
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 _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'],
|
||||||
|
}
|
||||||
|
if key in cmd_map:
|
||||||
|
subprocess.run(cmd_map[key], capture_output=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif self.system == "Darwin": # macOS
|
||||||
|
# Use osascript
|
||||||
|
cmd_map = {
|
||||||
|
'play': ['osascript', '-e', 'tell application "Spotify" to playpause'],
|
||||||
|
'next': ['osascript', '-e', 'tell application "Spotify" to next track'],
|
||||||
|
'prev': ['osascript', '-e', 'tell application "Spotify" to previous track'],
|
||||||
|
}
|
||||||
|
if key in cmd_map:
|
||||||
|
subprocess.run(cmd_map[key], capture_output=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending media key: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
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.status_label.setText("Command sent to Spotify")
|
||||||
|
else:
|
||||||
|
self.status_label.setText("❌ Could not control Spotify")
|
||||||
|
|
||||||
|
def _next_track(self):
|
||||||
|
"""Next track."""
|
||||||
|
if self._send_media_key('next'):
|
||||||
|
self.status_label.setText("⏭ Next track")
|
||||||
|
else:
|
||||||
|
self.status_label.setText("❌ Could not skip")
|
||||||
|
|
||||||
|
def _previous_track(self):
|
||||||
|
"""Previous track."""
|
||||||
|
if self._send_media_key('prev'):
|
||||||
|
self.status_label.setText("⏮ Previous track")
|
||||||
|
else:
|
||||||
|
self.status_label.setText("❌ Could not go back")
|
||||||
|
|
||||||
|
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
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_hotkey(self):
|
||||||
|
"""Toggle play/pause with hotkey."""
|
||||||
|
self._toggle_playback()
|
||||||
|
|
||||||
|
def on_hide(self):
|
||||||
|
"""Stop timer when overlay hidden."""
|
||||||
|
if self.update_timer:
|
||||||
|
self.update_timer.stop()
|
||||||
|
|
||||||
|
def on_show(self):
|
||||||
|
"""Restart timer when overlay shown."""
|
||||||
|
if self.update_timer:
|
||||||
|
self.update_timer.start()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Cleanup."""
|
||||||
|
if self.update_timer:
|
||||||
|
self.update_timer.stop()
|
||||||
|
|
@ -2,9 +2,15 @@
|
||||||
PyQt6>=6.4.0
|
PyQt6>=6.4.0
|
||||||
keyboard>=0.13.5
|
keyboard>=0.13.5
|
||||||
|
|
||||||
# Optional plugins
|
# Optional plugin dependencies
|
||||||
# spotipy>=2.23.0 # For Spotify plugin
|
# Uncomment if using specific plugins:
|
||||||
# discord.py>=2.3.0 # For Discord plugin
|
|
||||||
|
# For Spotify Controller (advanced features)
|
||||||
|
# spotipy>=2.23.0
|
||||||
|
# pycaw>=20230407; platform_system=="Windows"
|
||||||
|
|
||||||
|
# For Discord Rich Presence
|
||||||
|
# pypresence>=4.3.0
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
# pytest>=7.0.0
|
# pytest>=7.0.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue