## 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>
345 lines
12 KiB
Python
345 lines
12 KiB
Python
# Settings Dialog for MacroPad Server
|
|
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget,
|
|
QWidget, QLabel, QLineEdit, QCheckBox, QPushButton,
|
|
QGroupBox, QFormLayout, QMessageBox
|
|
)
|
|
from PySide6.QtCore import Qt, Signal
|
|
|
|
from config import THEME
|
|
|
|
|
|
class SettingsDialog(QDialog):
|
|
"""Settings dialog for application preferences."""
|
|
|
|
relay_settings_changed = Signal()
|
|
|
|
def __init__(self, settings_manager, parent=None):
|
|
super().__init__(parent)
|
|
self.settings_manager = settings_manager
|
|
self.setup_ui()
|
|
self.load_settings()
|
|
|
|
def setup_ui(self):
|
|
"""Setup the dialog UI."""
|
|
self.setWindowTitle("Settings")
|
|
self.setMinimumSize(500, 400)
|
|
self.setStyleSheet(f"""
|
|
QDialog {{
|
|
background-color: {THEME['bg_color']};
|
|
color: {THEME['fg_color']};
|
|
}}
|
|
QTabWidget::pane {{
|
|
border: 1px solid {THEME['highlight_color']};
|
|
background: {THEME['bg_color']};
|
|
}}
|
|
QTabBar::tab {{
|
|
background: {THEME['tab_bg']};
|
|
color: {THEME['fg_color']};
|
|
padding: 8px 16px;
|
|
margin-right: 2px;
|
|
}}
|
|
QTabBar::tab:selected {{
|
|
background: {THEME['tab_selected']};
|
|
}}
|
|
QGroupBox {{
|
|
border: 1px solid {THEME['highlight_color']};
|
|
border-radius: 4px;
|
|
margin-top: 12px;
|
|
padding-top: 8px;
|
|
color: {THEME['fg_color']};
|
|
}}
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}}
|
|
QLabel {{
|
|
color: {THEME['fg_color']};
|
|
}}
|
|
QLineEdit {{
|
|
background-color: {THEME['highlight_color']};
|
|
color: {THEME['fg_color']};
|
|
border: 1px solid {THEME['button_bg']};
|
|
border-radius: 4px;
|
|
padding: 6px;
|
|
}}
|
|
QLineEdit:focus {{
|
|
border-color: {THEME['accent_color']};
|
|
}}
|
|
QLineEdit:read-only {{
|
|
background-color: {THEME['bg_color']};
|
|
}}
|
|
QCheckBox {{
|
|
color: {THEME['fg_color']};
|
|
}}
|
|
QCheckBox::indicator {{
|
|
width: 18px;
|
|
height: 18px;
|
|
}}
|
|
QPushButton {{
|
|
background-color: {THEME['button_bg']};
|
|
color: {THEME['fg_color']};
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {THEME['highlight_color']};
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: {THEME['accent_color']};
|
|
}}
|
|
""")
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Tab widget
|
|
self.tab_widget = QTabWidget()
|
|
|
|
# General tab
|
|
general_tab = self.create_general_tab()
|
|
self.tab_widget.addTab(general_tab, "General")
|
|
|
|
# Relay Server tab
|
|
relay_tab = self.create_relay_tab()
|
|
self.tab_widget.addTab(relay_tab, "Relay Server")
|
|
|
|
layout.addWidget(self.tab_widget)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
|
|
cancel_btn = QPushButton("Cancel")
|
|
cancel_btn.clicked.connect(self.reject)
|
|
button_layout.addWidget(cancel_btn)
|
|
|
|
save_btn = QPushButton("Save")
|
|
save_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: {THEME['accent_color']};
|
|
color: white;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: #0096ff;
|
|
}}
|
|
""")
|
|
save_btn.clicked.connect(self.save_settings)
|
|
button_layout.addWidget(save_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
def create_general_tab(self) -> QWidget:
|
|
"""Create the general settings tab."""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Behavior group
|
|
behavior_group = QGroupBox("Behavior")
|
|
behavior_layout = QVBoxLayout(behavior_group)
|
|
|
|
self.minimize_to_tray_cb = QCheckBox("Minimize to system tray")
|
|
self.minimize_to_tray_cb.setToolTip(
|
|
"When enabled, minimizing the window will hide it to the system tray\n"
|
|
"instead of the taskbar. Double-click the tray icon to restore."
|
|
)
|
|
behavior_layout.addWidget(self.minimize_to_tray_cb)
|
|
|
|
layout.addWidget(behavior_group)
|
|
layout.addStretch()
|
|
|
|
return tab
|
|
|
|
def create_relay_tab(self) -> QWidget:
|
|
"""Create the relay server settings tab."""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Info label
|
|
info_label = QLabel(
|
|
"The relay server allows you to access MacroPad from anywhere\n"
|
|
"via a secure HTTPS connection. Your macros will be accessible\n"
|
|
"through a unique URL that you can share with your devices."
|
|
)
|
|
info_label.setWordWrap(True)
|
|
info_label.setStyleSheet(f"color: #aaa; margin-bottom: 10px;")
|
|
layout.addWidget(info_label)
|
|
|
|
# Connection group
|
|
connection_group = QGroupBox("Connection")
|
|
connection_layout = QFormLayout(connection_group)
|
|
|
|
self.relay_enabled_cb = QCheckBox("Enable relay server")
|
|
self.relay_enabled_cb.stateChanged.connect(self.on_relay_enabled_changed)
|
|
connection_layout.addRow("", self.relay_enabled_cb)
|
|
|
|
self.relay_url_edit = QLineEdit()
|
|
self.relay_url_edit.setPlaceholderText("wss://relay.example.com")
|
|
connection_layout.addRow("Server URL:", self.relay_url_edit)
|
|
|
|
layout.addWidget(connection_group)
|
|
|
|
# Authentication group
|
|
auth_group = QGroupBox("Authentication")
|
|
auth_layout = QFormLayout(auth_group)
|
|
|
|
self.relay_password_edit = QLineEdit()
|
|
self.relay_password_edit.setEchoMode(QLineEdit.Password)
|
|
self.relay_password_edit.setPlaceholderText("Required - minimum 8 characters")
|
|
auth_layout.addRow("Password:", self.relay_password_edit)
|
|
|
|
# Show password toggle
|
|
show_password_cb = QCheckBox("Show password")
|
|
show_password_cb.stateChanged.connect(
|
|
lambda state: self.relay_password_edit.setEchoMode(
|
|
QLineEdit.Normal if state else QLineEdit.Password
|
|
)
|
|
)
|
|
auth_layout.addRow("", show_password_cb)
|
|
|
|
layout.addWidget(auth_group)
|
|
|
|
# Status group
|
|
status_group = QGroupBox("Status")
|
|
status_layout = QFormLayout(status_group)
|
|
|
|
self.relay_session_id_edit = QLineEdit()
|
|
self.relay_session_id_edit.setReadOnly(True)
|
|
self.relay_session_id_edit.setPlaceholderText("Not connected")
|
|
status_layout.addRow("Session ID:", self.relay_session_id_edit)
|
|
|
|
self.relay_full_url_edit = QLineEdit()
|
|
self.relay_full_url_edit.setReadOnly(True)
|
|
self.relay_full_url_edit.setPlaceholderText("Connect to see your URL")
|
|
status_layout.addRow("Your URL:", self.relay_full_url_edit)
|
|
|
|
# Regenerate button
|
|
regen_btn = QPushButton("Generate New URL")
|
|
regen_btn.setToolTip("Generate a new unique URL (invalidates the old one)")
|
|
regen_btn.clicked.connect(self.regenerate_session_id)
|
|
status_layout.addRow("", regen_btn)
|
|
|
|
layout.addWidget(status_group)
|
|
layout.addStretch()
|
|
|
|
return tab
|
|
|
|
def load_settings(self):
|
|
"""Load current settings into the UI."""
|
|
# General
|
|
self.minimize_to_tray_cb.setChecked(
|
|
self.settings_manager.get_minimize_to_tray()
|
|
)
|
|
|
|
# Relay
|
|
self.relay_enabled_cb.setChecked(
|
|
self.settings_manager.get_relay_enabled()
|
|
)
|
|
self.relay_url_edit.setText(
|
|
self.settings_manager.get_relay_url()
|
|
)
|
|
self.relay_password_edit.setText(
|
|
self.settings_manager.get_relay_password()
|
|
)
|
|
|
|
session_id = self.settings_manager.get_relay_session_id()
|
|
if session_id:
|
|
self.relay_session_id_edit.setText(session_id)
|
|
base_url = self.relay_url_edit.text().replace('wss://', 'https://').replace('ws://', 'http://')
|
|
base_url = base_url.replace('/desktop', '')
|
|
self.relay_full_url_edit.setText(f"{base_url}/{session_id}")
|
|
|
|
self.on_relay_enabled_changed()
|
|
|
|
def on_relay_enabled_changed(self):
|
|
"""Handle relay enabled checkbox change."""
|
|
enabled = self.relay_enabled_cb.isChecked()
|
|
self.relay_url_edit.setEnabled(enabled)
|
|
self.relay_password_edit.setEnabled(enabled)
|
|
|
|
def validate_settings(self) -> bool:
|
|
"""Validate settings before saving."""
|
|
if self.relay_enabled_cb.isChecked():
|
|
url = self.relay_url_edit.text().strip()
|
|
password = self.relay_password_edit.text()
|
|
|
|
if not url:
|
|
QMessageBox.warning(
|
|
self, "Validation Error",
|
|
"Relay server URL is required when relay is enabled."
|
|
)
|
|
return False
|
|
|
|
if not url.startswith(('ws://', 'wss://')):
|
|
QMessageBox.warning(
|
|
self, "Validation Error",
|
|
"Relay server URL must start with ws:// or wss://"
|
|
)
|
|
return False
|
|
|
|
if len(password) < 8:
|
|
QMessageBox.warning(
|
|
self, "Validation Error",
|
|
"Password must be at least 8 characters."
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def save_settings(self):
|
|
"""Save settings and close dialog."""
|
|
if not self.validate_settings():
|
|
return
|
|
|
|
# General
|
|
self.settings_manager.set(
|
|
'minimize_to_tray',
|
|
self.minimize_to_tray_cb.isChecked()
|
|
)
|
|
|
|
# Relay - check if settings changed
|
|
old_enabled = self.settings_manager.get_relay_enabled()
|
|
old_url = self.settings_manager.get_relay_url()
|
|
old_password = self.settings_manager.get_relay_password()
|
|
|
|
new_enabled = self.relay_enabled_cb.isChecked()
|
|
new_url = self.relay_url_edit.text().strip()
|
|
new_password = self.relay_password_edit.text()
|
|
|
|
relay_changed = (
|
|
old_enabled != new_enabled or
|
|
old_url != new_url or
|
|
old_password != new_password
|
|
)
|
|
|
|
self.settings_manager.set('relay.enabled', new_enabled)
|
|
self.settings_manager.set('relay.server_url', new_url)
|
|
self.settings_manager.set('relay.password', new_password)
|
|
|
|
self.settings_manager.save()
|
|
|
|
if relay_changed:
|
|
self.relay_settings_changed.emit()
|
|
|
|
self.accept()
|
|
|
|
def regenerate_session_id(self):
|
|
"""Clear session ID to force regeneration on next connect."""
|
|
reply = QMessageBox.question(
|
|
self, "Regenerate URL",
|
|
"This will generate a new URL. The old URL will stop working.\n\n"
|
|
"Are you sure you want to continue?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
self.settings_manager.set('relay.session_id', None)
|
|
self.relay_session_id_edit.setText("")
|
|
self.relay_full_url_edit.setText("")
|
|
self.settings_manager.save()
|
|
QMessageBox.information(
|
|
self, "URL Regenerated",
|
|
"A new URL will be generated when you reconnect to the relay server."
|
|
)
|