486 lines
18 KiB
JavaScript
486 lines
18 KiB
JavaScript
/* ─── 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 = `
|
||
<div class="monitor-section" id="monitor-cpu">
|
||
<div class="monitor-title">CPU</div>
|
||
<div class="monitor-bar-bg"><div class="monitor-bar monitor-bar-cpu" id="cpu-bar" style="width:0%"></div></div>
|
||
<div class="monitor-value" id="cpu-value">—</div>
|
||
<div class="monitor-cores" id="cpu-cores"></div>
|
||
</div>
|
||
<div class="monitor-section" id="monitor-mem">
|
||
<div class="monitor-title">MEMORY</div>
|
||
<div class="monitor-bar-bg"><div class="monitor-bar monitor-bar-mem" id="mem-bar" style="width:0%"></div></div>
|
||
<div class="monitor-value" id="mem-value">—</div>
|
||
</div>
|
||
<div class="monitor-section" id="monitor-sys">
|
||
<div class="monitor-title">SYSTEM</div>
|
||
<div class="monitor-line" id="monitor-uptime">Uptime: —</div>
|
||
<div class="monitor-line" id="monitor-load">Load: —</div>
|
||
<div class="monitor-line" id="monitor-host">Host: —</div>
|
||
</div>
|
||
`;
|
||
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 = '<div class="monitor-title">CPU</div><div style="color:#666;padding:10px">Only available in Electron mode</div>');
|
||
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 += `<div class="core-bar-bg"><span class="core-label">C${i}</span><div class="core-bar-fill" style="width:${pct}%"></div><span class="core-pct">${pct}%</span></div>`;
|
||
});
|
||
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 <span style="color:${color}">(${stats.memory.usagePercent}%)</span>`;
|
||
}
|
||
|
||
// 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 += `<div>${entry}</div>`;
|
||
debugEl.scrollTop = debugEl.scrollHeight;
|
||
if (++debugCount > 100) { debugEl.innerHTML = debugEl.innerHTML.split('</div>').slice(-50).join('</div>'); }
|
||
};
|
||
|
||
// 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();
|
||
})();
|