Update v2 to not be dependent on Windows C++ runtimes, add QRCode, Add Refresh button to Web Interface

This commit is contained in:
jknapp 2025-03-18 12:43:08 -07:00
parent 933e166b06
commit d9d870096d
3 changed files with 234 additions and 197 deletions

BIN
dist/mp-server-v2.exe vendored Normal file

Binary file not shown.

View File

@ -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

View File

@ -3,4 +3,5 @@ pyautogui
pystray
flask
waitress
netifaces
netifaces
qrcode