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