/* ─── 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 = `
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 += ``;
});
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();
})();