Initial commit: StreamDeck Electron app
This commit is contained in:
+485
@@ -0,0 +1,485 @@
|
||||
/* ─── 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user