655 lines
26 KiB
HTML
655 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GUI Editor - KNX Display</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #1e1e2e;
|
|
color: #cdd6f4;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
/* Header */
|
|
.header {
|
|
background: #181825;
|
|
padding: 10px 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
border-bottom: 1px solid #313244;
|
|
}
|
|
.header h1 { font-size: 18px; color: #89b4fa; }
|
|
.header-buttons { display: flex; gap: 10px; margin-left: auto; }
|
|
/* Main Layout */
|
|
.main {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
/* Sidebar - Widget Types */
|
|
.sidebar {
|
|
width: 200px;
|
|
background: #181825;
|
|
border-right: 1px solid #313244;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
.sidebar h3 {
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
color: #6c7086;
|
|
margin-bottom: 10px;
|
|
}
|
|
.widget-type {
|
|
background: #313244;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
}
|
|
.widget-type:hover { background: #45475a; }
|
|
.widget-type span { display: block; font-size: 12px; color: #a6adc8; }
|
|
/* Canvas Area */
|
|
.canvas-container {
|
|
flex: 1;
|
|
padding: 20px;
|
|
overflow: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.canvas {
|
|
position: relative;
|
|
background: #1a1a2e;
|
|
border: 2px solid #313244;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
/* Widgets on Canvas */
|
|
.widget {
|
|
position: absolute;
|
|
cursor: move;
|
|
user-select: none;
|
|
touch-action: none;
|
|
}
|
|
.widget.selected {
|
|
outline: 2px solid #89b4fa;
|
|
outline-offset: 2px;
|
|
}
|
|
.widget-label {
|
|
padding: 4px 6px;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
.widget-button {
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
/* Properties Panel */
|
|
.properties {
|
|
width: 280px;
|
|
background: #181825;
|
|
border-left: 1px solid #313244;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
}
|
|
.properties h3 {
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
color: #6c7086;
|
|
margin: 15px 0 10px;
|
|
}
|
|
.properties h3:first-child { margin-top: 0; }
|
|
.prop-row {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
.prop-row label {
|
|
width: 80px;
|
|
font-size: 12px;
|
|
color: #a6adc8;
|
|
}
|
|
.prop-row input, .prop-row select {
|
|
flex: 1;
|
|
background: #313244;
|
|
border: 1px solid #45475a;
|
|
border-radius: 4px;
|
|
padding: 6px 8px;
|
|
color: #cdd6f4;
|
|
font-size: 12px;
|
|
}
|
|
.prop-row input[type="color"] {
|
|
padding: 2px;
|
|
height: 30px;
|
|
cursor: pointer;
|
|
}
|
|
.prop-row input[type="checkbox"] {
|
|
width: auto;
|
|
flex: none;
|
|
}
|
|
.prop-row input:focus, .prop-row select:focus {
|
|
outline: none;
|
|
border-color: #89b4fa;
|
|
}
|
|
/* Buttons */
|
|
button {
|
|
background: #89b4fa;
|
|
color: #1e1e2e;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
button:hover { background: #b4befe; }
|
|
button.danger { background: #f38ba8; }
|
|
button.danger:hover { background: #eba0ac; }
|
|
button.secondary {
|
|
background: #45475a;
|
|
color: #cdd6f4;
|
|
}
|
|
button.secondary:hover { background: #585b70; }
|
|
button.usb { background: #fab387; }
|
|
button.usb:hover { background: #f9e2af; }
|
|
/* Status */
|
|
.status {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #a6e3a1;
|
|
color: #1e1e2e;
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
z-index: 1000;
|
|
}
|
|
.status.show { opacity: 1; }
|
|
.status.error { background: #f38ba8; }
|
|
/* No selection */
|
|
.no-selection {
|
|
color: #6c7086;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>GUI Editor</h1>
|
|
<span style="color:#6c7086">KNX Display Konfiguration</span>
|
|
<div class="header-buttons">
|
|
<button class="usb" onclick="enableUsbMode()">USB-Modus</button>
|
|
<button class="secondary" onclick="resetConfig()">Zuruecksetzen</button>
|
|
<button onclick="saveConfig()">Speichern & Anwenden</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<div class="sidebar">
|
|
<h3>Widgets</h3>
|
|
<div class="widget-type" onclick="addWidget('label')">
|
|
<strong>Label</strong>
|
|
<span>Text / KNX-Wert</span>
|
|
</div>
|
|
<div class="widget-type" onclick="addWidget('button')">
|
|
<strong>Button</strong>
|
|
<span>KNX Schalten</span>
|
|
</div>
|
|
|
|
<h3 style="margin-top:20px">Bildschirm</h3>
|
|
<div class="prop-row">
|
|
<label>Hintergrund</label>
|
|
<input type="color" id="bgColor" value="#1a1a2e" onchange="updateBgColor()">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="canvas-container">
|
|
<div class="canvas" id="canvas" style="width:640px;height:400px">
|
|
<!-- Widgets appear here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="properties" id="properties">
|
|
<div class="no-selection">
|
|
Kein Widget ausgewaehlt.<br><br>
|
|
Klicken Sie auf ein Widget oder fuegen Sie ein neues hinzu.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status" id="status"></div>
|
|
|
|
<script>
|
|
const SCALE = 0.5; // Canvas scale (1280x800 -> 640x400)
|
|
const DISPLAY_W = 1280;
|
|
const DISPLAY_H = 800;
|
|
|
|
let config = { bgColor: '#1A1A2E', widgets: [] };
|
|
let selectedWidget = null;
|
|
let dragState = null;
|
|
let nextId = 0;
|
|
let knxAddresses = []; // KNX group objects
|
|
|
|
const textSources = {
|
|
0: 'Statisch',
|
|
1: 'KNX Temperatur',
|
|
2: 'KNX Schalter',
|
|
3: 'KNX Prozent',
|
|
4: 'KNX Text'
|
|
};
|
|
|
|
// Font sizes matching LVGL (montserrat fonts)
|
|
const fontSizes = [14, 18, 22, 28, 36, 48];
|
|
|
|
// Default format strings for KNX data types
|
|
const defaultFormats = {
|
|
0: '', // Static - user defines
|
|
1: '%.1f °C', // Temperature
|
|
2: '%s', // Switch (EIN/AUS handled in code)
|
|
3: '%d %%', // Percent
|
|
4: '%s' // Text
|
|
};
|
|
|
|
// Preview values for KNX data types
|
|
const previewValues = {
|
|
0: null, // Static - use actual text
|
|
1: '21.5 °C', // Temperature
|
|
2: 'EIN', // Switch
|
|
3: '75 %', // Percent
|
|
4: 'Text' // Text
|
|
};
|
|
|
|
// Get preview text for widget
|
|
function getPreviewText(w) {
|
|
if (w.textSrc === 0) return w.text; // Static
|
|
return previewValues[w.textSrc] || w.text;
|
|
}
|
|
|
|
// Convert hex color to rgba
|
|
function hexToRgba(hex, alpha) {
|
|
const r = parseInt(hex.slice(1,3), 16);
|
|
const g = parseInt(hex.slice(3,5), 16);
|
|
const b = parseInt(hex.slice(5,7), 16);
|
|
return `rgba(${r},${g},${b},${alpha})`;
|
|
}
|
|
|
|
async function loadKnxAddresses() {
|
|
try {
|
|
const resp = await fetch('/api/knx/addresses');
|
|
knxAddresses = await resp.json();
|
|
console.log('KNX addresses loaded:', knxAddresses.length);
|
|
} catch (e) {
|
|
console.error('Failed to load KNX addresses', e);
|
|
knxAddresses = [];
|
|
}
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
await loadKnxAddresses(); // Load KNX addresses first
|
|
const resp = await fetch('/api/config');
|
|
config = await resp.json();
|
|
nextId = Math.max(0, ...config.widgets.map(w => w.id)) + 1;
|
|
document.getElementById('bgColor').value = config.bgColor;
|
|
renderCanvas();
|
|
} catch (e) {
|
|
showStatus('Fehler beim Laden', true);
|
|
}
|
|
}
|
|
|
|
function renderCanvas() {
|
|
const canvas = document.getElementById('canvas');
|
|
canvas.style.background = config.bgColor;
|
|
canvas.innerHTML = '';
|
|
|
|
config.widgets.forEach(w => {
|
|
if (!w.visible) return;
|
|
|
|
const el = document.createElement('div');
|
|
el.className = 'widget' + (selectedWidget === w.id ? ' selected' : '');
|
|
el.dataset.id = w.id;
|
|
el.style.left = (w.x * SCALE) + 'px';
|
|
el.style.top = (w.y * SCALE) + 'px';
|
|
el.style.width = (w.w * SCALE) + 'px';
|
|
el.style.height = (w.h * SCALE) + 'px';
|
|
|
|
// Apply scaled font size and color
|
|
const fontSize = fontSizes[w.fontSize] || 14;
|
|
el.style.fontSize = (fontSize * SCALE) + 'px';
|
|
el.style.color = w.textColor;
|
|
|
|
// Get display text (preview for KNX values)
|
|
const displayText = getPreviewText(w);
|
|
|
|
if (w.type === 0) { // Label
|
|
el.className += ' widget-label';
|
|
if (w.bgOpacity > 0) {
|
|
const alpha = (w.bgOpacity / 255).toFixed(2);
|
|
el.style.background = hexToRgba(w.bgColor, alpha);
|
|
}
|
|
el.textContent = displayText;
|
|
} else { // Button
|
|
el.className += ' widget-button';
|
|
el.style.background = w.bgColor;
|
|
el.style.borderRadius = (w.radius * SCALE) + 'px';
|
|
if (w.shadow && w.shadow.enabled) {
|
|
el.style.boxShadow = `${w.shadow.x * SCALE}px ${w.shadow.y * SCALE}px ${w.shadow.blur * SCALE}px ${w.shadow.color}`;
|
|
}
|
|
el.textContent = displayText;
|
|
}
|
|
|
|
el.addEventListener('mousedown', startDrag);
|
|
el.addEventListener('touchstart', startDrag, {passive: false});
|
|
el.addEventListener('click', (e) => { e.stopPropagation(); selectWidget(w.id); });
|
|
|
|
canvas.appendChild(el);
|
|
});
|
|
}
|
|
|
|
function selectWidget(id) {
|
|
selectedWidget = id;
|
|
renderCanvas();
|
|
renderProperties();
|
|
}
|
|
|
|
function renderProperties() {
|
|
const panel = document.getElementById('properties');
|
|
const w = config.widgets.find(x => x.id === selectedWidget);
|
|
|
|
if (!w) {
|
|
panel.innerHTML = '<div class="no-selection">Kein Widget ausgewaehlt.</div>';
|
|
return;
|
|
}
|
|
|
|
panel.innerHTML = `
|
|
<h3>Position & Groesse</h3>
|
|
<div class="prop-row"><label>X</label><input type="number" id="px" value="${w.x}" onchange="updateProp('x')"></div>
|
|
<div class="prop-row"><label>Y</label><input type="number" id="py" value="${w.y}" onchange="updateProp('y')"></div>
|
|
<div class="prop-row"><label>Breite</label><input type="number" id="pw" value="${w.w}" onchange="updateProp('w')"></div>
|
|
<div class="prop-row"><label>Hoehe</label><input type="number" id="ph" value="${w.h}" onchange="updateProp('h')"></div>
|
|
<div class="prop-row"><label>Sichtbar</label><input type="checkbox" id="pvis" ${w.visible?'checked':''} onchange="updateProp('visible')"></div>
|
|
|
|
<h3>Text</h3>
|
|
<div class="prop-row"><label>Quelle</label>
|
|
<select id="ptextsrc" onchange="updateProp('textSrc')">
|
|
${Object.entries(textSources).map(([k,v]) => `<option value="${k}" ${w.textSrc==k?'selected':''}>${v}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
${w.textSrc === 0 ? `
|
|
<div class="prop-row"><label>Text</label><input type="text" id="ptext" value="${w.text}" onchange="updateProp('text')"></div>
|
|
` : `
|
|
<div class="prop-row"><label>KNX Objekt</label>
|
|
<select id="pknx" onchange="updateProp('knxAddr')">
|
|
<option value="0" ${w.knxAddr==0?'selected':''}>-- Waehlen --</option>
|
|
${knxAddresses.map(a => `<option value="${a.index}" ${w.knxAddr==a.index?'selected':''}>GO${a.index} (${a.addrStr})</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="prop-row"><label>Vorschau</label><span style="color:#a6adc8">${previewValues[w.textSrc] || '...'}</span></div>
|
|
<input type="hidden" id="ptext" value="${w.text}">
|
|
`}
|
|
<div class="prop-row"><label>Schriftgr.</label>
|
|
<select id="pfont" onchange="updateProp('fontSize')">
|
|
<option value="0" ${w.fontSize==0?'selected':''}>14</option>
|
|
<option value="1" ${w.fontSize==1?'selected':''}>18</option>
|
|
<option value="2" ${w.fontSize==2?'selected':''}>22</option>
|
|
<option value="3" ${w.fontSize==3?'selected':''}>28</option>
|
|
<option value="4" ${w.fontSize==4?'selected':''}>36</option>
|
|
<option value="5" ${w.fontSize==5?'selected':''}>48</option>
|
|
</select>
|
|
</div>
|
|
|
|
<h3>Farben</h3>
|
|
<div class="prop-row"><label>Textfarbe</label><input type="color" id="ptc" value="${w.textColor}" onchange="updateProp('textColor')"></div>
|
|
<div class="prop-row"><label>Hintergrund</label><input type="color" id="pbc" value="${w.bgColor}" onchange="updateProp('bgColor')"></div>
|
|
<div class="prop-row"><label>HG Deckkraft</label><input type="number" id="pbo" min="0" max="255" value="${w.bgOpacity}" onchange="updateProp('bgOpacity')"></div>
|
|
<div class="prop-row"><label>Radius</label><input type="number" id="prad" value="${w.radius}" onchange="updateProp('radius')"></div>
|
|
|
|
<h3>Schatten</h3>
|
|
<div class="prop-row"><label>Aktiv</label><input type="checkbox" id="pse" ${w.shadow?.enabled?'checked':''} onchange="updateShadow('enabled')"></div>
|
|
<div class="prop-row"><label>X</label><input type="number" id="psx" value="${w.shadow?.x||0}" onchange="updateShadow('x')"></div>
|
|
<div class="prop-row"><label>Y</label><input type="number" id="psy" value="${w.shadow?.y||0}" onchange="updateShadow('y')"></div>
|
|
<div class="prop-row"><label>Blur</label><input type="number" id="psb" value="${w.shadow?.blur||0}" onchange="updateShadow('blur')"></div>
|
|
<div class="prop-row"><label>Farbe</label><input type="color" id="psc" value="${w.shadow?.color||'#000000'}" onchange="updateShadow('color')"></div>
|
|
|
|
${w.type === 1 ? `
|
|
<h3>Button</h3>
|
|
<div class="prop-row"><label>Toggle</label><input type="checkbox" id="ptog" ${w.isToggle?'checked':''} onchange="updateProp('isToggle')"></div>
|
|
<div class="prop-row"><label>KNX Schreib</label>
|
|
<select id="pknxw" onchange="updateProp('knxAddrWrite')">
|
|
<option value="0" ${w.knxAddrWrite==0?'selected':''}>-- Keine --</option>
|
|
${knxAddresses.filter(a => a.write).map(a => `<option value="${a.index}" ${w.knxAddrWrite==a.index?'selected':''}>GO${a.index} (${a.addrStr})</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div style="margin-top:20px">
|
|
<button class="danger" onclick="deleteWidget()">Widget loeschen</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function updateProp(prop) {
|
|
const w = config.widgets.find(x => x.id === selectedWidget);
|
|
if (!w) return;
|
|
|
|
if (prop === 'x') w.x = parseInt(document.getElementById('px').value);
|
|
else if (prop === 'y') w.y = parseInt(document.getElementById('py').value);
|
|
else if (prop === 'w') w.w = parseInt(document.getElementById('pw').value);
|
|
else if (prop === 'h') w.h = parseInt(document.getElementById('ph').value);
|
|
else if (prop === 'visible') w.visible = document.getElementById('pvis').checked;
|
|
else if (prop === 'textSrc') {
|
|
const newSrc = parseInt(document.getElementById('ptextsrc').value);
|
|
w.textSrc = newSrc;
|
|
// Auto-fill format string when switching to KNX source
|
|
if (newSrc > 0 && defaultFormats[newSrc]) {
|
|
w.text = defaultFormats[newSrc];
|
|
// Update text field in UI
|
|
const textEl = document.getElementById('ptext');
|
|
if (textEl) textEl.value = w.text;
|
|
}
|
|
}
|
|
else if (prop === 'text') w.text = document.getElementById('ptext').value;
|
|
else if (prop === 'knxAddr') w.knxAddr = parseInt(document.getElementById('pknx').value);
|
|
else if (prop === 'fontSize') w.fontSize = parseInt(document.getElementById('pfont').value);
|
|
else if (prop === 'textColor') w.textColor = document.getElementById('ptc').value;
|
|
else if (prop === 'bgColor') w.bgColor = document.getElementById('pbc').value;
|
|
else if (prop === 'bgOpacity') w.bgOpacity = parseInt(document.getElementById('pbo').value);
|
|
else if (prop === 'radius') w.radius = parseInt(document.getElementById('prad').value);
|
|
else if (prop === 'isToggle') w.isToggle = document.getElementById('ptog').checked;
|
|
else if (prop === 'knxAddrWrite') w.knxAddrWrite = parseInt(document.getElementById('pknxw').value);
|
|
|
|
renderCanvas();
|
|
}
|
|
|
|
function updateShadow(prop) {
|
|
const w = config.widgets.find(x => x.id === selectedWidget);
|
|
if (!w) return;
|
|
if (!w.shadow) w.shadow = {enabled:false,x:2,y:2,blur:8,spread:0,color:'#000000'};
|
|
|
|
if (prop === 'enabled') w.shadow.enabled = document.getElementById('pse').checked;
|
|
else if (prop === 'x') w.shadow.x = parseInt(document.getElementById('psx').value);
|
|
else if (prop === 'y') w.shadow.y = parseInt(document.getElementById('psy').value);
|
|
else if (prop === 'blur') w.shadow.blur = parseInt(document.getElementById('psb').value);
|
|
else if (prop === 'color') w.shadow.color = document.getElementById('psc').value;
|
|
|
|
renderCanvas();
|
|
}
|
|
|
|
function updateBgColor() {
|
|
config.bgColor = document.getElementById('bgColor').value;
|
|
renderCanvas();
|
|
}
|
|
|
|
function addWidget(type) {
|
|
const w = {
|
|
id: nextId++,
|
|
type: type === 'label' ? 0 : 1,
|
|
x: 100, y: 100,
|
|
w: type === 'label' ? 150 : 120,
|
|
h: type === 'label' ? 40 : 50,
|
|
visible: true,
|
|
textSrc: 0,
|
|
text: type === 'label' ? 'Neues Label' : 'Button',
|
|
knxAddr: 0,
|
|
fontSize: 1,
|
|
textColor: '#FFFFFF',
|
|
bgColor: type === 'label' ? '#000000' : '#2196F3',
|
|
bgOpacity: type === 'label' ? 0 : 255,
|
|
radius: type === 'label' ? 0 : 8,
|
|
shadow: {enabled: type === 'button', x:2, y:2, blur:8, spread:0, color:'#000000'},
|
|
isToggle: false,
|
|
knxAddrWrite: 0
|
|
};
|
|
config.widgets.push(w);
|
|
selectWidget(w.id);
|
|
}
|
|
|
|
function deleteWidget() {
|
|
config.widgets = config.widgets.filter(w => w.id !== selectedWidget);
|
|
selectedWidget = null;
|
|
renderCanvas();
|
|
renderProperties();
|
|
}
|
|
|
|
function startDrag(e) {
|
|
e.preventDefault();
|
|
const el = e.target.closest('.widget');
|
|
if (!el) return;
|
|
|
|
const id = parseInt(el.dataset.id);
|
|
selectWidget(id);
|
|
|
|
const rect = document.getElementById('canvas').getBoundingClientRect();
|
|
const cx = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const cy = e.touches ? e.touches[0].clientY : e.clientY;
|
|
|
|
dragState = {
|
|
id: id,
|
|
startX: cx,
|
|
startY: cy,
|
|
origX: config.widgets.find(w => w.id === id).x,
|
|
origY: config.widgets.find(w => w.id === id).y
|
|
};
|
|
|
|
document.addEventListener('mousemove', drag);
|
|
document.addEventListener('mouseup', endDrag);
|
|
document.addEventListener('touchmove', drag, {passive:false});
|
|
document.addEventListener('touchend', endDrag);
|
|
}
|
|
|
|
function drag(e) {
|
|
if (!dragState) return;
|
|
e.preventDefault();
|
|
|
|
const cx = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const cy = e.touches ? e.touches[0].clientY : e.clientY;
|
|
|
|
const dx = (cx - dragState.startX) / SCALE;
|
|
const dy = (cy - dragState.startY) / SCALE;
|
|
|
|
const w = config.widgets.find(x => x.id === dragState.id);
|
|
if (w) {
|
|
w.x = Math.max(0, Math.min(DISPLAY_W - w.w, Math.round(dragState.origX + dx)));
|
|
w.y = Math.max(0, Math.min(DISPLAY_H - w.h, Math.round(dragState.origY + dy)));
|
|
renderCanvas();
|
|
// Update properties panel
|
|
const px = document.getElementById('px');
|
|
const py = document.getElementById('py');
|
|
if (px) px.value = w.x;
|
|
if (py) py.value = w.y;
|
|
}
|
|
}
|
|
|
|
function endDrag() {
|
|
dragState = null;
|
|
document.removeEventListener('mousemove', drag);
|
|
document.removeEventListener('mouseup', endDrag);
|
|
document.removeEventListener('touchmove', drag);
|
|
document.removeEventListener('touchend', endDrag);
|
|
}
|
|
|
|
async function saveConfig() {
|
|
try {
|
|
// First update config
|
|
await fetch('/api/config', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(config)
|
|
});
|
|
// Then save and apply
|
|
await fetch('/api/save', {method: 'POST'});
|
|
showStatus('Gespeichert!');
|
|
} catch (e) {
|
|
showStatus('Fehler beim Speichern', true);
|
|
}
|
|
}
|
|
|
|
async function resetConfig() {
|
|
if (!confirm('Wirklich auf Standardeinstellungen zuruecksetzen?')) return;
|
|
try {
|
|
await fetch('/api/reset', {method: 'POST'});
|
|
await loadConfig();
|
|
showStatus('Zurueckgesetzt!');
|
|
} catch (e) {
|
|
showStatus('Fehler', true);
|
|
}
|
|
}
|
|
|
|
async function enableUsbMode() {
|
|
if (!confirm('USB-Modus aktivieren?\n\nDie SD-Karte wird als USB-Laufwerk am PC verfuegbar.\nZum Beenden: Geraet neu starten.')) return;
|
|
try {
|
|
const resp = await fetch('/api/usb-mode', {method: 'POST'});
|
|
const data = await resp.json();
|
|
if (data.status === 'ok') {
|
|
showStatus('USB-Modus aktiv! SD-Karte am PC verfuegbar.');
|
|
alert('USB Mass Storage aktiviert!\n\nVerbinden Sie das USB-Kabel mit Ihrem PC.\nDie SD-Karte erscheint als Wechseldatentraeger.\n\nZum Beenden: Geraet neu starten.');
|
|
} else {
|
|
showStatus('USB-Modus fehlgeschlagen', true);
|
|
}
|
|
} catch (e) {
|
|
showStatus('Fehler beim Aktivieren des USB-Modus', true);
|
|
}
|
|
}
|
|
|
|
function showStatus(msg, isError = false) {
|
|
const el = document.getElementById('status');
|
|
el.textContent = msg;
|
|
el.className = 'status show' + (isError ? ' error' : '');
|
|
setTimeout(() => el.className = 'status', 2000);
|
|
}
|
|
|
|
// Deselect when clicking canvas background
|
|
document.getElementById('canvas').addEventListener('click', (e) => {
|
|
if (e.target.id === 'canvas') {
|
|
selectedWidget = null;
|
|
renderCanvas();
|
|
renderProperties();
|
|
}
|
|
});
|
|
|
|
// Load on start
|
|
loadConfig();
|
|
</script>
|
|
</body>
|
|
</html>
|