Add v0.9.5 features: minimize to tray, settings, relay support
## New Features ### Minimize to Tray - Window minimizes to system tray instead of taskbar - Tray notification shown when minimized - Double-click tray icon to restore ### Settings System - New settings dialog (Edit > Settings or Ctrl+,) - JSON-based settings persistence - General tab: minimize to tray toggle - Relay Server tab: enable/configure relay connection ### Relay Server Support - New relay_client.py for connecting to relay server - WebSocket client with auto-reconnection - Forwards API requests to local server - Updates QR code/URL when relay connected ### PWA Updates - Added relay mode detection and authentication - Password passed via header for API requests - WebSocket authentication for relay connections - Desktop status handling (connected/disconnected) - Wake lock icon now always visible with status indicator ## Files Added - gui/settings_manager.py - gui/settings_dialog.py - relay_client.py ## Dependencies - Added aiohttp>=3.9.0 for relay client 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,10 @@ import sys
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
# Windows startup management
|
||||
if sys.platform == 'win32':
|
||||
import winreg
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""Get the path to a bundled resource file."""
|
||||
@@ -20,12 +24,13 @@ from PySide6.QtWidgets import (
|
||||
QScrollArea, QFrame, QMenu, QMenuBar, QStatusBar,
|
||||
QMessageBox, QApplication, QSystemTrayIcon, QStyle
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QTimer, QSize
|
||||
from PySide6.QtCore import Qt, Signal, QTimer, QSize, QEvent
|
||||
from PySide6.QtGui import QIcon, QPixmap, QAction, QFont
|
||||
|
||||
from config import VERSION, THEME, DEFAULT_PORT
|
||||
from config import VERSION, THEME, DEFAULT_PORT, SETTINGS_FILE
|
||||
from macro_manager import MacroManager
|
||||
from web_server import WebServer
|
||||
from .settings_manager import SettingsManager
|
||||
|
||||
|
||||
class MacroButton(QPushButton):
|
||||
@@ -123,6 +128,10 @@ class MainWindow(QMainWindow):
|
||||
self.current_tab = "All"
|
||||
self.sort_by = "name"
|
||||
|
||||
# Initialize settings manager
|
||||
settings_file = os.path.join(app_dir, SETTINGS_FILE)
|
||||
self.settings_manager = SettingsManager(settings_file)
|
||||
|
||||
# Initialize macro manager
|
||||
data_file = os.path.join(app_dir, "macros.json")
|
||||
images_dir = os.path.join(app_dir, "macro_images")
|
||||
@@ -134,6 +143,9 @@ class MainWindow(QMainWindow):
|
||||
self.web_server = WebServer(self.macro_manager, app_dir, DEFAULT_PORT)
|
||||
self.server_thread = None
|
||||
|
||||
# Relay client (initialized later if enabled)
|
||||
self.relay_client = None
|
||||
|
||||
# Setup UI
|
||||
self.setup_ui()
|
||||
self.setup_menu()
|
||||
@@ -142,6 +154,10 @@ class MainWindow(QMainWindow):
|
||||
# Start web server
|
||||
self.start_server()
|
||||
|
||||
# Start relay client if enabled
|
||||
if self.settings_manager.get_relay_enabled():
|
||||
self.start_relay_client()
|
||||
|
||||
# Connect signals
|
||||
self.macros_changed.connect(self.refresh_macros)
|
||||
|
||||
@@ -275,11 +291,28 @@ class MainWindow(QMainWindow):
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
# Windows startup option (only on Windows)
|
||||
if sys.platform == 'win32':
|
||||
self.startup_action = QAction("Start on Windows Startup", self)
|
||||
self.startup_action.setCheckable(True)
|
||||
self.startup_action.setChecked(self.get_startup_enabled())
|
||||
self.startup_action.triggered.connect(self.toggle_startup)
|
||||
file_menu.addAction(self.startup_action)
|
||||
file_menu.addSeparator()
|
||||
|
||||
quit_action = QAction("Quit", self)
|
||||
quit_action.setShortcut("Ctrl+Q")
|
||||
quit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(quit_action)
|
||||
|
||||
# Edit menu
|
||||
edit_menu = menubar.addMenu("Edit")
|
||||
|
||||
settings_action = QAction("Settings...", self)
|
||||
settings_action.setShortcut("Ctrl+,")
|
||||
settings_action.triggered.connect(self.show_settings)
|
||||
edit_menu.addAction(settings_action)
|
||||
|
||||
# View menu
|
||||
view_menu = menubar.addMenu("View")
|
||||
|
||||
@@ -379,6 +412,18 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def update_ip_label(self):
|
||||
"""Update the IP address label."""
|
||||
# Check if relay is connected and has a session ID
|
||||
if self.relay_client and self.relay_client.is_connected():
|
||||
session_id = self.settings_manager.get_relay_session_id()
|
||||
if session_id:
|
||||
relay_url = self.settings_manager.get_relay_url()
|
||||
# Convert wss:// to https:// for display
|
||||
base_url = relay_url.replace('wss://', 'https://').replace('ws://', 'http://')
|
||||
base_url = base_url.replace('/desktop', '').rstrip('/')
|
||||
self.ip_label.setText(f"{base_url}/{session_id}")
|
||||
return
|
||||
|
||||
# Fall back to local IP
|
||||
try:
|
||||
import netifaces
|
||||
for iface in netifaces.interfaces():
|
||||
@@ -555,8 +600,149 @@ class MainWindow(QMainWindow):
|
||||
about_box.setStandardButtons(QMessageBox.Ok)
|
||||
about_box.exec()
|
||||
|
||||
def show_settings(self):
|
||||
"""Show settings dialog."""
|
||||
from .settings_dialog import SettingsDialog
|
||||
dialog = SettingsDialog(self.settings_manager, parent=self)
|
||||
dialog.relay_settings_changed.connect(self.on_relay_settings_changed)
|
||||
dialog.exec_()
|
||||
|
||||
def on_relay_settings_changed(self):
|
||||
"""Handle relay settings changes."""
|
||||
# Stop existing relay client if running
|
||||
self.stop_relay_client()
|
||||
|
||||
# Start new relay client if enabled
|
||||
if self.settings_manager.get_relay_enabled():
|
||||
self.start_relay_client()
|
||||
|
||||
# Update IP label to show relay URL if connected
|
||||
self.update_ip_label()
|
||||
|
||||
def start_relay_client(self):
|
||||
"""Start the relay client in a background thread."""
|
||||
try:
|
||||
from relay_client import RelayClient
|
||||
except ImportError:
|
||||
self.status_bar.showMessage("Relay client not available", 3000)
|
||||
return
|
||||
|
||||
url = self.settings_manager.get_relay_url()
|
||||
password = self.settings_manager.get_relay_password()
|
||||
session_id = self.settings_manager.get_relay_session_id()
|
||||
|
||||
if not url or not password:
|
||||
self.status_bar.showMessage("Relay not configured", 3000)
|
||||
return
|
||||
|
||||
self.relay_client = RelayClient(
|
||||
relay_url=url,
|
||||
password=password,
|
||||
session_id=session_id,
|
||||
local_port=DEFAULT_PORT,
|
||||
on_connected=self.on_relay_connected,
|
||||
on_disconnected=self.on_relay_disconnected,
|
||||
on_session_id=self.on_relay_session_id
|
||||
)
|
||||
self.relay_client.start()
|
||||
self.status_bar.showMessage("Connecting to relay server...")
|
||||
|
||||
def stop_relay_client(self):
|
||||
"""Stop the relay client."""
|
||||
if self.relay_client:
|
||||
self.relay_client.stop()
|
||||
self.relay_client = None
|
||||
self.status_bar.showMessage("Relay disconnected", 2000)
|
||||
|
||||
def on_relay_connected(self):
|
||||
"""Handle relay connection established."""
|
||||
QTimer.singleShot(0, lambda: self._update_relay_status(True))
|
||||
|
||||
def on_relay_disconnected(self):
|
||||
"""Handle relay disconnection."""
|
||||
QTimer.singleShot(0, lambda: self._update_relay_status(False))
|
||||
|
||||
def on_relay_session_id(self, session_id: str):
|
||||
"""Handle receiving session ID from relay."""
|
||||
self.settings_manager.set_relay_session_id(session_id)
|
||||
QTimer.singleShot(0, self.update_ip_label)
|
||||
|
||||
def _update_relay_status(self, connected: bool):
|
||||
"""Update UI for relay status (called on main thread)."""
|
||||
if connected:
|
||||
self.status_bar.showMessage("Connected to relay server")
|
||||
else:
|
||||
self.status_bar.showMessage("Relay disconnected - reconnecting...")
|
||||
self.update_ip_label()
|
||||
|
||||
# Windows startup management
|
||||
def get_startup_enabled(self) -> bool:
|
||||
"""Check if app is set to start on Windows startup."""
|
||||
if sys.platform != 'win32':
|
||||
return False
|
||||
try:
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
||||
0, winreg.KEY_READ
|
||||
)
|
||||
try:
|
||||
winreg.QueryValueEx(key, "MacroPad Server")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
finally:
|
||||
winreg.CloseKey(key)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def set_startup_enabled(self, enabled: bool):
|
||||
"""Enable or disable starting on Windows startup."""
|
||||
if sys.platform != 'win32':
|
||||
return
|
||||
|
||||
try:
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
||||
0, winreg.KEY_SET_VALUE
|
||||
)
|
||||
try:
|
||||
if enabled:
|
||||
# Get the executable path
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as compiled executable
|
||||
exe_path = sys.executable
|
||||
else:
|
||||
# Running as script - use pythonw to avoid console
|
||||
exe_path = f'"{sys.executable}" "{os.path.abspath(sys.argv[0])}"'
|
||||
|
||||
winreg.SetValueEx(key, "MacroPad Server", 0, winreg.REG_SZ, exe_path)
|
||||
self.status_bar.showMessage("Added to Windows startup", 3000)
|
||||
else:
|
||||
try:
|
||||
winreg.DeleteValue(key, "MacroPad Server")
|
||||
self.status_bar.showMessage("Removed from Windows startup", 3000)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
finally:
|
||||
winreg.CloseKey(key)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Error", f"Failed to update startup settings: {e}")
|
||||
|
||||
def toggle_startup(self):
|
||||
"""Toggle the startup setting."""
|
||||
current = self.get_startup_enabled()
|
||||
self.set_startup_enabled(not current)
|
||||
# Update menu checkmark
|
||||
if hasattr(self, 'startup_action'):
|
||||
self.startup_action.setChecked(not current)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close."""
|
||||
# Stop the relay client
|
||||
self.stop_relay_client()
|
||||
|
||||
# Stop the web server
|
||||
self.stop_server()
|
||||
|
||||
@@ -569,3 +755,19 @@ class MainWindow(QMainWindow):
|
||||
"""Handle window resize."""
|
||||
super().resizeEvent(event)
|
||||
self.refresh_macros()
|
||||
|
||||
def changeEvent(self, event):
|
||||
"""Handle window state changes - minimize to tray."""
|
||||
if event.type() == QEvent.Type.WindowStateChange:
|
||||
if self.windowState() & Qt.WindowMinimized:
|
||||
# Hide instead of minimize (goes to tray)
|
||||
event.ignore()
|
||||
self.hide()
|
||||
self.tray_icon.showMessage(
|
||||
"MacroPad Server",
|
||||
"Running in system tray. Double-click to restore.",
|
||||
QSystemTrayIcon.Information,
|
||||
2000
|
||||
)
|
||||
return
|
||||
super().changeEvent(event)
|
||||
|
||||
Reference in New Issue
Block a user