Amélioration avec des fonctions d'un vrai dashboard automotive et containerisation du projet pour faciliter le déploiement

This commit is contained in:
Christian CUNAT-BRULE
2026-09-12 21:13:24 +02:00
parent d170f95c92
commit d8eb7f4375
31 changed files with 2233 additions and 65 deletions
+146
View File
@@ -0,0 +1,146 @@
export const applications = [
{ id: 'home', name: 'Accueil', icon: '⌂', description: 'Heure, météo et votre espace à bord.' },
{ id: 'drive', name: 'Conduite', icon: '◉', description: 'Les instruments de votre véhicule.' },
{ id: 'map', name: 'Navigation', icon: '⌖', description: 'Carte, itinéraires et zones de vigilance.' },
{ id: 'music', name: 'Musique locale', icon: '♫', description: 'Vos morceaux sur cet appareil.' },
{ id: 'stream', name: 'Jellyfin', icon: '▷', external: true, url: 'https://media.cunatbrule.fr/web/', description: 'Votre bibliothèque multimédia personnelle.' },
{ id: 'spotify', name: 'Spotify', icon: '≋', external: true, url: 'https://open.spotify.com/', description: 'Ouvrir le lecteur Web Spotify.' },
{ id: 'youtube', name: 'YouTube', icon: '▶', external: true, url: 'https://www.youtube.com/', description: 'Vidéos, chaînes et playlists.' },
{ id: 'youtube-music', name: 'YouTube Music', icon: '◉', external: true, url: 'https://music.youtube.com/', description: 'Votre musique sur YouTube Music.' },
{ id: 'waze', name: 'Waze', icon: '⌁', external: true, url: 'https://waze.com/ul?utm_source=caria', description: 'Ouvrir Waze ou lui transmettre votre destination.' }
];
const ids = new Set(applications.map(a => a.id));
export function normalizePreferences(value) {
const raw = value && typeof value === 'object' ? value : {};
const hidden = [...new Set(Array.isArray(raw.hidden) ? raw.hidden.filter(id => ids.has(id)) : [])];
const seen = new Set();
const favorites = Array.from({ length: 4 }, (_, i) => {
const id = (Array.isArray(raw.favorites) ? raw.favorites : ['home', 'drive', 'map', 'music'])[i];
if (!ids.has(id) || hidden.includes(id) || seen.has(id)) return null;
seen.add(id); return id;
});
const modes = Object.fromEntries(applications.filter(a => a.external).map(a => [a.id, raw.modes?.[a.id] === 'same' ? 'same' : raw.modes?.[a.id] === 'embed' && a.id !== 'waze' ? 'embed' : 'new']));
return { hidden, favorites, modes };
}
export function wazeUrl(destination) {
const url = new URL('https://waze.com/ul'); url.searchParams.set('utm_source', 'caria');
if (destination && Number.isFinite(destination.lat) && Number.isFinite(destination.lon) && Math.abs(destination.lat) <= 90 && Math.abs(destination.lon) <= 180) {
url.searchParams.set('ll', `${destination.lat},${destination.lon}`); url.searchParams.set('navigate', 'yes');
}
return url.href;
}
export function safeWebUrl(value) {
try { const u = new URL(value); return ['https:', 'http:'].includes(u.protocol) && !u.username && !u.password ? u.href : ''; } catch { return ''; }
}
export function initApplications({ storage, toast, navigate }) {
const $ = s => document.querySelector(s);
let preferences, destination = null;
function load() { let raw; try { raw = JSON.parse(storage.get('applications', 'null')); } catch {} preferences = normalizePreferences(raw); }
load();
function urlFor(app) {
if (!app.external) return `#${app.id}`;
if (app.id === 'waze') return wazeUrl(destination);
return app.url;
}
function configureLink(link, app) {
link.onclick=null;
const href = urlFor(app);
link.dataset.application = app.id;
if (href) { link.href = href; link.removeAttribute('aria-disabled'); }
else { link.removeAttribute('href'); link.setAttribute('aria-disabled', 'true'); }
if (app.external && preferences.modes[app.id] === 'embed') {
link.href='#embedded';link.removeAttribute('target');
link.onclick=e=>{e.preventDefault();location.hash='embedded';navigate();document.dispatchEvent(new CustomEvent('caria-embed',{detail:app.id}));};
}
else if (app.external) { link.target = preferences.modes[app.id] === 'same' ? '_self' : '_blank'; link.rel = 'noopener noreferrer'; }
else link.dataset.view = app.id;
}
function icon(app) { const span = document.createElement('span'); span.className = 'icon'; span.textContent = app.icon; span.setAttribute('aria-hidden', 'true'); return span; }
function save(next) {
const normalized = normalizePreferences(next);
if (!storage.set('applications', JSON.stringify(normalized))) { toast('Le navigateur bloque l’enregistrement des applications.'); renderSettings(); return; }
preferences = normalized; render(); renderSettings();
}
function render() {
const nav = $('#favorite-apps'); nav.replaceChildren();
preferences.favorites.filter(Boolean).forEach(id => {
const app = applications.find(a => a.id === id), link = document.createElement('a'); configureLink(link, app);
const name = document.createElement('span'); name.textContent = app.id === 'music' ? 'Musique' : app.name;
link.append(icon(app), name); nav.append(link);
});
const catalogLink = document.createElement('a'); catalogLink.href = '#apps'; catalogLink.dataset.view = 'apps';
const label = document.createElement('span'); label.textContent = 'Applications';
catalogLink.append(icon({ icon: '▦' }), label); nav.append(catalogLink);
const catalog = $('#applications-grid'); catalog.replaceChildren();
applications.filter(a => !preferences.hidden.includes(a.id)).forEach(app => {
const card = document.createElement('article'); card.className = 'card application-card';
const link = document.createElement('a'); configureLink(link, app); link.className = 'application-open';
const name = document.createElement('h2'); name.textContent = app.name;
const description = document.createElement('p'); description.textContent = app.description;
const hint = document.createElement('span'); hint.className = 'application-kind';
hint.textContent = !urlFor(app) ? 'Adresse à configurer dans les réglages' : app.external ? preferences.modes[app.id] === 'embed' ? 'Lecteur intégré · dans CARIA' : preferences.modes[app.id] === 'same' ? 'Site externe · même onglet ↗' : 'Site externe · nouvel onglet ↗' : 'Dans CARIA';
link.append(icon(app), name, description, hint);
const favorite = document.createElement('button'); favorite.className = 'favorite-toggle';
const selected = preferences.favorites.includes(app.id); favorite.textContent = selected ? '★' : '☆';
favorite.setAttribute('aria-label', `${selected ? 'Retirer' : 'Ajouter'} ${app.name} ${selected ? 'des' : 'aux'} favoris`); favorite.setAttribute('aria-pressed', String(selected));
favorite.onclick = () => {
const next = structuredClone(preferences), slot = next.favorites.indexOf(selected ? app.id : null);
if (slot < 0) return toast('Quatre favoris maximum. Utilisez Personnaliser pour remplacer un favori.');
next.favorites[slot] = selected ? null : app.id; save(next);
};
card.append(link, favorite); catalog.append(card);
});
$('#applications-empty').hidden = catalog.childElementCount !== 0;
$('#favorite-count').textContent = `${preferences.favorites.filter(Boolean).length} / 4 favoris`;
document.querySelectorAll('[data-waze-destination]').forEach(link => {
configureLink(link, applications.find(a => a.id === 'waze'));
link.hidden = !destination || preferences.hidden.includes('waze');
});
configureLink($('#stream-link'), applications.find(a=>a.id==='stream'));
$('#stream-opening').textContent = preferences.modes.stream === 'embed' ? 'S’ouvre dans le lecteur intégré CARIA, si votre serveur Jellyfin autorise cet affichage.' : preferences.modes.stream === 'same' ? 'S’ouvre dans cet onglet. Utilisez Retour pour retrouver CARIA ; la lecture locale peut être interrompue.' : 'S’ouvre dans un nouvel onglet. Votre cockpit reste accessible.';
navigate();
}
function renderSettings() {
const slots = $('#favorite-slots'); slots.replaceChildren();
preferences.favorites.forEach((id, index) => {
const label = document.createElement('label'); label.textContent = `Favori ${index + 1}`;
const select = document.createElement('select'); select.dataset.favoriteSlot = index;
select.add(new Option('Aucun', ''));
applications.filter(a => !preferences.hidden.includes(a.id)).forEach(app => select.add(new Option(app.name, app.id)));
select.value = id || '';
select.onchange = () => {
const next = structuredClone(preferences), chosen = select.value || null, other = chosen ? next.favorites.indexOf(chosen) : -1;
if (other >= 0 && other !== index) next.favorites[other] = id;
next.favorites[index] = chosen; save(next);
};
label.append(select); slots.append(label);
});
const list = $('#application-settings-list'); list.replaceChildren();
applications.forEach(app => {
const row = document.createElement('div'); row.className = 'application-setting'; row.dataset.appSetting = app.id;
const visible = document.createElement('label'), checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = !preferences.hidden.includes(app.id);
checkbox.setAttribute('aria-label', `Afficher ${app.name}`); visible.append(checkbox, document.createTextNode(app.name));
checkbox.onchange = () => {
const next = structuredClone(preferences); next.hidden = checkbox.checked ? next.hidden.filter(id => id !== app.id) : [...next.hidden, app.id]; save(next);
};
row.append(visible);
if (app.external) {
const mode = document.createElement('select'); mode.setAttribute('aria-label', `Ouverture de ${app.name}`);
mode.add(new Option('Nouvel onglet', 'new')); mode.add(new Option('Même onglet', 'same')); mode.value = preferences.modes[app.id];
if(app.id !== 'waze') mode.add(new Option('Lecteur intégré','embed'));
mode.value=preferences.modes[app.id];
mode.onchange = () => { const next = structuredClone(preferences); next.modes[app.id] = mode.value; save(next); }; row.append(mode);
} else { const hint = document.createElement('span'); hint.textContent = 'Dans CARIA'; row.append(hint); }
list.append(row);
});
}
function openSettings() { renderSettings(); $('#application-settings').showModal(); }
$('#applications-customize').onclick = openSettings;
$('#applications-settings-open').onclick = () => { $('#settings').close(); openSettings(); };
$('#application-settings-close').onclick = () => $('#application-settings').close();
$('#applications-reset').onclick = () => save(normalizePreferences(null));
document.addEventListener('caria-destination', event => { destination = event.detail; render(); });
document.addEventListener('caria-stream', render);
window.addEventListener('storage', event => { if (event.key === 'caria.applications' || event.key === null) { load(); render(); renderSettings(); } });
render();
}