/* Atlus — X11 Display app (noVNC viewer for virtual desktop) */ (function () { 'use strict'; let container = null; let iframeEl = null; let statusEl = null; let toolbarEl = null; let wsPort = null; let displayStarted = false; // ---- Toolbar buttons state ---- let appListEl = null; let refreshTimer = null; // ---- Known launchable apps ---- const KNOWN_APPS = [ { command: 'nextcloud', name: 'Nextcloud', has_tray_icon: true }, ]; // ---- Display management ---- async function startDisplay() { setStatus('Starting display…', 'info'); try { const res = await Atlus.apiFetch('/api/display/start', { method: 'POST' }); if (!res || !res.ok) { const err = res ? await res.json().catch(() => ({ detail: 'Unknown error' })) : { detail: 'No response' }; setStatus(err.detail || 'Failed to start display', 'error'); return false; } const data = await res.json(); wsPort = data.ws_port; displayStarted = true; return true; } catch (e) { setStatus('Failed to start display: ' + e.message, 'error'); return false; } } async function connectViewer() { if (!displayStarted) { const ok = await startDisplay(); if (!ok) return; } setStatus('Connecting to display…', 'info'); // Use noVNC served by websockify // The websockify --web flag serves noVNC files // Try direct websockify noVNC endpoint first const host = location.hostname; const novncUrl = `http://${host}:${wsPort}/vnc.html?host=${host}&port=${wsPort}&autoconnect=true&resize=scale&reconnect=true&reconnect_delay=1000&view_only=false`; if (iframeEl) { iframeEl.remove(); } iframeEl = document.createElement('iframe'); iframeEl.className = 'xdisplay-iframe'; iframeEl.src = novncUrl; iframeEl.setAttribute('allowfullscreen', 'true'); iframeEl.onload = () => { statusEl.classList.add('hidden'); }; iframeEl.onerror = () => { // If noVNC files aren't served by websockify, fallback to raw WebSocket viewer setStatus('noVNC not available — install novnc package', 'error'); }; const viewerArea = container.querySelector('.xdisplay-viewer'); viewerArea.appendChild(iframeEl); } // ---- App launcher ---- async function launchApp(command, args, name, hasTrayIcon) { try { const res = await Atlus.apiFetch('/api/display/apps/launch', { method: 'POST', body: { command, args: args || [], name: name || command, has_tray_icon: hasTrayIcon || false }, }); if (!res || !res.ok) { const err = res ? await res.json().catch(() => ({})) : {}; console.error('Failed to launch app:', err.detail || 'unknown'); return null; } const data = await res.json(); refreshAppList(); return data; } catch (e) { console.error('Failed to launch app:', e); return null; } } async function stopApp(appId) { try { await Atlus.apiFetch(`/api/display/apps/${appId}`, { method: 'DELETE' }); } catch (e) { /* best effort */ } refreshAppList(); } async function refreshAppList() { if (!appListEl) return; try { const res = await Atlus.apiFetch('/api/display/apps'); if (!res || !res.ok) return; const data = await res.json(); renderAppList(data.apps || []); } catch (e) { /* ignore */ } } function renderAppList(apps) { if (!appListEl) return; appListEl.innerHTML = ''; if (apps.length === 0) { appListEl.innerHTML = 'No apps running'; return; } apps.forEach(app => { const item = document.createElement('div'); item.className = 'xdisplay-app-item'; item.innerHTML = ` ${app.name} `; item.querySelector('.xdisplay-app-stop').addEventListener('click', (e) => { e.stopPropagation(); stopApp(app.app_id); }); appListEl.appendChild(item); }); } // ---- Status ---- function setStatus(message, type) { if (!statusEl) return; statusEl.textContent = message; statusEl.className = 'xdisplay-status'; if (type) statusEl.classList.add(type); statusEl.classList.remove('hidden'); } // ---- App registration ---- Atlus.registerApp('xdisplay', { title: 'Display', init(el) { container = el; container.classList.add('app-xdisplay'); // Toolbar toolbarEl = document.createElement('div'); toolbarEl.className = 'xdisplay-toolbar'; // Launch dropdown const launchGroup = document.createElement('div'); launchGroup.className = 'xdisplay-launch-group'; const launchBtn = document.createElement('button'); launchBtn.className = 'xdisplay-btn xdisplay-launch-btn'; launchBtn.textContent = '+ Launch App'; const launchMenu = document.createElement('div'); launchMenu.className = 'xdisplay-launch-menu hidden'; KNOWN_APPS.forEach(app => { const item = document.createElement('button'); item.className = 'xdisplay-launch-menu-item'; item.textContent = app.name; item.addEventListener('click', () => { launchMenu.classList.add('hidden'); launchApp(app.command, [], app.name, app.has_tray_icon); }); launchMenu.appendChild(item); }); // Custom app option const customItem = document.createElement('button'); customItem.className = 'xdisplay-launch-menu-item'; customItem.textContent = 'Custom…'; customItem.addEventListener('click', () => { launchMenu.classList.add('hidden'); const cmd = prompt('Enter command to launch:'); if (cmd && cmd.trim()) { const parts = cmd.trim().split(/\s+/); launchApp(parts[0], parts.slice(1), parts[0], false); } }); launchMenu.appendChild(customItem); launchBtn.addEventListener('click', () => { launchMenu.classList.toggle('hidden'); }); // Close menu on outside click document.addEventListener('click', (e) => { if (!launchGroup.contains(e.target)) { launchMenu.classList.add('hidden'); } }); launchGroup.appendChild(launchBtn); launchGroup.appendChild(launchMenu); toolbarEl.appendChild(launchGroup); // Running apps list appListEl = document.createElement('div'); appListEl.className = 'xdisplay-app-list'; toolbarEl.appendChild(appListEl); // Connection button (right side) const connectBtn = document.createElement('button'); connectBtn.className = 'xdisplay-btn xdisplay-connect-btn'; connectBtn.textContent = 'Connect'; connectBtn.addEventListener('click', () => connectViewer()); toolbarEl.appendChild(connectBtn); container.appendChild(toolbarEl); // Status statusEl = document.createElement('div'); statusEl.className = 'xdisplay-status'; statusEl.textContent = 'Click Connect to start the virtual display'; // Viewer area const viewerArea = document.createElement('div'); viewerArea.className = 'xdisplay-viewer'; viewerArea.appendChild(statusEl); container.appendChild(viewerArea); // Check if display is already running checkExistingDisplay(); // Poll running apps every 5 seconds refreshTimer = setInterval(refreshAppList, 5000); }, destroy() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } if (iframeEl) { iframeEl.remove(); iframeEl = null; } container = null; toolbarEl = null; statusEl = null; appListEl = null; }, onFocus() { refreshAppList(); }, /** Public API: launch an app from outside (e.g., panel tray click) */ launchApp(command, args, name, hasTrayIcon) { return launchApp(command, args, name, hasTrayIcon); }, /** Public API: connect/show the viewer */ showViewer() { if (!displayStarted) { connectViewer(); } }, }); async function checkExistingDisplay() { try { const res = await Atlus.apiFetch('/api/display/status'); if (!res || !res.ok) return; const data = await res.json(); if (data.missing && data.missing.length > 0) { setStatus( `Missing packages: ${data.missing.join(', ')}. Install with: sudo apt install ${data.missing.map(n => n.toLowerCase()).join(' ')}`, 'error' ); return; } if (data.session && data.session.started) { wsPort = data.session.ws_port; displayStarted = true; connectViewer(); refreshAppList(); } } catch (e) { /* ignore */ } } })();