/* ─── State ────────────────────────────────────────────────────────────────── */ const IS_ELECTRON = typeof window !== 'undefined' && window.electronAPI !== undefined; let config = { pages: [], gridCols: 5, gridRows: 3 }; let currentPage = 0; let ws = null; let wsReconnectTimer = null; let btnExecuting = {}; // { id: setTimeout } /* ─── DOM refs ────────────────────────────────────────────────────────────── */ const gridEl = document.getElementById('grid'); const navEl = document.getElementById('nav'); const titleEl = document.getElementById('page-title'); const wsStatusEl = document.getElementById('ws-status'); const pageIndicatorEl = document.getElementById('page-indicator'); const navPrevEl = document.getElementById('nav-prev'); const navNextEl = document.getElementById('nav-next'); /* ─── Config + Monitor page ───────────────────────────────────────────────── */ // A Monitor page index: mindig az utolsó oldal (config.pages.length után hozzáfűzve) const MONITOR_PAGE_LABEL = '📊'; async function loadConfig() { try { if (IS_ELECTRON) { config = await window.electronAPI.getConfig(); } else { const res = await fetch('/api/config'); config = await res.json(); } if (!config.pages || config.pages.length === 0) { config.pages = [{ title: 'Hello', buttons: [] }]; } // Virtuális Monitor oldal hozzáfűzése (nem mentjük config.json-ba) config.pages.push({ title: 'Monitor', buttons: [], _isMonitor: true, }); return true; } catch (e) { console.error('Failed to load config:', e); return false; } } /* ─── WebSocket (böngésző módban) ────────────────────────────────────────── */ function connectWS() { if (IS_ELECTRON) { // Electron: nincs szükség WS-re document.body.classList.add('connected'); wsStatusEl.title = 'Electron app – natív'; wsStatusEl.className = 'ws-online'; return; } if (ws && ws.readyState === WebSocket.OPEN) return; const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${proto}//${location.host}`); ws.onopen = () => { console.log('◉ WebSocket connected'); if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; } document.body.classList.add('connected'); wsStatusEl.className = 'ws-online'; }; ws.onclose = () => { console.log('◉ WebSocket disconnected'); document.body.classList.remove('connected'); wsStatusEl.className = 'ws-offline'; ws = null; wsReconnectTimer = setTimeout(connectWS, 2000); }; ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === 'connected') console.log(' ✓', msg.message); } catch (e) { /* ignore */ } }; ws.onerror = () => { /* onclose will fire next */ }; } /* ─── Send press ──────────────────────────────────────────────────────────── */ function sendPress(button) { const actions = button.actions || []; if (actions.length === 0) return; if (IS_ELECTRON) { // Elektron: natív IPC-n keresztül window.electronAPI.executeActions(actions).catch(console.error); } else if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'press', actions: actions, id: button.id || button.label })); } // Visual feedback const btnId = button.id || button.label; if (btnExecuting[btnId]) clearTimeout(btnExecuting[btnId]); delete btnExecuting[btnId]; } /* ─── Render grid (or monitor dashboard) ─────────────────────────────────── */ let monitorInterval = null; function renderPage(pageIdx) { const page = config.pages[pageIdx]; if (!page) return; currentPage = pageIdx; titleEl.textContent = page.title || ''; // Ha Monitor oldal, rendereld a dashboard-ot if (page._isMonitor) { renderMonitorPage(); renderNav(); updateNavArrows(); updatePageIndicator(); return; } // Biztonság: ha volt monitor poll, állítsd le stopMonitorPoll(); // Build grid gridEl.innerHTML = ''; gridEl.style.gridTemplateColumns = `repeat(${config.gridCols || 5}, 1fr)`; gridEl.style.gridTemplateRows = `repeat(${config.gridRows || 3}, 1fr)`; gridEl.classList.remove('drag-scroll'); const buttons = page.buttons || []; buttons.forEach((btn, i) => { const el = document.createElement('button'); el.className = 'button'; el.dataset.index = i; // Color if (btn.color) el.classList.add(`color-${btn.color}`); // Icon if (btn.icon) { const iconSpan = document.createElement('span'); iconSpan.className = 'icon'; iconSpan.textContent = btn.icon; el.appendChild(iconSpan); } // Label if (btn.label) { const labelSpan = document.createElement('span'); labelSpan.className = 'label'; labelSpan.textContent = btn.label; el.appendChild(labelSpan); } // Background image if (btn.image) { el.style.backgroundImage = `url(${btn.image})`; el.style.backgroundSize = 'cover'; el.style.backgroundPosition = 'center'; } // ── Event handlers (multiple redundancies for different touchscreen drivers) ── function fireAction(e) { // Log to debug overlay if (window.debugLog) window.debugLog(e.type, btn.label || '(no label)'); // Visual feedback el.classList.add('pressed'); setTimeout(() => el.classList.remove('pressed'), 150); // Send action to server sendPress(btn); } el.addEventListener('pointerdown', fireAction); el.addEventListener('mousedown', fireAction); el.addEventListener('click', (e) => { e.preventDefault(); fireAction(e); }); el.addEventListener('touchstart', (e) => { // Some touchscreens ONLY send touchstart fireAction(e); }, { passive: true }); // Colspan / rowspan (via CSS grid) if (btn.colspan) el.style.gridColumn = `span ${btn.colspan}`; if (btn.rowspan) el.style.gridRow = `span ${btn.rowspan}`; // Size classes for easier styling if (btn.width) el.style.width = btn.width; if (btn.height) el.style.height = btn.height; // Empty button (spacer) if (btn.empty) { el.style.visibility = 'hidden'; el.style.pointerEvents = 'none'; } gridEl.appendChild(el); }); // Navigation dots, arrows, page indicator renderNav(); updateNavArrows(); updatePageIndicator(); } /* ─── Monitor dashboard ───────────────────────────────────────────────────── * Realtime CPU és memória grafikonok. Csak Electron módban működik. * ──────────────────────────────────────────────────────────────────────────── */ function renderMonitorPage() { gridEl.innerHTML = ''; gridEl.style.gridTemplateColumns = '1fr'; gridEl.style.gridTemplateRows = '1fr'; gridEl.classList.remove('drag-scroll'); // Container const dash = document.createElement('div'); dash.id = 'monitor-dashboard'; dash.innerHTML = `
CPU
MEMORY
SYSTEM
Uptime: —
Load: —
Host: —
`; gridEl.appendChild(dash); // Start polling startMonitorPoll(); } function stopMonitorPoll() { if (monitorInterval) { clearInterval(monitorInterval); monitorInterval = null; } } function startMonitorPoll() { stopMonitorPoll(); if (!IS_ELECTRON) { document.getElementById('monitor-cpu') && (document.getElementById('monitor-cpu').innerHTML = '
CPU
Only available in Electron mode
'); return; } async function update() { try { const stats = await window.electronAPI.getSystemStats(); if (!stats) return; // CPU const cpuEl = document.getElementById('cpu-bar'); const cpuVal = document.getElementById('cpu-value'); const cpuCores = document.getElementById('cpu-cores'); if (cpuEl) cpuEl.style.width = Math.min(stats.cpu.usage, 100) + '%'; if (cpuVal) cpuVal.textContent = stats.cpu.usage.toFixed(1) + '%'; if (cpuCores && stats.cpu.perCore) { let html = ''; stats.cpu.perCore.forEach((core, i) => { const pct = Math.min(core.usage || 0, 100); html += `
C${i}
${pct}%
`; }); cpuCores.innerHTML = html; } // Memory const memEl = document.getElementById('mem-bar'); const memVal = document.getElementById('mem-value'); if (memEl) memEl.style.width = Math.min(stats.memory.usagePercent, 100) + '%'; if (memVal) { const usedGB = (stats.memory.used / 1024 / 1024 / 1024).toFixed(1); const totalGB = (stats.memory.total / 1024 / 1024 / 1024).toFixed(1); const color = stats.memory.usagePercent > 85 ? '#f87171' : stats.memory.usagePercent > 65 ? '#fbbf24' : '#4ade80'; memVal.innerHTML = `${usedGB} GB / ${totalGB} GB (${stats.memory.usagePercent}%)`; } // System info const uptimeEl = document.getElementById('monitor-uptime'); const loadEl = document.getElementById('monitor-load'); const hostEl = document.getElementById('monitor-host'); if (uptimeEl) { const days = Math.floor(stats.uptime / 86400); const hrs = Math.floor((stats.uptime % 86400) / 3600); const mins = Math.floor((stats.uptime % 3600) / 60); uptimeEl.textContent = `Uptime: ${days}d ${hrs}h ${mins}m`; } if (loadEl) { loadEl.textContent = `Load: ${stats.loadavg.map(v => v.toFixed(2)).join(', ')}`; } if (hostEl) { hostEl.textContent = `Host: ${stats.hostname}`; } } catch (e) { console.error('Monitor update error:', e); } } update(); monitorInterval = setInterval(update, 2000); } function renderNav() { navEl.innerHTML = ''; config.pages.forEach((page, i) => { const dot = document.createElement('button'); dot.className = 'nav-dot'; dot.dataset.index = i; if (i === currentPage) dot.classList.add('active'); function gotoPage(e) { e.preventDefault(); renderPage(i); } dot.addEventListener('click', gotoPage); dot.addEventListener('touchstart', (e) => { // For touchscreen drivers that only send touchstart, not click gotoPage(e); }, { passive: true }); navEl.appendChild(dot); }); } /* ─── Fullscreen toggle ──────────────────────────────────────────────────── */ document.getElementById('fullscreen-btn').addEventListener('click', async (e) => { e.preventDefault(); if (IS_ELECTRON) { await window.electronAPI.toggleFullscreen(); } else if (!document.fullscreenElement) { await document.documentElement.requestFullscreen().catch(() => {}); } else { await document.exitFullscreen().catch(() => {}); } }); // Auto-fullscreen Electron indításkor if (IS_ELECTRON) { window.electronAPI.toggleFullscreen(); } else { // Böngésző: első touch-ra fullscreen document.addEventListener('touchstart', () => { if (!document.fullscreenElement && document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen().catch(() => {}); } }, { once: true }); } function updateNavArrows() { navPrevEl.disabled = currentPage <= 0; navNextEl.disabled = currentPage >= config.pages.length - 1; navPrevEl.style.opacity = navPrevEl.disabled ? '0.15' : '0.3'; navNextEl.style.opacity = navNextEl.disabled ? '0.15' : '0.3'; } function updatePageIndicator() { pageIndicatorEl.textContent = `${currentPage + 1} / ${config.pages.length}`; } // ─── Navigation arrow handlers ────────────────────────────────────────────── function goToPrevPage(e) { if (currentPage > 0) renderPage(currentPage - 1); } function goToNextPage(e) { if (currentPage < config.pages.length - 1) renderPage(currentPage + 1); } navPrevEl.addEventListener('click', goToPrevPage); navPrevEl.addEventListener('touchstart', (e) => { goToPrevPage(e); }, { passive: true }); navNextEl.addEventListener('click', goToNextPage); navNextEl.addEventListener('touchstart', (e) => { goToNextPage(e); }, { passive: true }); /* ─── Swipe gesture for page navigation ──────────────────────────────────── * Egyszerűsített verzió: NEM vár pointermove/touchmove eseményekre. * Közvetlenül a felengedéskor méri a távolságot a lenyomás pontjától. * Ez azért kell, mert egyes touchscreen-ek (pl. WCH USB touch) NEM * küldenek mozgás eseményeket (pointermove/touchmove). * ──────────────────────────────────────────────────────────────────────────── */ let swipeStartX = 0; let swipeStartY = 0; let swipeDownTime = 0; let swipeTarget = null; function handleSwipeEnd(endX, endY, target) { const dx = endX - swipeStartX; const dy = endY - swipeStartY; const elapsed = Date.now() - swipeDownTime; // Nem swipe ha: túl kis távolság (>20px), vagy túl lassú (>500ms), // vagy nem elég vízszintes, vagy gombról indult if (Math.abs(dx) < 20 || elapsed > 500) return; if (Math.abs(dx) < Math.abs(dy) * 1.5) return; // nem elég vízszintes if (target && target.closest('.button')) return; // gombról indult if (dx > 0 && currentPage > 0) renderPage(currentPage - 1); else if (dx < 0 && currentPage < config.pages.length - 1) renderPage(currentPage + 1); } // Pointer events – minden touchscreen ezt küldi (UPDD driver is) document.addEventListener('pointerdown', (e) => { swipeStartX = e.screenX; swipeStartY = e.screenY; swipeDownTime = Date.now(); swipeTarget = e.target; }, { passive: true }); document.addEventListener('pointerup', (e) => { handleSwipeEnd(e.screenX, e.screenY, swipeTarget); }, { passive: true }); // Touch events – egyes eszközök ezt küldik pointer helyett document.addEventListener('touchstart', (e) => { swipeStartX = e.changedTouches[0].screenX; swipeStartY = e.changedTouches[0].screenY; swipeDownTime = Date.now(); swipeTarget = e.target; }, { passive: true }); document.addEventListener('touchend', (e) => { handleSwipeEnd( e.changedTouches[0].screenX, e.changedTouches[0].screenY, swipeTarget ); }, { passive: true }); // ← → keyboard nav document.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight' && currentPage < config.pages.length - 1) renderPage(currentPage + 1); if (e.key === 'ArrowLeft' && currentPage > 0) renderPage(currentPage - 1); }); /* ─── Document-level event logger (for troubleshooting touch) ──────────── */ function docLog(e) { window.debugLog(`📄 ${e.type}`, `target:${e.target?.tagName} pt:${e.pointerType || '-'} btn:${e.which || e.button || 0}`); } document.addEventListener('pointerdown', docLog); document.addEventListener('pointerup', docLog); document.addEventListener('mousedown', docLog); document.addEventListener('mouseup', docLog); document.addEventListener('click', docLog); document.addEventListener('touchstart', docLog); document.addEventListener('touchend', docLog); /* ─── Debug overlay ──────────────────────────────────────────────────────── */ const debugEl = document.getElementById('debug-overlay'); let debugCount = 0; window.debugLog = function(type, label) { const now = new Date(); const t = now.toLocaleTimeString(); const entry = `[${t}] ${type.padEnd(15)} ${label || ''}`; debugEl.innerHTML += `
${entry}
`; debugEl.scrollTop = debugEl.scrollHeight; if (++debugCount > 100) { debugEl.innerHTML = debugEl.innerHTML.split('').slice(-50).join(''); } }; // Press F2 to toggle debug overlay document.addEventListener('keydown', (e) => { if (e.key === 'F2') { debugEl.style.display = debugEl.style.display === 'none' ? 'block' : 'none'; } }); /* ─── Init ────────────────────────────────────────────────────────────────── */ (async function init() { await loadConfig(); renderPage(0); connectWS(); })();