/**
* 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);
});
})();