Files
CARIA-AUTOMOTIVE/ui/app.js
T

119 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });