/** * pc-live | Desktop – boot, icon grid, clock, message router */ /* ── NUI fetch helper ───────────────────────────────────── */ async function fetchNui(event, data = {}) { try { const resp = await fetch(`https://pc-live/${event}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return resp.ok ? await resp.json() : null; } catch { return null; } } /* ── State ──────────────────────────────────────────────── */ const Desktop = (() => { let _session = null; let _clockTimer = null; let _installedApps = []; const APP_META = { 'pc.browser': { name: 'Browser', icon: '🌐', open: () => BrowserApp.open() }, 'pc.mail': { name: 'Mail', icon: '✉', open: () => MailApp.open() }, 'pc.addressbook': { name: 'Adressbuch', icon: '👤', open: () => AddressBookApp.open() }, 'pc.store': { name: 'Software Store', icon: '🛒', open: () => StoreApp.open() }, 'pc.parkuhr': { name: 'Parkuhr Dashboard', icon: '🅿', open: () => ParkuhrApp.open() }, 'pc.ic_verwaltung': { name: 'IC-Verwaltung', icon: '🏛', open: () => ICVerwaltungApp.open() }, 'pc.gesetzbuch': { name: 'Gesetzbuch', icon: '📖', open: () => GesetzbuchApp.open() }, 'pbs.dashboard': { name: 'PBS Dashboard', icon: '📞', open: () => PBSDashboardApp.open() }, 'pc.webhosting': { name: 'Webhosting', icon: '🌍', open: () => WebhostingApp.open() }, 'pc.bleeter': { name: 'Bleeter', icon: '🐦', open: () => BleeterApp.open() }, 'pc.bleeteradmin': { name: 'Bleeter-Verwaltung', icon: '🛠', open: () => BleeterAdminApp.open() }, }; /* ── Boot sequence ────────────────────────────────────── */ function boot(session) { _session = session; MailApp.initMailboxes(session.sharedMailboxes || [], session.personalAddress || '', session.personalSignature || ''); // Freigeschaltete Postfächer nachladen, damit die ✕-Knöpfe stimmen. MailApp.refreshMailboxes(); // Apply accent color if (session.accentColor) { document.documentElement.style.setProperty('--accent', session.accentColor); } const app = document.getElementById('app'); const bootEl = document.getElementById('boot-screen'); const fill = document.getElementById('boot-bar-fill'); const desktopEl = document.getElementById('desktop'); // Set labels in titlebar and boot screen const label = session.pcLabel || 'Terminal'; const pcLabel = document.getElementById('pc-label'); const pcBootLabel = document.getElementById('pc-boot-label'); if (pcLabel) pcLabel.textContent = label; if (pcBootLabel) pcBootLabel.textContent = label; app.classList.add('visible'); // Animate progress bar const duration = 2500; fill.style.transition = `width ${duration}ms linear`; requestAnimationFrame(() => { fill.style.width = '100%'; }); setTimeout(() => { bootEl.classList.add('fade-out'); setTimeout(() => { bootEl.style.display = 'none'; desktopEl.classList.add('visible'); startClock(); renderIcons(session.installedApps || []); // Startseite automatisch öffnen falls konfiguriert if (session.startPage) { BrowserApp.open(session.startPage); } // Auto-Open-App (z.B. mobiler Streifenwagen-Terminal → IC-Verwaltung/Personalakte) if (session.autoOpenApp) { openApp(session.autoOpenApp); } }, 600); }, duration); } /* ── Clock ────────────────────────────────────────────── */ function startClock() { const el = document.getElementById('clock'); const tick = () => { if (!el) return; const now = new Date(); el.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); }; tick(); _clockTimer = setInterval(tick, 1000); } function stopClock() { clearInterval(_clockTimer); _clockTimer = null; } /* ── Desktop Icons ────────────────────────────────────── */ function renderIcons(apps) { _installedApps = apps || []; const grid = document.getElementById('icon-grid'); if (!grid) return; grid.innerHTML = ''; _installedApps.forEach(app => { const meta = APP_META[app.app_id] || {}; const icon = meta.icon || app.icon || '📦'; const name = meta.name || app.name || app.app_id; const div = document.createElement('div'); div.className = 'desktop-icon'; div.innerHTML = `
${icon}
${name}
`; div.addEventListener('dblclick', () => openApp(app.app_id)); grid.appendChild(div); }); } function openApp(appId) { const meta = APP_META[appId]; if (meta && meta.open) { meta.open(); } else { showNotification('⚠ App not yet available'); } } /* ── Close ────────────────────────────────────────────── */ function close() { WindowManager.closeAll(); stopClock(); const app = document.getElementById('app'); const bootEl = document.getElementById('boot-screen'); const desktopEl = document.getElementById('desktop'); const fill = document.getElementById('boot-bar-fill'); const grid = document.getElementById('icon-grid'); const taskbar = document.getElementById('taskbar-apps'); const container = document.getElementById('pc-container'); const minBtn = document.getElementById('pc-minimize-btn'); desktopEl.classList.remove('visible'); bootEl.style.display = 'flex'; bootEl.classList.remove('fade-out'); fill.style.transition = ''; fill.style.width = '0%'; if (grid) grid.innerHTML = ''; if (taskbar) taskbar.innerHTML = ''; // Reset PC container position and minimized state if (container) { container.classList.remove('minimized'); container.style.left = '50%'; container.style.top = '50%'; container.style.transform = 'translate(-50%, -50%)'; } if (minBtn) { minBtn.textContent = '─'; minBtn.title = 'Minimize'; } app.classList.remove('visible'); app.classList.remove('interact-mode'); _session = null; } /* ── Message handler ───────────────────────────────────── */ window.addEventListener('message', (e) => { const { action, data } = e.data || {}; switch (action) { case 'boot': boot(data); break; case 'close': close(); break; case 'exitInteract': exitInteractMode(); break; case 'setInstalledApps': renderIcons(data); break; case 'setAccentColor': document.documentElement.style.setProperty('--accent', data.color); break; // Mail case 'setInbox': MailApp.onInbox(data); break; case 'setMailContent': MailApp.onMailContent(data); break; case 'mailSent': MailApp.onMailSent(); break; case 'mailDeleted': MailApp.onMailDeleted(data.id); break; case 'newMailNotify': // Show unread badge on desktop icon (quick visual cue) showNotification('📧 New mail received'); break; // Store case 'setStoreList': StoreApp.onStoreData(data); break; // Mail extended case 'setSent': MailApp.onSetSent(data); break; case 'folderCreated': MailApp.onFolderCreated(data); break; case 'folderDeleted': MailApp.onFolderDeleted(data.id, data.address); break; case 'foldersData': MailApp.onFoldersData(data.address, data.folders); break; case 'folderMailsData': MailApp.onFolderMailsData(data.folderId, data.rows); break; case 'mailMoved': MailApp.onMailMoved(data.id, data.folderId); break; // Geteilte Postfächer case 'sharedMailboxes': MailApp.onSharedMailboxes(data.mailboxes, data.personalAddress, data.personalSignature); break; case 'signatureSaved': MailApp.onSignatureSaved(data); break; // Mail-Einrichtung case 'mailAddressStatus': MailApp.onMailAddressStatus(data); break; case 'mailCreateResult': MailApp.onMailCreateResult(data); break; // Calendar case 'calendarData': MailApp.onCalendarData(data); break; case 'calendarEventAdded': MailApp.onCalendarEventAdded(data); break; case 'calendarEventUpdated': MailApp.onCalendarEventUpdated(data); break; case 'calendarEventDeleted': MailApp.onCalendarEventDeleted(data.id); break; // Contacts case 'contactsData': AddressBookApp.onContactsData(data); break; case 'contactAdded': AddressBookApp.onContactAdded(data); break; case 'contactUpdated': AddressBookApp.onContactUpdated(data); break; case 'contactDeleted': AddressBookApp.onContactDeleted(data.id); break; // IC-Verwaltung relay: forward Lua SendNUIMessage into the iframe case 'ic_verwaltung_relay': ICVerwaltungApp.onRelay(data); break; // Webhosting (ic-web): Antwort auf eine Anfrage mit Anfrage-Id case 'icweb_response': ICWebNet.onResponse(data); break; // Bleeter schickt alles über einen Kanal: Zustand und Verwaltungsantworten. case 'bleeter_data': BleeterApp.onData(data); break; case 'bleeter_notify': BleeterApp.onNotify(data); break; case 'bleeter_uploaded': BleeterApp.onUploaded(data); break; } }); /* ── Keyboard: Escape closes PC ────────────────────────── */ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.getElementById('desktop')?.classList.contains('visible')) { doClosePC(); } }); /* ── Notification toast ─────────────────────────────────── */ function showNotification(msg) { const el = document.createElement('div'); el.style.cssText = ` position:fixed;top:50px;right:16px;z-index:9000; background:var(--bg-window);border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:var(--radius);padding:10px 14px; font-size:.78rem;color:var(--text); box-shadow:var(--shadow); animation:fadeIn .2s ease; `; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 4000); } return { boot, close, openApp, showNotification, get session() { return _session; } }; })(); /* ── Close button ───────────────────────────────────────── */ function doClosePC() { fetchNui('closePC', {}); } /* ── PC Container: Minimize / Maximize ─────────────────── */ function toggleMinimizePC() { const container = document.getElementById('pc-container'); const btn = document.getElementById('pc-minimize-btn'); if (!container) return; const isMin = container.classList.contains('minimized'); container.classList.toggle('minimized', !isMin); if (btn) { btn.textContent = isMin ? '─' : '□'; btn.title = isMin ? 'Minimize' : 'Restore'; } } /* ── Interaktionsmodus: PC transparent + Fokus zurück ins Spiel ── */ function toggleInteractMode() { const app = document.getElementById('app'); const btn = document.getElementById('pc-interact-btn'); if (!app) return; const on = !app.classList.contains('interact-mode'); app.classList.toggle('interact-mode', on); if (btn) btn.classList.toggle('active', on); // interact AN -> NUI-Fokus AUS (man interagiert mit dem Spiel); AUS -> Fokus zurück auf den PC fetchNui('setFocus', { focus: !on }); } function exitInteractMode() { const app = document.getElementById('app'); const btn = document.getElementById('pc-interact-btn'); if (app) app.classList.remove('interact-mode'); if (btn) btn.classList.remove('active'); } /* ── PC Size (viewport %) ────────────────────────────────── */ const PCScale = (() => { const STEP = 5; const MIN = 40; const MAX = 98; const KEY = 'pc_live_vp'; let _vp = parseInt(localStorage.getItem(KEY) || '88', 10); function apply() { _vp = Math.max(MIN, Math.min(MAX, _vp)); document.documentElement.style.setProperty('--pc-vp', _vp); localStorage.setItem(KEY, _vp); const c = document.getElementById('pc-container'); if (c) { c.style.left = '50%'; c.style.top = '50%'; c.style.transform = 'translate(-50%, -50%)'; } const disp = document.getElementById('pc-vp-display'); const slider = document.getElementById('pc-vp-slider'); if (disp) disp.textContent = _vp + '%'; if (slider) slider.value = _vp; } function step(dir) { _vp += dir * STEP; apply(); } function setVP(v) { _vp = parseInt(v, 10); apply(); } document.addEventListener('wheel', (e) => { if (!e.ctrlKey) return; e.preventDefault(); step(e.deltaY < 0 ? 1 : -1); }, { passive: false }); apply(); return { step, setVP }; })(); /* ── PC Font Size ─────────────────────────────────────────── */ const PCFontSize = (() => { const STEP = 1; const MIN = 11; const MAX = 20; const KEY = 'pc_live_fs'; let _fs = parseInt(localStorage.getItem(KEY) || '13', 10); function apply() { _fs = Math.max(MIN, Math.min(MAX, _fs)); document.documentElement.style.setProperty('--pc-fs', _fs); localStorage.setItem(KEY, _fs); const disp = document.getElementById('pc-fs-display'); const slider = document.getElementById('pc-fs-slider'); if (disp) disp.textContent = _fs + 'px'; if (slider) slider.value = _fs; } function step(dir) { _fs += dir * STEP; apply(); } function set(v) { _fs = parseInt(v, 10); apply(); } apply(); return { step, set }; })(); /* ── Settings Panel ───────────────────────────────────────── */ const PCSettings = (() => { let _open = false; function toggle() { _open = !_open; const panel = document.getElementById('pc-settings-panel'); const btn = document.getElementById('pc-settings-btn'); if (panel) panel.classList.toggle('open', _open); if (btn) btn.classList.toggle('active', _open); // Sync slider values when opening if (_open) { const fsSlider = document.getElementById('pc-fs-slider'); const vpSlider = document.getElementById('pc-vp-slider'); if (fsSlider) fsSlider.value = parseInt(localStorage.getItem('pc_live_fs') || '13', 10); if (vpSlider) vpSlider.value = parseInt(localStorage.getItem('pc_live_vp') || '88', 10); } } function close() { _open = false; const panel = document.getElementById('pc-settings-panel'); const btn = document.getElementById('pc-settings-btn'); if (panel) panel.classList.remove('open'); if (btn) btn.classList.remove('active'); } // Schließen bei Klick außerhalb document.addEventListener('click', (e) => { if (!_open) return; const panel = document.getElementById('pc-settings-panel'); const btn = document.getElementById('pc-settings-btn'); if (panel && !panel.contains(e.target) && btn && !btn.contains(e.target)) { close(); } }); return { toggle, close }; })(); /* ── PC Container: Draggable via Titelleiste ────────────── */ (function initPCDrag() { const container = document.getElementById('pc-container'); const titlebar = document.getElementById('pc-titlebar'); if (!container || !titlebar) return; let startX, startY, startL, startT; titlebar.addEventListener('mousedown', (e) => { // Ignore clicks on buttons if (e.target.closest('#pc-minimize-btn, #close-pc-btn, #pc-settings-btn, #pc-settings-panel')) return; // Convert centered transform to explicit pixel position const rect = container.getBoundingClientRect(); container.style.left = rect.left + 'px'; container.style.top = rect.top + 'px'; container.style.transform = 'none'; startX = e.clientX; startY = e.clientY; startL = rect.left; startT = rect.top; const onMove = (e2) => { const newL = startL + e2.clientX - startX; const newT = startT + e2.clientY - startY; const maxL = window.innerWidth - container.offsetWidth; const maxT = window.innerHeight - container.offsetHeight; container.style.left = Math.max(0, Math.min(maxL, newL)) + 'px'; container.style.top = Math.max(0, Math.min(maxT, newT)) + 'px'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); })();