Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7922910bd8 | |||
| 664d652e9e | |||
| 10971e6a02 | |||
| 17f4bc0c5f | |||
| 44c21e68d8 |
21
README.md
21
README.md
@@ -20,6 +20,7 @@ A cross-platform macro management application with desktop and web interfaces. C
|
|||||||
- **System Tray**: Minimize to tray, always accessible
|
- **System Tray**: Minimize to tray, always accessible
|
||||||
|
|
||||||
### Additional Features
|
### Additional Features
|
||||||
|
- **Relay Server Support**: Access your macros securely over HTTPS from anywhere
|
||||||
- **QR Code Generation**: Quickly connect mobile devices
|
- **QR Code Generation**: Quickly connect mobile devices
|
||||||
- **Real-time Sync**: WebSocket updates across all connected devices
|
- **Real-time Sync**: WebSocket updates across all connected devices
|
||||||
- **Offline Support**: PWA caches for offline macro viewing
|
- **Offline Support**: PWA caches for offline macro viewing
|
||||||
@@ -39,6 +40,7 @@ A cross-platform macro management application with desktop and web interfaces. C
|
|||||||
- pystray (System tray)
|
- pystray (System tray)
|
||||||
- netifaces (Network detection)
|
- netifaces (Network detection)
|
||||||
- qrcode (QR code generation)
|
- qrcode (QR code generation)
|
||||||
|
- aiohttp (Relay server client)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -136,6 +138,24 @@ The web interface provides full macro management:
|
|||||||
- **Show**: Restore window
|
- **Show**: Restore window
|
||||||
- **Quit**: Exit application
|
- **Quit**: Exit application
|
||||||
|
|
||||||
|
### Relay Server (Remote Access)
|
||||||
|
|
||||||
|
Access your macros from outside your local network using a relay server:
|
||||||
|
|
||||||
|
1. Click **Settings** (gear icon) in the toolbar
|
||||||
|
2. Check **Enable Relay Server**
|
||||||
|
3. Enter your relay server URL and password
|
||||||
|
4. Click **Save**
|
||||||
|
|
||||||
|
Once connected, a relay URL will appear in the toolbar. Use this URL from any device with internet access. The relay provides:
|
||||||
|
- Secure HTTPS connection
|
||||||
|
- Full macro execution and management
|
||||||
|
- PWA installation support
|
||||||
|
- Wake lock and fullscreen mode
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> You need access to a relay server. See [MP-Relay](https://repo.anhonesthost.net/MacroPad/MP-Relay) for self-hosting instructions.
|
||||||
|
|
||||||
## Command Types Reference
|
## Command Types Reference
|
||||||
|
|
||||||
| Type | Description | Parameters |
|
| Type | Description | Parameters |
|
||||||
@@ -213,6 +233,7 @@ MP-Server/
|
|||||||
├── config.py # Configuration constants
|
├── config.py # Configuration constants
|
||||||
├── macro_manager.py # Macro storage and execution
|
├── macro_manager.py # Macro storage and execution
|
||||||
├── web_server.py # FastAPI web server
|
├── web_server.py # FastAPI web server
|
||||||
|
├── relay_client.py # Relay server WebSocket client
|
||||||
├── pyproject.toml # Dependencies and build config
|
├── pyproject.toml # Dependencies and build config
|
||||||
├── gui/ # PySide6 desktop interface
|
├── gui/ # PySide6 desktop interface
|
||||||
│ ├── main_window.py
|
│ ├── main_window.py
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Configuration and constants for MacroPad Server
|
# Configuration and constants for MacroPad Server
|
||||||
|
|
||||||
VERSION = "0.9.5"
|
VERSION = "1.0.0"
|
||||||
DEFAULT_PORT = 40000
|
DEFAULT_PORT = 40000
|
||||||
SETTINGS_FILE = "settings.json"
|
SETTINGS_FILE = "settings.json"
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class MacroButton(QPushButton):
|
|||||||
edit_requested = Signal(str)
|
edit_requested = Signal(str)
|
||||||
delete_requested = Signal(str)
|
delete_requested = Signal(str)
|
||||||
|
|
||||||
def __init__(self, macro_id: str, macro: dict, parent=None):
|
def __init__(self, macro_id: str, macro: dict, app_dir: str, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.macro_id = macro_id
|
self.macro_id = macro_id
|
||||||
self.macro = macro
|
self.macro = macro
|
||||||
@@ -75,7 +75,9 @@ class MacroButton(QPushButton):
|
|||||||
image_label.setAlignment(Qt.AlignCenter)
|
image_label.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
if macro.get("image_path"):
|
if macro.get("image_path"):
|
||||||
pixmap = QPixmap(macro["image_path"])
|
# Resolve relative image path against app directory
|
||||||
|
image_path = os.path.join(app_dir, macro["image_path"])
|
||||||
|
pixmap = QPixmap(image_path)
|
||||||
if not pixmap.isNull():
|
if not pixmap.isNull():
|
||||||
pixmap = pixmap.scaled(48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
pixmap = pixmap.scaled(48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
image_label.setPixmap(pixmap)
|
image_label.setPixmap(pixmap)
|
||||||
@@ -174,7 +176,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
"""Setup the main UI components."""
|
"""Setup the main UI components."""
|
||||||
self.setWindowTitle(f"MacroPad Server v{VERSION}")
|
self.setWindowTitle("MacroPad Server")
|
||||||
self.setMinimumSize(600, 400)
|
self.setMinimumSize(600, 400)
|
||||||
self.setStyleSheet(f"background-color: {THEME['bg_color']};")
|
self.setStyleSheet(f"background-color: {THEME['bg_color']};")
|
||||||
|
|
||||||
@@ -210,9 +212,18 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
toolbar_layout.addStretch()
|
toolbar_layout.addStretch()
|
||||||
|
|
||||||
# IP address label
|
# Clickable IP address label (click to copy)
|
||||||
self.ip_label = QLabel()
|
self.ip_label = QPushButton()
|
||||||
self.ip_label.setStyleSheet(f"color: {THEME['fg_color']};")
|
self.ip_label.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.ip_label.setStyleSheet(f"""
|
||||||
|
QPushButton {{
|
||||||
|
background-color: transparent;
|
||||||
|
color: {THEME['fg_color']};
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
self.ip_label.clicked.connect(self.copy_url_to_clipboard)
|
||||||
self.update_ip_label()
|
self.update_ip_label()
|
||||||
toolbar_layout.addWidget(self.ip_label)
|
toolbar_layout.addWidget(self.ip_label)
|
||||||
|
|
||||||
@@ -358,7 +369,7 @@ class MainWindow(QMainWindow):
|
|||||||
# Tray menu
|
# Tray menu
|
||||||
tray_menu = QMenu()
|
tray_menu = QMenu()
|
||||||
show_action = tray_menu.addAction("Show")
|
show_action = tray_menu.addAction("Show")
|
||||||
show_action.triggered.connect(self.show)
|
show_action.triggered.connect(self.restore_window)
|
||||||
quit_action = tray_menu.addAction("Quit")
|
quit_action = tray_menu.addAction("Quit")
|
||||||
quit_action.triggered.connect(self.close)
|
quit_action.triggered.connect(self.close)
|
||||||
|
|
||||||
@@ -369,8 +380,15 @@ class MainWindow(QMainWindow):
|
|||||||
def on_tray_activated(self, reason):
|
def on_tray_activated(self, reason):
|
||||||
"""Handle tray icon activation."""
|
"""Handle tray icon activation."""
|
||||||
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
||||||
self.show()
|
self.restore_window()
|
||||||
self.activateWindow()
|
|
||||||
|
def restore_window(self):
|
||||||
|
"""Restore and bring window to front."""
|
||||||
|
# Clear minimized state and show
|
||||||
|
self.setWindowState(Qt.WindowNoState)
|
||||||
|
self.show()
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
|
|
||||||
def start_server(self):
|
def start_server(self):
|
||||||
"""Start the web server in a background thread."""
|
"""Start the web server in a background thread."""
|
||||||
@@ -422,7 +440,6 @@ class MainWindow(QMainWindow):
|
|||||||
# Check if relay is connected and has a session ID
|
# Check if relay is connected and has a session ID
|
||||||
relay_connected = self.relay_client and self.relay_client.is_connected()
|
relay_connected = self.relay_client and self.relay_client.is_connected()
|
||||||
session_id = self.settings_manager.get_relay_session_id()
|
session_id = self.settings_manager.get_relay_session_id()
|
||||||
print(f"[DEBUG] update_ip_label: relay_connected={relay_connected}, session_id={session_id}")
|
|
||||||
|
|
||||||
if relay_connected and session_id:
|
if relay_connected and session_id:
|
||||||
relay_url = self.settings_manager.get_relay_url()
|
relay_url = self.settings_manager.get_relay_url()
|
||||||
@@ -430,7 +447,6 @@ class MainWindow(QMainWindow):
|
|||||||
base_url = relay_url.replace('wss://', 'https://').replace('ws://', 'http://')
|
base_url = relay_url.replace('wss://', 'https://').replace('ws://', 'http://')
|
||||||
base_url = base_url.replace('/desktop', '').rstrip('/')
|
base_url = base_url.replace('/desktop', '').rstrip('/')
|
||||||
full_url = f"{base_url}/{session_id}"
|
full_url = f"{base_url}/{session_id}"
|
||||||
print(f"[DEBUG] Setting relay URL: {full_url}")
|
|
||||||
self.ip_label.setText(full_url)
|
self.ip_label.setText(full_url)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -449,6 +465,40 @@ class MainWindow(QMainWindow):
|
|||||||
pass
|
pass
|
||||||
self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}")
|
self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}")
|
||||||
|
|
||||||
|
def copy_url_to_clipboard(self):
|
||||||
|
"""Copy the web interface URL to clipboard."""
|
||||||
|
url = self.ip_label.text()
|
||||||
|
if url == "Copied!":
|
||||||
|
return # Already showing copied feedback
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(url)
|
||||||
|
|
||||||
|
# Show "Copied!" feedback with dimmed style
|
||||||
|
self.ip_label.setText("Copied!")
|
||||||
|
self.ip_label.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #888888;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Restore original URL after delay
|
||||||
|
QTimer.singleShot(1500, self._restore_url_label)
|
||||||
|
|
||||||
|
def _restore_url_label(self):
|
||||||
|
"""Restore URL label after copy feedback."""
|
||||||
|
self.ip_label.setStyleSheet(f"""
|
||||||
|
QPushButton {{
|
||||||
|
background-color: transparent;
|
||||||
|
color: {THEME['fg_color']};
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
self.update_ip_label()
|
||||||
|
|
||||||
def refresh_tabs(self):
|
def refresh_tabs(self):
|
||||||
"""Refresh the tab widget."""
|
"""Refresh the tab widget."""
|
||||||
self.tab_widget.blockSignals(True)
|
self.tab_widget.blockSignals(True)
|
||||||
@@ -499,7 +549,7 @@ class MainWindow(QMainWindow):
|
|||||||
# Add macro buttons
|
# Add macro buttons
|
||||||
cols = max(1, (self.width() - 40) // 130)
|
cols = max(1, (self.width() - 40) // 130)
|
||||||
for i, (macro_id, macro) in enumerate(filtered):
|
for i, (macro_id, macro) in enumerate(filtered):
|
||||||
btn = MacroButton(macro_id, macro)
|
btn = MacroButton(macro_id, macro, self.app_dir)
|
||||||
btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid))
|
btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid))
|
||||||
btn.edit_requested.connect(self.edit_macro)
|
btn.edit_requested.connect(self.edit_macro)
|
||||||
btn.delete_requested.connect(self.delete_macro)
|
btn.delete_requested.connect(self.delete_macro)
|
||||||
@@ -675,12 +725,10 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def on_relay_session_id(self, session_id: str):
|
def on_relay_session_id(self, session_id: str):
|
||||||
"""Handle receiving session ID from relay (called from background thread)."""
|
"""Handle receiving session ID from relay (called from background thread)."""
|
||||||
print(f"[DEBUG] on_relay_session_id called with: {session_id}")
|
|
||||||
self.relay_session_received.emit(session_id)
|
self.relay_session_received.emit(session_id)
|
||||||
|
|
||||||
def _handle_relay_session(self, session_id: str):
|
def _handle_relay_session(self, session_id: str):
|
||||||
"""Handle relay session on main thread."""
|
"""Handle relay session on main thread."""
|
||||||
print(f"[DEBUG] _handle_relay_session on main thread: {session_id}")
|
|
||||||
self.settings_manager.set_relay_session_id(session_id)
|
self.settings_manager.set_relay_session_id(session_id)
|
||||||
self.update_ip_label()
|
self.update_ip_label()
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.9.0
|
1.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user