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(); }