import { initMusic } from './music.js'; import { initInstruments } from './instruments.js'; import { applyTheme, accents } from './theme.js'; import { initJourney } from './journey.js'; import { initCockpit } from './cockpit.js'; import { initEmbeds } from './embeds.js'; import { initApplications, applications } from './applications.js'; const $ = selector => document.querySelector(selector); const all = selector => document.querySelectorAll(selector); const storage = { get(key, fallback = '') { try { return localStorage.getItem(`caria.${key}`) ?? fallback; } catch { return fallback; } }, set(key, value) { try { localStorage.setItem(`caria.${key}`, value); return true; } catch { return false; } } }; function toast(message) { $('#toast').textContent = message; $('#toast').hidden = false; clearTimeout(toast.timer); toast.timer = setTimeout(() => $('#toast').hidden = true, 4000); } function navigate() { const titles = { home: 'Accueil', drive: 'Conduite', music: 'Musique', stream: 'Jellyfin', map: 'Navigation', apps: 'Applications', 'vehicle-details': 'Détails véhicule', 'now-playing': 'Lecture', embedded: 'Lecteur intégré' }; const view = Object.hasOwn(titles, location.hash.slice(1)) ? location.hash.slice(1) : 'home'; all('.view').forEach(section => section.hidden = section.id !== view); all('[data-view]').forEach(link => { const active = link.dataset.view === view; link.classList.toggle('active', active); if (active) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current'); }); document.title = `CARIA · ${titles[view]}`; document.dispatchEvent(new CustomEvent('caria-view', { detail: view })); } window.addEventListener('hashchange', navigate); navigate(); function clock() { const date = new Date(); const time = date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); ['#hero-clock', '#header-clock'].forEach(id => { $(id).textContent = time; $(id).dateTime = date.toISOString(); }); $('#date').textContent = date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } clock(); setInterval(clock, 1000); const { speedGauge, rpmGauge, updateLevels } = initInstruments(); const savedProfile = storage.get('profile', 'route'); $('#profile').value = ['route', 'ville', 'arret'].includes(savedProfile) ? savedProfile : 'route'; let generation = 0, timer; function offline() { $('#connection').textContent = 'Données indisponibles'; $('#connection').classList.add('error'); $('#footer-status').textContent = 'Connexion perdue · tentative de reconnexion'; document.body.classList.add('stale'); all('[data-value]').forEach(el => el.textContent = '—'); all('[data-fuel]').forEach(el => el.style.width = '0%'); updateLevels(null); speedGauge(null); rpmGauge(null); } async function poll() { const current = ++generation; clearTimeout(timer); try { const response = await fetch(`/api/telemetry?profile=${encodeURIComponent($('#profile').value)}`, { signal: AbortSignal.timeout(3000), cache: 'no-store' }); if (!response.ok) throw new Error('API'); const data = await response.json(); if (current !== generation) return; const numeric = ['speedKph', 'rpm', 'fuelPercent', 'coolantCelsius', 'batteryVolts', 'outsideCelsius', 'rangeKm', 'tripKm', 'consumptionL100']; if (data.schemaVersion !== 1 || data.source !== 'demo' || !Number.isFinite(data.timestamp) || Math.abs(Date.now() - data.timestamp) > 10000 || numeric.some(key => !Number.isFinite(data[key]))) throw new Error('Données invalides'); $('#connection').textContent = '● Démonstration'; $('#connection').classList.remove('error'); document.body.classList.remove('stale'); $('#footer-status').textContent = 'Mode démonstration · CAN non connecté'; all('[data-value]').forEach(el => { const value = data[el.dataset.value]; el.textContent = typeof value === 'number' ? value.toLocaleString('fr-FR', { maximumFractionDigits: 1 }) : value; }); all('[data-fuel]').forEach(el => el.style.width = `${Math.max(0, Math.min(100, data.fuelPercent))}%`); updateLevels(data); speedGauge(data.speedKph); rpmGauge(data.rpm); } catch { if (current === generation) offline(); } finally { if (current === generation) timer = setTimeout(poll, 500); } } $('#profile').addEventListener('change', () => { storage.set('profile', $('#profile').value); poll(); }); window.addEventListener('storage', event => { if (event.key === 'caria.profile' && ['route', 'ville', 'arret'].includes(event.newValue)) { $('#profile').value = event.newValue; poll(); } if (['caria.theme', 'caria.accent', 'caria.customColor', 'caria.dialStyle'].includes(event.key)) applyTheme(storage); }); poll(); const jellyfinUrl = applications.find(app => app.id === 'stream').url; $('#stream-link').hidden = false; $('#stream-link').href = jellyfinUrl; $('#stream-description').textContent = new URL(jellyfinUrl).hostname; applyTheme(storage); function openSettings() { $('#theme').value = document.body.classList.contains('light') ? 'light' : 'dark'; $('#accent').value = storage.get('accent', 'green'); $('#dial-style').value = ['digital', 'luminous'].includes(storage.get('dialStyle')) ? storage.get('dialStyle') : 'analog'; $('#custom-color').value = storage.get('customColor', accents.green); $('#settings-message').textContent = ''; $('#settings').showModal(); } $('#custom-color').addEventListener('input', () => { $('#accent').value = 'custom'; }); $('#settings-open').onclick = openSettings; $('#settings-close').onclick = () => $('#settings').close(); $('#settings-form').addEventListener('submit', event => { event.preventDefault(); const dialSaved = storage.set('dialStyle', $('#dial-style').value); const themeSaved = storage.set('theme', $('#theme').value); const accentSaved = storage.set('accent', $('#accent').value), colorSaved = storage.set('customColor', $('#custom-color').value); if (!dialSaved || !themeSaved || !accentSaved || !colorSaved) { $('#settings-message').textContent = 'Le navigateur bloque l’enregistrement local des réglages.'; return; } applyTheme(storage); $('#settings').close(); toast('Réglages enregistrés'); }); $('#fullscreen').onclick = async () => { try { if (document.fullscreenElement) await document.exitFullscreen(); else await document.documentElement.requestFullscreen(); } catch { toast('Le plein écran n’est pas disponible dans ce navigateur.'); } }; initMusic({ storage, toast }); initJourney({ storage, toast }); initApplications({ storage, toast, navigate }); initEmbeds({ storage, toast }); initCockpit({ storage, toast });