Files

146 lines
10 KiB
JavaScript
Raw Permalink 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.
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(); });
window.addEventListener('storage', event => { if (event.key === 'caria.applications' || event.key === null) { load(); render(); renderSettings(); } });
render();
}