Files
StreamDeck/public/app.js
T

513 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ─── 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);
});
/* ─── Clipboard result toast ────────────────────────────────────────────── */
const toastEl = document.getElementById('toast');
let toastTimer = null;
function showToast(msg, duration) {
duration = duration || 2000;
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.style.display = 'block';
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastEl.style.display = 'none';
}, duration);
}
if (IS_ELECTRON) {
window.electronAPI.onClipboardResult((data) => {
if (data.action === 'copy' && data.text) {
const short = data.text.length > 60 ? data.text.substring(0, 57) + '…' : data.text;
showToast(`Copied: "${short}"`, 2500);
} else if (data.action === 'cut' && data.text) {
const short = data.text.length > 60 ? data.text.substring(0, 57) + '…' : data.text;
showToast(`Cut: "${short}"`, 2500);
}
});
}
/* ─── 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();
})();