119 lines
6.5 KiB
JavaScript
119 lines
6.5 KiB
JavaScript
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 });
|