Update v2 to not be dependent on Windows C++ runtimes, add QRCode, Add Refresh button to Web Interface
This commit is contained in:
parent
933e166b06
commit
d9d870096d
BIN
dist/mp-server-v2.exe
vendored
Normal file
BIN
dist/mp-server-v2.exe
vendored
Normal file
Binary file not shown.
428
mp-server-v2.py
428
mp-server-v2.py
@ -12,7 +12,9 @@ from flask import Flask, render_template_string, request, jsonify, send_file
|
||||
import webbrowser
|
||||
from waitress import serve
|
||||
import logging
|
||||
import netifaces
|
||||
import socket
|
||||
import qrcode
|
||||
import sys
|
||||
|
||||
class MacroPadServer:
|
||||
def __init__(self, root):
|
||||
@ -22,9 +24,13 @@ class MacroPadServer:
|
||||
self.configure_styles()
|
||||
|
||||
# Set up directories
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if getattr(sys, 'frozen', False):
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.data_file = os.path.join(base_dir, "macros.json")
|
||||
self.images_dir = os.path.join(base_dir, "macro_images")
|
||||
self.app_dir = base_dir
|
||||
os.makedirs(self.images_dir, exist_ok=True)
|
||||
|
||||
self.macros = {}
|
||||
@ -178,7 +184,7 @@ class MacroPadServer:
|
||||
if macro["image_path"] in self.image_cache:
|
||||
button_image = self.image_cache[macro["image_path"]]
|
||||
else:
|
||||
img_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), macro["image_path"])
|
||||
img_path = os.path.join(self.app_dir, macro["image_path"])
|
||||
img = Image.open(img_path)
|
||||
img = img.resize((32, 32))
|
||||
button_image = ImageTk.PhotoImage(img)
|
||||
@ -213,19 +219,19 @@ class MacroPadServer:
|
||||
self.flask_thread.start()
|
||||
self.server_button.config(text="Stop Web Server")
|
||||
|
||||
# Get all IP addresses to display
|
||||
ip_addresses = self.get_ip_addresses()
|
||||
if ip_addresses:
|
||||
# Get the systems internal IP address
|
||||
ip_address = self.get_ip_addresses()
|
||||
if ip_address:
|
||||
# Set the URL display
|
||||
urls = [f"http://{ip}:{self.server_port}" for ip in ip_addresses]
|
||||
url_text = "Web UI available at:\n" + "\n".join(urls)
|
||||
url = f"http://{ip_address}:{self.server_port}"
|
||||
url_text = "Web UI available at:\n" + "\n" + url
|
||||
self.url_var.set(url_text)
|
||||
|
||||
# Enable browser button
|
||||
self.browser_button.config(state=tk.NORMAL)
|
||||
|
||||
# Generate and display QR code for the first IP
|
||||
self.generate_qr_code(urls[0])
|
||||
self.generate_qr_code(url)
|
||||
else:
|
||||
self.url_var.set("No network interfaces found")
|
||||
except Exception as e:
|
||||
@ -246,30 +252,34 @@ class MacroPadServer:
|
||||
# The Flask server will be stopped on the next request
|
||||
|
||||
def get_ip_addresses(self):
|
||||
ip_addresses = []
|
||||
"""Get the primary internal IPv4 address of the machine."""
|
||||
try:
|
||||
# Get all network interfaces
|
||||
interfaces = netifaces.interfaces()
|
||||
for interface in interfaces:
|
||||
# Skip loopback interface
|
||||
if interface.startswith('lo'):
|
||||
continue
|
||||
|
||||
# Get addresses for this interface
|
||||
addresses = netifaces.ifaddresses(interface)
|
||||
if netifaces.AF_INET in addresses:
|
||||
for address in addresses[netifaces.AF_INET]:
|
||||
ip = address.get('addr')
|
||||
if ip and not ip.startswith('127.'):
|
||||
ip_addresses.append(ip)
|
||||
|
||||
# Always include localhost
|
||||
ip_addresses.append('localhost')
|
||||
|
||||
return ip_addresses
|
||||
# Create a socket to connect to an external server
|
||||
# This helps determine which network interface is used for outbound connections
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
# We don't need to actually send data - just configure the socket
|
||||
s.connect(("8.8.8.8", 80))
|
||||
# Get the IP address that would be used for this connection
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception as e:
|
||||
print(f"Error getting IP addresses: {e}")
|
||||
return ['localhost']
|
||||
print(f"Error getting IP address: {e}")
|
||||
# Fallback method if the above doesn't work
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
ip = socket.gethostbyname(hostname)
|
||||
# Don't return localhost address
|
||||
if ip.startswith("127."):
|
||||
for addr_info in socket.getaddrinfo(hostname, None):
|
||||
potential_ip = addr_info[4][0]
|
||||
# If IPv4 and not localhost
|
||||
if '.' in potential_ip and not potential_ip.startswith("127."):
|
||||
return potential_ip
|
||||
else:
|
||||
return ip
|
||||
except:
|
||||
return "127.0.0.1" # Last resort fallback
|
||||
|
||||
def generate_qr_code(self, url):
|
||||
try:
|
||||
@ -316,170 +326,196 @@ class MacroPadServer:
|
||||
# Define HTML templates
|
||||
index_html = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MacroPad Web Interface</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #2e2e2e;
|
||||
color: #ffffff;
|
||||
}
|
||||
h1 {
|
||||
color: #007acc;
|
||||
text-align: center;
|
||||
}
|
||||
.macro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.macro-button {
|
||||
background-color: #505050;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 15px 10px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100px;
|
||||
}
|
||||
.macro-button:hover, .macro-button:active {
|
||||
background-color: #007acc;
|
||||
}
|
||||
.macro-button img {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
.error {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.macro-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
.macro-button {
|
||||
padding: 10px 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MacroPad Web Interface</h1>
|
||||
|
||||
<div class="status success" id="success-status">Macro executed successfully!</div>
|
||||
<div class="status error" id="error-status">Failed to execute macro</div>
|
||||
|
||||
<div class="macro-grid" id="macro-grid">
|
||||
<!-- Macros will be loaded here -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMacros();
|
||||
});
|
||||
|
||||
function loadMacros() {
|
||||
fetch('/api/macros')
|
||||
.then(response => response.json())
|
||||
.then(macros => {
|
||||
const macroGrid = document.getElementById('macro-grid');
|
||||
macroGrid.innerHTML = '';
|
||||
|
||||
if (Object.keys(macros).length === 0) {
|
||||
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros defined.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [macroId, macro] of Object.entries(macros)) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'macro-button';
|
||||
button.onclick = function() { executeMacro(macroId); };
|
||||
|
||||
// Add image if available
|
||||
if (macro.image_path) {
|
||||
const img = document.createElement('img');
|
||||
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
|
||||
img.alt = macro.name;
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
};
|
||||
button.appendChild(img);
|
||||
}
|
||||
|
||||
const text = document.createTextNode(macro.name);
|
||||
button.appendChild(text);
|
||||
|
||||
macroGrid.appendChild(button);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading macros:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function executeMacro(macroId) {
|
||||
fetch('/api/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ macro_id: macroId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const successStatus = document.getElementById('success-status');
|
||||
const errorStatus = document.getElementById('error-status');
|
||||
|
||||
if (data.success) {
|
||||
successStatus.style.display = 'block';
|
||||
errorStatus.style.display = 'none';
|
||||
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
|
||||
} else {
|
||||
errorStatus.style.display = 'block';
|
||||
successStatus.style.display = 'none';
|
||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error executing macro:', error);
|
||||
const errorStatus = document.getElementById('error-status');
|
||||
errorStatus.style.display = 'block';
|
||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MacroPad Web Interface</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #2e2e2e;
|
||||
color: #ffffff;
|
||||
}
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #007acc;
|
||||
margin: 0;
|
||||
}
|
||||
.refresh-button {
|
||||
background-color: #505050;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.refresh-button:hover {
|
||||
background-color: #007acc;
|
||||
}
|
||||
.refresh-button svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.macro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.macro-button {
|
||||
background-color: #505050;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 15px 10px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100px;
|
||||
}
|
||||
.macro-button:hover, .macro-button:active {
|
||||
background-color: #007acc;
|
||||
}
|
||||
.macro-button img {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.status {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
.error {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.macro-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
.macro-button {
|
||||
padding: 10px 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.refresh-button {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-container">
|
||||
<h1>MacroPad Web Interface</h1>
|
||||
<button class="refresh-button" onclick="loadMacros()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="status success" id="success-status">Macro executed successfully!</div>
|
||||
<div class="status error" id="error-status">Failed to execute macro</div>
|
||||
<div class="macro-grid" id="macro-grid">
|
||||
<!-- Macros will be loaded here -->
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadMacros();
|
||||
});
|
||||
function loadMacros() {
|
||||
fetch('/api/macros')
|
||||
.then(response => response.json())
|
||||
.then(macros => {
|
||||
const macroGrid = document.getElementById('macro-grid');
|
||||
macroGrid.innerHTML = '';
|
||||
if (Object.keys(macros).length === 0) {
|
||||
macroGrid.innerHTML = '<p style="grid-column: 1 / -1; text-align: center;">No macros defined.</p>';
|
||||
return;
|
||||
}
|
||||
for (const [macroId, macro] of Object.entries(macros)) {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'macro-button';
|
||||
button.onclick = function() { executeMacro(macroId); };
|
||||
// Add image if available
|
||||
if (macro.image_path) {
|
||||
const img = document.createElement('img');
|
||||
img.src = `/api/image/${encodeURIComponent(macro.image_path)}`;
|
||||
img.alt = macro.name;
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
};
|
||||
button.appendChild(img);
|
||||
}
|
||||
const text = document.createTextNode(macro.name);
|
||||
button.appendChild(text);
|
||||
macroGrid.appendChild(button);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading macros:', error);
|
||||
});
|
||||
}
|
||||
function executeMacro(macroId) {
|
||||
fetch('/api/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ macro_id: macroId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const successStatus = document.getElementById('success-status');
|
||||
const errorStatus = document.getElementById('error-status');
|
||||
if (data.success) {
|
||||
successStatus.style.display = 'block';
|
||||
errorStatus.style.display = 'none';
|
||||
setTimeout(() => { successStatus.style.display = 'none'; }, 2000);
|
||||
} else {
|
||||
errorStatus.style.display = 'block';
|
||||
successStatus.style.display = 'none';
|
||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error executing macro:', error);
|
||||
const errorStatus = document.getElementById('error-status');
|
||||
errorStatus.style.display = 'block';
|
||||
setTimeout(() => { errorStatus.style.display = 'none'; }, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
@app.route('/')
|
||||
@ -495,7 +531,7 @@ class MacroPadServer:
|
||||
try:
|
||||
# Using os.path.join would translate slashes incorrectly on Windows
|
||||
# Use normpath instead
|
||||
image_path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), image_path))
|
||||
image_path = os.path.join(self.app_dir, image_path)
|
||||
return send_file(image_path)
|
||||
except Exception as e:
|
||||
return str(e), 404
|
||||
|
@ -3,4 +3,5 @@ pyautogui
|
||||
pystray
|
||||
flask
|
||||
waitress
|
||||
netifaces
|
||||
netifaces
|
||||
qrcode
|
Loading…
x
Reference in New Issue
Block a user