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:
344
gui/settings_dialog.py
Normal file
344
gui/settings_dialog.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# 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."
|
||||
)
|
||||
Reference in New Issue
Block a user