Nettoyage et construction micro services
This commit is contained in:
@@ -5,4 +5,5 @@ COPY ui/journey.js ui/geo.js ui/theme.js ui/journey.css /usr/share/nginx/html/
|
||||
COPY ui/vendor /usr/share/nginx/html/vendor
|
||||
COPY ui/applications.js ui/applications.css /usr/share/nginx/html/
|
||||
COPY ui/cockpit.js ui/cockpit.css ui/embeds.js /usr/share/nginx/html/
|
||||
COPY ui/instruments.js ui/music.js /usr/share/nginx/html/
|
||||
EXPOSE 80
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -33,85 +35,10 @@ function clock() {
|
||||
}
|
||||
clock(); setInterval(clock, 1000);
|
||||
|
||||
function makeGauge(id, max, step, unit, label) {
|
||||
const point = (angle, radius) => {
|
||||
const r = angle * Math.PI / 180;
|
||||
return [150 + radius * Math.sin(r), 150 - radius * Math.cos(r)];
|
||||
};
|
||||
let ticks = '';
|
||||
for (let v = 0; v <= max; v += step / 4) {
|
||||
const angle = -130 + v / max * 260;
|
||||
const major = v % step === 0;
|
||||
const a = point(angle, major ? 112 : 119), b = point(angle, 126), n = point(angle, 96);
|
||||
ticks += `<line class="tick ${major ? 'major' : v % (step / 2) === 0 ? 'intermediate' : 'quarter'}" x1="${a[0]}" y1="${a[1]}" x2="${b[0]}" y2="${b[1]}"/>`;
|
||||
if (major) ticks += `<text class="dial-number" x="${n[0]}" y="${n[1] + 4}">${max === 7000 ? v / 1000 : v}</text>`;
|
||||
}
|
||||
// Decorative demonstration red zone; not a calibrated vehicle redline.
|
||||
const redStart = point(-130 + 5500 / 7000 * 260, 128), redEnd = point(130, 128);
|
||||
const redZone = max === 7000 ? `<defs><linearGradient id="rpm-red-zone" gradientUnits="userSpaceOnUse" x1="${redStart[0]}" y1="${redStart[1]}" x2="${redEnd[0]}" y2="${redEnd[1]}"><stop stop-color="#f7a65d" stop-opacity="0.1"/><stop offset="0.5" stop-color="#f67855"/><stop offset="1" stop-color="#fa4d5b"/></linearGradient></defs><path class="rpm-red-zone" d="M${redStart.join(' ')} A128 128 0 0 1 ${redEnd.join(' ')}"/>` : '';
|
||||
const levelKey = max === 7000 ? 'coolantCelsius' : 'fuelPercent';
|
||||
const iconPath = document.querySelector(`[data-level="${levelKey}"] .aux-title svg`).innerHTML;
|
||||
// Compact auxiliary arc, separated from the main instrument.
|
||||
const levelPoint = (angle, radius = 64) => [150+radius*Math.sin(angle*Math.PI/180),244-radius*Math.cos(angle*Math.PI/180)];
|
||||
// 86.8 degrees: 30% shorter than the previous 124-degree auxiliary arc.
|
||||
const arcPath = (from,to) => `M${levelPoint(from).join(' ')} A64 64 0 0 0 ${levelPoint(to).join(' ')}`;
|
||||
const arc = arcPath(223.4,136.6);
|
||||
const redArc = max===7000 ? arcPath(153.96,136.6) : arcPath(223.4,212.984);
|
||||
const tickRatios = max === 7000 ? [0,.28,.56,.84,1] : [0,.12,.4133333333,.7066666667,1];
|
||||
const levelTicks = tickRatios.map((fraction,index) => {
|
||||
const angle = 223.4 - 86.8*fraction;
|
||||
const inner=levelPoint(angle,54), outer=levelPoint(angle,61);
|
||||
return `<path class="level-tick" d="M${inner.join(' ')} L${outer.join(' ')}"/>`;
|
||||
}).join('');
|
||||
const level = `<g class="digital-levels" data-digital-level="${levelKey}"><path class="level-arc-track" d="${arc}"/><path class="level-arc-progress" pathLength="100" d="${arc}"/><path class="level-arc-red" d="${redArc}"/>${levelTicks}<text class="level-end" x="95" y="304">${max===7000?'C':'E'}</text><text class="level-end" x="205" y="304">${max===7000?'H':'F'}</text>${max===7000?'':'<text class="level-end" x="150" y="325">½</text>'}<g class="level-icon" transform="translate(${max===7000?139.8:141.6} ${max===7000?267.8:269.6}) scale(${max===7000?.85:.7})" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${iconPath}</g><text class="level-value" x="150" y="348"><tspan data-value="${levelKey}">—</tspan> ${max===7000?'°C':'%'}</text></g>`;
|
||||
const low = max === 7000 ? 'C' : 'E', high = max === 7000 ? 'H' : 'F';
|
||||
const mini = `<path class="analog-separator" d="M59.590 240.609 L77 226 L223 226 L240.410 240.609"/><g class="analog-levels" data-analog-level="${levelKey}"><g transform="translate(150 264) scale(0.90) translate(-150 -264)"><g transform="translate(0 572) scale(1 -1)"><path class="mini-track" d="M90 308 A60 60 0 0 1 210 308"/><path class="mini-red" d="${max===7000?'M192.4 265.6 A60 60 0 0 1 210 308':'M90 308 A60 60 0 0 1 107.6 265.6'}"/><path class="mini-ticks" d="M90 308h8M107.6 265.6l6 6M150 248v8M192.4 265.6l-6 6M210 308h-8"/><text class="mini-end" transform="translate(0 604) scale(1 -1)" x="104" y="302">${low}</text><text class="mini-end" transform="translate(0 604) scale(1 -1)" x="196" y="302">${high}</text>${max===7000?'':`<text class="mini-end" transform="translate(0 520) scale(1 -1)" x="150" y="260">½</text>`}<line class="mini-needle" x1="150" y1="308" x2="150" y2="258"/><circle class="mini-hub" cx="150" cy="308" r="3"/></g></g><g class="level-icon" transform="translate(${max===7000?139.8:141.6} ${max===7000?277.8:279.6}) scale(${max===7000?.85:.7})" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${iconPath}</g><text class="level-value" x="150" y="368"><tspan data-value="${levelKey}">—</tspan> ${max===7000?'°C':'%'}</text></g>`;
|
||||
$(id).innerHTML = `<svg viewBox="0 0 300 265" role="img" aria-label="${label} : en attente"><path class="dial-rim" d="M68.4 255 A133 133 0 1 1 231.6 255 Z" fill="none" stroke="var(--border)"/><path class="dial-light" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/><path class="digital-track" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/><path class="digital-progress" pathLength="100" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/>${ticks}${redZone}<text x="150" y="110" class="dial-label analog-unit">${unit}</text><text x="150" y="110" class="dial-label digital-unit">${max === 7000 ? 'TR/MIN' : unit}</text><line class="needle" x1="150" y1="150" x2="150" y2="43" style="transform-origin:150px 150px;transform:rotate(-130deg)"/><circle class="needle-hub" cx="150" cy="150" r="7" fill="var(--accent)"/><circle class="needle-hub" cx="150" cy="150" r="3" fill="var(--surface)"/><text x="150" y="212" class="dial-value">—</text><text x="150" y="239" class="dial-label">${label}</text>${level}${mini}</svg>`;
|
||||
return value => {
|
||||
const valid = typeof value === 'number' && Number.isFinite(value);
|
||||
$(`${id} .dial-value`).textContent = valid ? Math.round(value).toLocaleString('fr-FR') : '—';
|
||||
$(`${id} .digital-progress`).style.strokeDashoffset = String(valid ? 100 - Math.max(0,Math.min(1,value/max))*100 : 100);
|
||||
$(`${id} .needle`).style.opacity = valid ? '1' : '0';
|
||||
if (valid) $(`${id} .needle`).style.transform = `rotate(${-130 + Math.max(0, Math.min(max, value)) / max * 260}deg)`;
|
||||
$(`${id} svg`).setAttribute('aria-label', `${label} : ${valid ? Math.round(value) : 'indisponible'} ${max === 7000 ? 'tr/min' : 'km/h'}`);
|
||||
};
|
||||
}
|
||||
const speedGauge = makeGauge('#speed-gauge', 220, 20, 'KM/H', 'VITESSE');
|
||||
const rpmGauge = makeGauge('#rpm-gauge', 7000, 1000, '× 1 000 TR/MIN', 'RÉGIME MOTEUR');
|
||||
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 updateLevels(data) {
|
||||
all('[data-level]').forEach(el => {
|
||||
const key = el.dataset.level, value = data?.[key], valid = Number.isFinite(value);
|
||||
const min = Number(el.getAttribute('aria-valuemin')), max = Number(el.getAttribute('aria-valuemax'));
|
||||
const needle = el.querySelector('.aux-needle');
|
||||
if (needle) { needle.style.transform = `rotate(${valid ? -90+Math.max(0,Math.min(1,(value-min)/(max-min)))*180 : -90}deg)`; needle.style.opacity = valid ? '1' : '0'; }
|
||||
// Demo visualization ranges; actual CAN calibration belongs to the vehicle profile.
|
||||
const ratio = valid ? Math.max(0,Math.min(1,(value-min)/(max-min))) : 0;
|
||||
const starts = key === 'fuelPercent' ? [0,.1,.4,.7] : [0,.25,.5,.75];
|
||||
el.querySelectorAll('.sensor-segments i').forEach((segment,i) => {
|
||||
segment.classList.toggle('filled', valid && ratio > starts[i]);
|
||||
});
|
||||
const analog = document.querySelector(`[data-analog-level="${key}"]`);
|
||||
analog.querySelector('.mini-needle').style.transform = `rotate(${-90 + ratio*180}deg)`;
|
||||
analog.querySelector('.mini-needle').style.opacity = valid ? '1' : '0';
|
||||
analog.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
|
||||
const digital = document.querySelector(`[data-digital-level="${key}"]`);
|
||||
const stops = key === 'fuelPercent' ? [0,.12,.4133333333,.7066666667,1] : [0,.28,.56,.84,1];
|
||||
const stepped = stops.filter(stop => stop <= ratio).at(-1) ?? 0;
|
||||
const progress = digital.querySelector('.level-arc-progress');
|
||||
progress.style.strokeDashoffset = String(100*(1-stepped));
|
||||
progress.style.opacity = valid && stepped > 0 ? '1' : '0';
|
||||
digital.classList.toggle('last-level', valid && key === 'coolantCelsius' && ratio >= .84);
|
||||
digital.classList.toggle('reserve-level', valid && key === 'fuelPercent' && stepped === .12);
|
||||
digital.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
|
||||
el.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
|
||||
el.classList.toggle('reserve', valid && key === 'fuelPercent' && value <= 10);
|
||||
if (valid) el.setAttribute('aria-valuenow', String(value)); else el.removeAttribute('aria-valuenow');
|
||||
el.setAttribute('aria-valuetext', valid ? value + (key === 'fuelPercent' ? ' %' : ' °C') : 'Indisponible');
|
||||
});
|
||||
}
|
||||
function offline() {
|
||||
$('#connection').textContent = 'Données indisponibles';
|
||||
$('#connection').classList.add('error');
|
||||
@@ -183,89 +110,7 @@ $('#fullscreen').onclick = async () => {
|
||||
catch { toast('Le plein écran n’est pas disponible dans ce navigateur.'); }
|
||||
};
|
||||
|
||||
const audio = $('#audio');
|
||||
let tracks = [], selected = -1;
|
||||
let shuffle = storage.get('shuffle') === 'true', shuffleBag = [], playbackHistory = [];
|
||||
$('#immersive-shuffle').setAttribute('aria-pressed', String(shuffle));
|
||||
$('#immersive-shuffle').onclick = () => {
|
||||
shuffle = !shuffle; shuffleBag = []; playbackHistory = [];
|
||||
storage.set('shuffle', String(shuffle));
|
||||
$('#immersive-shuffle').setAttribute('aria-pressed', String(shuffle));
|
||||
toast(shuffle ? 'Lecture aléatoire activée' : 'Lecture aléatoire désactivée');
|
||||
};
|
||||
function nextTrack() {
|
||||
if (!shuffle || tracks.length < 2) return selectTrack(selected + 1, true);
|
||||
if (!shuffleBag.length) {
|
||||
shuffleBag = tracks.map((_, index) => index).filter(index => index !== selected);
|
||||
for (let i = shuffleBag.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffleBag[i], shuffleBag[j]] = [shuffleBag[j], shuffleBag[i]];
|
||||
}
|
||||
}
|
||||
selectTrack(shuffleBag.pop(), true);
|
||||
}
|
||||
function renderPlaylist() {
|
||||
$('#playlist').replaceChildren();
|
||||
tracks.forEach((track, index) => {
|
||||
const li = document.createElement('li'), button = document.createElement('button');
|
||||
const number = document.createElement('span'), name = document.createElement('b');
|
||||
number.textContent = String(index + 1).padStart(2, '0'); name.textContent = track.name;
|
||||
button.append(number, name); button.classList.toggle('selected', index === selected);
|
||||
button.setAttribute('aria-pressed', String(index === selected)); button.onclick = () => selectTrack(index, true);
|
||||
li.append(button); $('#playlist').append(li);
|
||||
});
|
||||
$('#track-count').textContent = `${tracks.length} TITRE${tracks.length > 1 ? 'S' : ''}`;
|
||||
$('#empty-library').hidden = tracks.length > 0;
|
||||
['#play', '#previous', '#next'].forEach(id => $(id).disabled = tracks.length === 0);
|
||||
}
|
||||
async function playAudio() {
|
||||
try { await audio.play(); $('#audio-message').textContent = 'Lecture locale · aucun fichier envoyé au serveur.'; }
|
||||
catch { $('#audio-message').textContent = 'Lecture impossible : réessayez ou choisissez un format audio pris en charge.'; }
|
||||
}
|
||||
function selectTrack(index, play = false, remember = true) {
|
||||
if (!tracks.length) return;
|
||||
previousTap = -Infinity;
|
||||
if (remember && selected >= 0 && index !== selected) {
|
||||
playbackHistory.push(selected);
|
||||
if (playbackHistory.length > 256) playbackHistory.shift();
|
||||
}
|
||||
selected = (index + tracks.length) % tracks.length;
|
||||
shuffleBag = shuffleBag.filter(index => index !== selected);
|
||||
audio.src = tracks[selected].url;
|
||||
$('#track-title').textContent = tracks[selected].name;
|
||||
$('#home-track').textContent = tracks[selected].name;
|
||||
$('#track-subtitle').textContent = `Morceau ${selected + 1} sur ${tracks.length} · Bibliothèque locale`;
|
||||
$('#home-track-sub').textContent = 'Bibliothèque locale';
|
||||
renderPlaylist(); if (play) playAudio();
|
||||
}
|
||||
$('#music-files').addEventListener('change', event => {
|
||||
let added = 0;
|
||||
for (const file of event.target.files) {
|
||||
if (!file.type.startsWith('audio/') && !/\.(mp3|flac|ogg|wav|m4a|aac|opus)$/i.test(file.name)) continue;
|
||||
tracks.push({ name: file.name.replace(/\.[^.]+$/, ''), url: URL.createObjectURL(file) }); added++;
|
||||
}
|
||||
shuffleBag = [];
|
||||
renderPlaylist();
|
||||
if (selected === -1 && tracks.length) selectTrack(0);
|
||||
toast(added ? `${added} morceau${added > 1 ? 'x' : ''} ajouté${added > 1 ? 's' : ''}` : 'Aucun fichier audio reconnu');
|
||||
event.target.value = '';
|
||||
});
|
||||
$('#play').onclick = () => { if (selected === -1) return; if (audio.paused) playAudio(); else audio.pause(); };
|
||||
let previousTap = -Infinity;
|
||||
$('#previous').onclick = () => {
|
||||
const now = performance.now(), twice = now - previousTap < 600;
|
||||
previousTap = twice ? -Infinity : now;
|
||||
if (!twice) { audio.currentTime = 0; return; }
|
||||
if (shuffle) { shuffleBag = []; selectTrack(playbackHistory.pop() ?? selected, true, false); }
|
||||
else selectTrack(selected - 1, true);
|
||||
};
|
||||
$('#next').onclick = nextTrack;
|
||||
audio.addEventListener('play', () => $('#play').textContent = 'Pause');
|
||||
audio.addEventListener('pause', () => $('#play').textContent = 'Lecture');
|
||||
audio.addEventListener('ended', () => { if (audio.loop) return; if (shuffle || selected + 1 < tracks.length) nextTrack(); else $('#play').textContent = 'Lecture'; });
|
||||
audio.addEventListener('error', () => $('#audio-message').textContent = 'Ce fichier ne peut pas être lu. Essayez un autre format audio.');
|
||||
window.addEventListener('pagehide', event => { if (!event.persisted) tracks.forEach(track => URL.revokeObjectURL(track.url)); });
|
||||
renderPlaylist();
|
||||
initMusic({ storage, toast });
|
||||
initJourney({ storage, toast });
|
||||
initApplications({ storage, toast, navigate });
|
||||
initEmbeds({ storage, toast });
|
||||
|
||||
+231
-5
@@ -1,5 +1,231 @@
|
||||
#applications-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.catalog-heading{display:flex;justify-content:space-between;gap:15px;align-items:center;margin-bottom:18px}.catalog-heading p{font-size:13px;color:var(--muted);margin:0}.application-card{padding:0;position:relative;box-shadow:none}.application-open{display:block;padding:25px;min-height:205px}.application-open>.icon{display:grid;place-items:center;width:52px;height:52px;border-radius:15px;background:color-mix(in srgb,var(--accent) 10%,var(--surface));color:var(--accent);font-size:29px;margin-bottom:18px}.application-open h2{font-size:19px;margin-bottom:8px}.application-open p{font-size:12px;color:var(--muted);margin:0 0 16px;min-height:36px}.application-kind{font-size:10px;color:var(--accent)}.application-card:hover{border-color:var(--accent)}.favorite-toggle{position:absolute;right:12px;top:12px;font-size:23px;padding:4px 10px;border-color:transparent;background:transparent;color:var(--accent)}.favorite-toggle[aria-pressed=true]{background:color-mix(in srgb,var(--accent) 10%,var(--surface))}.application-open[aria-disabled=true]{opacity:.55;cursor:default}#application-settings{width:min(640px,calc(100% - 24px))}#application-settings h3{font-size:16px;margin:24px 0 8px}#favorite-slots{display:grid;grid-template-columns:1fr 1fr;gap:10px 16px}#favorite-slots label{margin-top:6px}#favorite-slots select{width:100%}.application-setting{display:grid;grid-template-columns:minmax(0,1fr) minmax(130px,180px);gap:12px;align-items:center;min-height:60px;border-bottom:1px solid var(--border);padding:8px 0}.application-setting label{display:flex;flex-direction:row;align-items:center;margin:0;min-height:44px;gap:12px}.application-setting input{width:20px;height:20px;accent-color:var(--accent)}.application-setting>span{color:var(--muted);font-size:12px}.application-setting select{width:100%;font-size:12px}.rail nav a span:last-child{overflow-wrap:anywhere;text-align:center}.map-actions a[data-waze-destination]{font-size:11px;padding:12px;border:1px solid var(--border);border-radius:10px;color:var(--accent)}
|
||||
@media(min-width:1450px){#applications-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}
|
||||
@media(max-width:900px){#applications-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.application-open{padding:20px}}
|
||||
@media(max-width:520px){#applications-grid{grid-template-columns:1fr}.application-open{min-height:165px}.application-open>.icon{margin-bottom:12px}.application-open p{min-height:0}#favorite-slots{grid-template-columns:1fr}.application-setting{grid-template-columns:1fr}.application-setting select{margin-bottom:6px}.catalog-heading{align-items:flex-start}.catalog-heading p{font-size:11px}}
|
||||
@media(max-height:650px) and (min-width:760px){.application-open{padding:18px;min-height:180px}.application-open>.icon{width:44px;height:44px;margin-bottom:12px}.application-open h2{font-size:17px}.application-open p{font-size:11px;margin-bottom:10px}}
|
||||
#applications-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.catalog-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.catalog-heading p {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.application-card {
|
||||
padding: 0px;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.application-open {
|
||||
display: block;
|
||||
padding: 25px;
|
||||
min-height: 205px;
|
||||
}
|
||||
|
||||
.application-open > .icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 15px;
|
||||
background: color-mix(in srgb,var(--accent) 10%,var(--surface));
|
||||
color: var(--accent);
|
||||
font-size: 29px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.application-open h2 {
|
||||
font-size: 19px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.application-open p {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin: 0px 0px 16px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.application-kind {
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.application-card:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.favorite-toggle {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
font-size: 23px;
|
||||
padding: 4px 10px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.favorite-toggle[aria-pressed="true"] {
|
||||
background: color-mix(in srgb,var(--accent) 10%,var(--surface));
|
||||
}
|
||||
|
||||
.application-open[aria-disabled="true"] {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#application-settings {
|
||||
width: min(640px, 100% - 24px);
|
||||
}
|
||||
|
||||
#application-settings h3 {
|
||||
font-size: 16px;
|
||||
margin: 24px 0px 8px;
|
||||
}
|
||||
|
||||
#favorite-slots {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 16px;
|
||||
}
|
||||
|
||||
#favorite-slots label {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
#favorite-slots select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.application-setting {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0px, 1fr) minmax(130px, 180px);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-height: 60px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 8px 0px;
|
||||
}
|
||||
|
||||
.application-setting label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0px;
|
||||
min-height: 44px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.application-setting input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.application-setting > span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.application-setting select {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rail nav a span:last-child {
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.map-actions a[data-waze-destination] {
|
||||
font-size: 11px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (min-width: 1450px) {
|
||||
#applications-grid {
|
||||
grid-template-columns: repeat(4, minmax(0px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
#applications-grid {
|
||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||
}
|
||||
|
||||
.application-open {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
#applications-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.application-open {
|
||||
min-height: 165px;
|
||||
}
|
||||
|
||||
.application-open > .icon {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.application-open p {
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
#favorite-slots {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.application-setting {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.application-setting select {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.catalog-heading {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.catalog-heading p {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 650px) and (min-width: 760px) {
|
||||
.application-open {
|
||||
padding: 18px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.application-open > .icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.application-open h2 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.application-open p {
|
||||
font-size: 11px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,6 @@ export function initApplications({ storage, toast, navigate }) {
|
||||
$('#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();
|
||||
}
|
||||
|
||||
+1982
-122
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -9,26 +9,27 @@ export function initCockpit({ storage, toast }) {
|
||||
document.dispatchEvent(new Event('caria-layout'));
|
||||
}
|
||||
$('#rail-toggle').onclick = () => { collapsed = !collapsed; storage.set('railCollapsed', String(collapsed)); rail(); };
|
||||
document.querySelectorAll('.screen-links a').forEach(link => link.onclick = () => $('#settings').close());
|
||||
rail();
|
||||
if (dedicated && !location.hash) location.hash = 'drive';
|
||||
const time = seconds => Number.isFinite(seconds) ? `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2,'0')}` : '0:00';
|
||||
let previousPaused, previousSilent;
|
||||
function updatePlayer() {
|
||||
const duration = Number.isFinite(audio.duration) ? audio.duration : 0, progress = duration ? audio.currentTime / duration : 0;
|
||||
$('#track-arc').style.strokeDashoffset = String(100 * (1 - progress));
|
||||
$('#immersive-current').textContent = time(audio.currentTime); $('#immersive-duration').textContent = time(duration);
|
||||
$('#immersive-title').textContent = $('#track-title').textContent;
|
||||
$('#immersive-subtitle').textContent = $('#track-subtitle').textContent;
|
||||
$('#immersive-play').innerHTML = audio.paused ? '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m9 5 11 7-11 7Z"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5v14M16 5v14"/></svg>';
|
||||
if (previousPaused !== audio.paused) $('#immersive-play').innerHTML = audio.paused ? '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m9 5 11 7-11 7Z"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5v14M16 5v14"/></svg>';
|
||||
$('#immersive-play').setAttribute('aria-label', audio.paused ? 'Lire la musique' : 'Mettre en pause');
|
||||
$('#now-playing').classList.toggle('playing', !audio.paused);
|
||||
$('#immersive-volume').value = audio.muted ? '0' : String(audio.volume);
|
||||
const silent = audio.muted || audio.volume === 0;
|
||||
$('#immersive-mute').innerHTML = silent ? '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4 6 8H3v8h3l5 4Zm5 5 6 6m0-6-6 6"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4 6 8H3v8h3l5 4Zm4 4a6 6 0 0 1 0 8m3-11a10 10 0 0 1 0 14"/></svg>';
|
||||
if (previousSilent !== silent) $('#immersive-mute').innerHTML = silent ? '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4 6 8H3v8h3l5 4Zm5 5 6 6m0-6-6 6"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4 6 8H3v8h3l5 4Zm4 4a6 6 0 0 1 0 8m3-11a10 10 0 0 1 0 14"/></svg>';
|
||||
$('#immersive-mute').setAttribute('aria-label', silent ? 'Activer le son' : 'Couper le son');
|
||||
$('#immersive-mute').title = silent ? 'Activer le son' : 'Couper le son';
|
||||
$('#immersive-mute').setAttribute('aria-pressed', String(silent));
|
||||
$('#immersive-volume-value').textContent = Math.round((silent ? 0 : audio.volume) * 100) + ' %';
|
||||
previousPaused = audio.paused; previousSilent = silent;
|
||||
document.querySelectorAll('[data-audio-control]').forEach(button => button.disabled = !audio.getAttribute('src'));
|
||||
}
|
||||
$('#immersive-play').onclick = () => $('#play').click();
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
</section>
|
||||
<section id="drive" class="view" hidden>
|
||||
<div class="page-heading"><div><p class="eyebrow">LES ESSENTIELS EN UN REGARD</p><h1>Tableau de bord</h1></div></div>
|
||||
<div class="cluster card"><div class="gauges"><div class="gauge" id="speed-gauge"></div><div class="gear"><p class="eyebrow">RAPPORT</p><strong data-value="gear">—</strong></div><div class="gauge" id="rpm-gauge"></div></div><div class="aux-gauges"><div class="aux-gauge" role="meter" aria-label="Carburant" aria-valuemin="0" aria-valuemax="100" data-level="fuelPercent"><svg class="aux-analog" viewBox="0 0 160 160" aria-hidden="true"><g transform="translate(0 4) rotate(-90 80 76)"><defs><linearGradient id="fuel-reserve-red" gradientUnits="userSpaceOnUse" x1="20" y1="76" x2="37.6" y2="33.6"><stop stop-color="#fa4d5b"/><stop offset="0.5" stop-color="#f67855"/><stop offset="1" stop-color="#f7a65d" stop-opacity="0.1"/></linearGradient></defs><path d="M20 76 A60 60 0 0 1 140 76"/><path class="aux-ticks" d="M20 76h9M38 34l7 7M80 16v10M122 34l-7 7M140 76h-9"/><text class="aux-mark" transform="rotate(90 33 72)" x="33" y="72" dx="-7">E</text><text class="aux-mark" transform="rotate(90 80 40)" x="80" y="40">½</text><text class="aux-mark" transform="rotate(90 127 72)" x="127" y="72" dx="-7">F</text><path class="fuel-reserve-zone" d="M20 76 A60 60 0 0 1 37.6 33.6"/><line class="aux-needle" x1="80" y1="76" x2="80" y2="24"/><circle cx="80" cy="76" r="4"/></g></svg><div class="aux-title" title="Carburant"><svg viewBox="0 0 24 24" role="img" aria-label="Carburant" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 21V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v16M3 21h12M4 10h10m3-6 3 3v11a2 2 0 0 1-4 0v-5a2 2 0 0 0-2-2m4-6v4h2"/></svg></div><strong><b data-value="fuelPercent">—</b> %</strong><div class="sensor-segments" aria-hidden="true"><i></i><i></i><i></i><i></i></div><div class="aux-limits"><span>E</span><span>F</span></div></div><div class="aux-gauge" role="meter" aria-label="Température moteur" aria-valuemin="40" aria-valuemax="130" data-level="coolantCelsius"><svg class="aux-analog" viewBox="0 0 160 160" aria-hidden="true"><g transform="translate(0 4) rotate(90 80 76)"><defs><linearGradient id="temperature-red" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#f5ac63" stop-opacity="0.15"/><stop offset="0.5" stop-color="#f57b56"/><stop offset="1" stop-color="#f04f55"/></linearGradient></defs><path d="M20 76 A60 60 0 0 1 140 76"/><path class="aux-ticks" d="M20 76h9M38 34l7 7M80 16v10M122 34l-7 7M140 76h-9"/><path class="temperature-hot" d="M122.4 33.6 A60 60 0 0 1 140 76"/><text class="aux-mark" transform="rotate(-90 33 72)" x="33" y="72" dx="7">C</text><text class="aux-mark" transform="rotate(-90 127 72)" x="127" y="72" dx="7">H</text><line class="aux-needle" x1="80" y1="76" x2="80" y2="24"/><circle cx="80" cy="76" r="4"/></g></svg><div class="aux-title" title="Température moteur"><svg viewBox="0 0 24 24" role="img" aria-label="Température moteur" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 12V4a2 2 0 0 1 4 0v8a3.5 3.5 0 1 1-4 0Zm2-6v9m4-9h2m-2 3h2M2 19q2-2 4 0t4 0t4 0t4 0t4 0M2 23q2-2 4 0t4 0t4 0t4 0t4 0"/></svg></div><strong><b data-value="coolantCelsius">—</b> °C</strong><div class="sensor-segments" aria-hidden="true"><i></i><i></i><i></i><i></i></div><div class="aux-limits"><span>C</span><span>H</span></div></div></div><div class="cluster-bottom"><span>Trajet <b><span data-value="tripKm">—</span> km</b></span><span>Consommation <b><span data-value="consumptionL100">—</span> L/100</b></span></div></div>
|
||||
<div class="cluster card"><div class="gauges"><div class="gauge" id="speed-gauge"></div><div class="gear"><p class="eyebrow">RAPPORT</p><strong data-value="gear">—</strong></div><div class="gauge" id="rpm-gauge"></div></div><div class="cluster-bottom"><span>Trajet <b><span data-value="tripKm">—</span> km</b></span><span>Consommation <b><span data-value="consumptionL100">—</span> L/100</b></span></div></div>
|
||||
<div class="drive-links"><div class="drive-actions"><a class="pill" href="#vehicle-details">Détails du véhicule ↗</a><label class="profile-label">Simulation <select id="profile"><option value="route">Route</option><option value="ville">Ville</option><option value="arret">À l’arrêt</option></select></label></div></div>
|
||||
</section>
|
||||
<section id="music" class="view" hidden>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Instrument rendering and telemetry projection; no network or persistence.
|
||||
export function initInstruments() {
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const icons = {
|
||||
"fuelPercent": "<path d=\"M4 21V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v16M3 21h12M4 10h10m3-6 3 3v11a2 2 0 0 1-4 0v-5a2 2 0 0 0-2-2m4-6v4h2\"/>",
|
||||
"coolantCelsius": "<path d=\"M10 12V4a2 2 0 0 1 4 0v8a3.5 3.5 0 1 1-4 0Zm2-6v9m4-9h2m-2 3h2M2 19q2-2 4 0t4 0t4 0t4 0t4 0M2 23q2-2 4 0t4 0t4 0t4 0t4 0\"/>"
|
||||
};
|
||||
function makeGauge(id, max, step, unit, label) {
|
||||
const point = (angle, radius) => {
|
||||
const r = angle * Math.PI / 180;
|
||||
return [150 + radius * Math.sin(r), 150 - radius * Math.cos(r)];
|
||||
};
|
||||
let ticks = '';
|
||||
for (let v = 0; v <= max; v += step / 4) {
|
||||
const angle = -130 + v / max * 260;
|
||||
const major = v % step === 0;
|
||||
const a = point(angle, major ? 112 : 119), b = point(angle, 126), n = point(angle, 96);
|
||||
ticks += `<line class="tick ${major ? 'major' : v % (step / 2) === 0 ? 'intermediate' : 'quarter'}" x1="${a[0]}" y1="${a[1]}" x2="${b[0]}" y2="${b[1]}"/>`;
|
||||
if (major) ticks += `<text class="dial-number" x="${n[0]}" y="${n[1] + 4}">${max === 7000 ? v / 1000 : v}</text>`;
|
||||
}
|
||||
// Decorative demonstration red zone; not a calibrated vehicle redline.
|
||||
const redStart = point(-130 + 5500 / 7000 * 260, 128), redEnd = point(130, 128);
|
||||
const redZone = max === 7000 ? `<defs><linearGradient id="rpm-red-zone" gradientUnits="userSpaceOnUse" x1="${redStart[0]}" y1="${redStart[1]}" x2="${redEnd[0]}" y2="${redEnd[1]}"><stop stop-color="#f7a65d" stop-opacity="0.1"/><stop offset="0.5" stop-color="#f67855"/><stop offset="1" stop-color="#fa4d5b"/></linearGradient></defs><path class="rpm-red-zone" d="M${redStart.join(' ')} A128 128 0 0 1 ${redEnd.join(' ')}"/>` : '';
|
||||
const levelKey = max === 7000 ? 'coolantCelsius' : 'fuelPercent';
|
||||
const iconPath = icons[levelKey];
|
||||
// Compact auxiliary arc, separated from the main instrument.
|
||||
const levelPoint = (angle, radius = 64) => [150+radius*Math.sin(angle*Math.PI/180),244-radius*Math.cos(angle*Math.PI/180)];
|
||||
// 86.8 degrees: 30% shorter than the previous 124-degree auxiliary arc.
|
||||
const arcPath = (from,to) => `M${levelPoint(from).join(' ')} A64 64 0 0 0 ${levelPoint(to).join(' ')}`;
|
||||
const arc = arcPath(223.4,136.6);
|
||||
const redArc = max===7000 ? arcPath(153.96,136.6) : arcPath(223.4,212.984);
|
||||
const tickRatios = max === 7000 ? [0,.28,.56,.84,1] : [0,.12,.4133333333,.7066666667,1];
|
||||
const levelTicks = tickRatios.map((fraction,index) => {
|
||||
const angle = 223.4 - 86.8*fraction;
|
||||
const inner=levelPoint(angle,54), outer=levelPoint(angle,61);
|
||||
return `<path class="level-tick" d="M${inner.join(' ')} L${outer.join(' ')}"/>`;
|
||||
}).join('');
|
||||
const level = `<g class="digital-levels" data-digital-level="${levelKey}"><path class="level-arc-track" d="${arc}"/><path class="level-arc-progress" pathLength="100" d="${arc}"/><path class="level-arc-red" d="${redArc}"/>${levelTicks}<text class="level-end" x="95" y="304">${max===7000?'C':'E'}</text><text class="level-end" x="205" y="304">${max===7000?'H':'F'}</text>${max===7000?'':'<text class="level-end" x="150" y="325">½</text>'}<g class="level-icon" transform="translate(${max===7000?139.8:141.6} ${max===7000?267.8:269.6}) scale(${max===7000?.85:.7})" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${iconPath}</g><text class="level-value" x="150" y="348"><tspan data-value="${levelKey}">—</tspan> ${max===7000?'°C':'%'}</text></g>`;
|
||||
const low = max === 7000 ? 'C' : 'E', high = max === 7000 ? 'H' : 'F';
|
||||
const mini = `<path class="analog-separator" d="M59.590 240.609 L77 226 L223 226 L240.410 240.609"/><g class="analog-levels" data-analog-level="${levelKey}"><g transform="translate(150 264) scale(0.90) translate(-150 -264)"><g transform="translate(0 572) scale(1 -1)"><path class="mini-track" d="M90 308 A60 60 0 0 1 210 308"/><path class="mini-red" d="${max===7000?'M192.4 265.6 A60 60 0 0 1 210 308':'M90 308 A60 60 0 0 1 107.6 265.6'}"/><path class="mini-ticks" d="M90 308h8M107.6 265.6l6 6M150 248v8M192.4 265.6l-6 6M210 308h-8"/><text class="mini-end" transform="translate(0 604) scale(1 -1)" x="104" y="302">${low}</text><text class="mini-end" transform="translate(0 604) scale(1 -1)" x="196" y="302">${high}</text>${max===7000?'':`<text class="mini-end" transform="translate(0 520) scale(1 -1)" x="150" y="260">½</text>`}<line class="mini-needle" x1="150" y1="308" x2="150" y2="258"/><circle class="mini-hub" cx="150" cy="308" r="3"/></g></g><g class="level-icon" transform="translate(${max===7000?139.8:141.6} ${max===7000?277.8:279.6}) scale(${max===7000?.85:.7})" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${iconPath}</g><text class="level-value" x="150" y="368"><tspan data-value="${levelKey}">—</tspan> ${max===7000?'°C':'%'}</text></g>`;
|
||||
$(id).innerHTML = `<svg viewBox="0 0 300 265" role="img" aria-label="${label} : en attente"><path class="dial-rim" d="M68.4 255 A133 133 0 1 1 231.6 255 Z" fill="none" stroke="var(--border)"/><path class="dial-light" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/><path class="digital-track" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/><path class="digital-progress" pathLength="100" d="M51.9 232.3 A128 128 0 1 1 248.1 232.3"/>${ticks}${redZone}<text x="150" y="110" class="dial-label analog-unit">${unit}</text><text x="150" y="110" class="dial-label digital-unit">${max === 7000 ? 'TR/MIN' : unit}</text><line class="needle" x1="150" y1="150" x2="150" y2="43" style="transform-origin:150px 150px;transform:rotate(-130deg)"/><circle class="needle-hub" cx="150" cy="150" r="7" fill="var(--accent)"/><circle class="needle-hub" cx="150" cy="150" r="3" fill="var(--surface)"/><text x="150" y="212" class="dial-value">—</text><text x="150" y="239" class="dial-label">${label}</text>${level}${mini}</svg>`;
|
||||
return value => {
|
||||
const valid = typeof value === 'number' && Number.isFinite(value);
|
||||
$(`${id} .dial-value`).textContent = valid ? Math.round(value).toLocaleString('fr-FR') : '—';
|
||||
$(`${id} .digital-progress`).style.strokeDashoffset = String(valid ? 100 - Math.max(0,Math.min(1,value/max))*100 : 100);
|
||||
$(`${id} .needle`).style.opacity = valid ? '1' : '0';
|
||||
if (valid) $(`${id} .needle`).style.transform = `rotate(${-130 + Math.max(0, Math.min(max, value)) / max * 260}deg)`;
|
||||
$(`${id} svg`).setAttribute('aria-label', `${label} : ${valid ? Math.round(value) : 'indisponible'} ${max === 7000 ? 'tr/min' : 'km/h'}`);
|
||||
};
|
||||
}
|
||||
const speedGauge = makeGauge('#speed-gauge', 220, 20, 'KM/H', 'VITESSE');
|
||||
const rpmGauge = makeGauge('#rpm-gauge', 7000, 1000, '× 1 000 TR/MIN', 'RÉGIME MOTEUR');
|
||||
function updateLevels(data) {
|
||||
for (const key of ['fuelPercent', 'coolantCelsius']) {
|
||||
const value = data?.[key], valid = Number.isFinite(value);
|
||||
const min = key === 'fuelPercent' ? 0 : 40, max = key === 'fuelPercent' ? 100 : 130;
|
||||
const ratio = valid ? Math.max(0,Math.min(1,(value-min)/(max-min))) : 0;
|
||||
const analog = document.querySelector(`[data-analog-level="${key}"]`);
|
||||
analog.querySelector('.mini-needle').style.transform = `rotate(${-90 + ratio*180}deg)`;
|
||||
analog.querySelector('.mini-needle').style.opacity = valid ? '1' : '0';
|
||||
analog.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
|
||||
const digital = document.querySelector(`[data-digital-level="${key}"]`);
|
||||
const stops = key === 'fuelPercent' ? [0,.12,.4133333333,.7066666667,1] : [0,.28,.56,.84,1];
|
||||
const stepped = stops.filter(stop => stop <= ratio).at(-1) ?? 0;
|
||||
const progress = digital.querySelector('.level-arc-progress');
|
||||
progress.style.strokeDashoffset = String(100*(1-stepped));
|
||||
progress.style.opacity = valid && stepped > 0 ? '1' : '0';
|
||||
digital.classList.toggle('last-level', valid && key === 'coolantCelsius' && ratio >= .84);
|
||||
digital.classList.toggle('reserve-level', valid && key === 'fuelPercent' && stepped === .12);
|
||||
digital.classList.toggle('red-zone', valid && (key === 'fuelPercent' ? ratio <= .25 : ratio >= .75));
|
||||
for (const el of [analog, digital]) {
|
||||
el.setAttribute('role', 'meter');
|
||||
el.setAttribute('aria-label', key === 'fuelPercent' ? 'Carburant' : 'Température moteur');
|
||||
el.setAttribute('aria-valuemin', String(min)); el.setAttribute('aria-valuemax', String(max));
|
||||
if (valid) el.setAttribute('aria-valuenow', String(value)); else el.removeAttribute('aria-valuenow');
|
||||
el.setAttribute('aria-valuetext', valid ? value + (key === 'fuelPercent' ? ' %' : ' °C') : 'Indisponible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { speedGauge, rpmGauge, updateLevels };
|
||||
}
|
||||
+441
-21
@@ -1,21 +1,441 @@
|
||||
/* The accent is the only configurable color; surfaces derive from it. */
|
||||
body.light{--bg:color-mix(in srgb,var(--accent) 3%,#f2f2ee);--surface2:color-mix(in srgb,var(--accent) 4%,#f5f5f2);--border:color-mix(in srgb,var(--accent) 9%,#deded8)}
|
||||
.rail,body.light .rail{background:color-mix(in srgb,var(--accent) 4%,var(--bg))}
|
||||
.brand span,.gear>span,.text-link span{color:var(--accent)}
|
||||
.clock-card{background:color-mix(in srgb,var(--accent) 13%,#14201e)}
|
||||
.sun{background:var(--accent);box-shadow:0 0 80px color-mix(in srgb,var(--accent) 20%,transparent)}
|
||||
.mountain{background:color-mix(in srgb,var(--accent) 22%,#23312d)}
|
||||
.mountain.two{background:color-mix(in srgb,var(--accent) 8%,#12221e)}
|
||||
.road{background:color-mix(in srgb,var(--accent) 45%,#48534c)}
|
||||
.album-art{background:linear-gradient(140deg,color-mix(in srgb,var(--accent) 70%,#849484),color-mix(in srgb,var(--accent) 12%,#243a33))}
|
||||
.record:after{border-color:color-mix(in srgb,var(--accent) 45%,#314436)}
|
||||
.tiny-dot{box-shadow:0 0 12px color-mix(in srgb,var(--accent) 25%,transparent)}
|
||||
body.light .rail nav a.active{color:var(--ink)}
|
||||
.theme-grid{display:grid;grid-template-columns:1fr 1fr;gap:15px}input[type=color]{width:100%;height:46px;padding:4px;border:1px solid var(--border);border-radius:10px;background:var(--surface2)}
|
||||
.weather-actions{display:flex;align-items:center;gap:9px;margin:0 0 10px;flex-wrap:wrap}.weather-actions button{font-size:10px;padding:6px 10px}.weather-actions a{font-size:10px;color:var(--accent)}.weather-credit{font-size:8px;color:var(--muted);margin-top:8px}.weather-main strong #weather-temperature{color:var(--text)}.weather-card{min-height:280px}.weather-card #weather-status{font-size:10px;min-height:26px;margin-bottom:8px}
|
||||
.journey-grid{display:grid;grid-template-columns:minmax(0,1fr) 285px;gap:16px}.map-column{min-width:0}#map-canvas{height:420px;min-height:200px;border-radius:17px;border:1px solid var(--border);background:var(--surface2);z-index:0;isolation:isolate}.journey-panel{padding:18px;overflow:auto;max-height:680px}.journey-panel label{font-size:11px;display:block;margin:12px 0 7px}.journey-panel input:not([type=checkbox]):not([type=file]),.journey-panel select{width:100%;min-width:0;min-height:44px;border:1px solid var(--border);border-radius:8px;padding:10px;color:var(--text);background:var(--surface2)}.search-line{display:flex;gap:6px}.search-line input{flex:1}.search-line button{font-size:23px;padding:5px 14px}#place-results button{text-align:left;width:100%;margin-top:8px;font-size:11px;overflow-wrap:anywhere}#place-results p{font-size:11px;color:var(--muted)}.map-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.map-actions button,.map-actions label{font-size:11px}.journey-panel p,#map-status{font-size:11px;color:var(--muted);line-height:1.5}.journey-panel details{padding:14px 0;border-top:1px solid var(--border);margin-top:10px}.journey-panel summary{font-size:12px;cursor:pointer;min-height:34px;padding-top:8px}.provider-credit a{color:var(--accent)}#route-steps{padding-left:20px;font-size:11px;color:var(--muted)}#route-steps li{padding:8px 0}#route-steps li.current{color:var(--accent)}#guidance{padding:15px;margin-top:10px}#guidance strong{font-size:18px}#guidance p{font-size:11px}.journey-panel label.check-label{display:flex;align-items:center;gap:10px;min-height:36px}.check-label input{accent-color:var(--accent)}#zone-alert{position:fixed;z-index:1500;bottom:22px;left:50%;transform:translateX(-50%);background:#fbd499;color:#30200d;border:2px solid #af6d16;box-shadow:var(--shadow);border-radius:14px;padding:18px 65px 18px 22px;max-width:calc(100vw - 28px);width:520px}#zone-alert strong,#zone-alert span{display:block}#zone-alert span{font-size:12px;margin-top:6px}#zone-alert button{position:absolute;right:10px;top:10px;background:#fff4;color:#30200d;border-color:#af6d16}
|
||||
.leaflet-container{font-family:inherit;color:#20312a}.leaflet-container .leaflet-control-attribution{font-size:10px}.leaflet-container .leaflet-control-attribution a{color:#22557b}.leaflet-control-zoom a{min-width:40px;min-height:40px;line-height:40px!important}.leaflet-tooltip{font-size:12px}.leaflet-control-attribution{max-width:100%;white-space:normal}.rail nav{gap:8px}.rail nav a{min-height:65px}
|
||||
@media(max-width:1100px){.journey-grid{grid-template-columns:minmax(0,1fr) 250px}#map-canvas{height:340px}.journey-panel{padding:14px}}
|
||||
@media(max-height:650px) and (min-width:760px){.weather-card{min-height:260px}.weather-card .weather-main{margin:4px 0}.weather-card #weather-status{min-height:0}.weather-actions{margin-bottom:6px}.weather-credit{margin-top:5px}#map-canvas{height:270px}.journey-panel{max-height:480px}.rail nav a{min-height:56px}.rail nav{gap:4px}}
|
||||
@media(max-height:520px) and (min-width:760px){.rail nav a{min-height:48px;gap:4px}.rail nav .icon{font-size:21px}.weather-card{min-height:220px}.weather-actions{gap:5px}.weather-actions button{font-size:9px;padding:5px}.weather-card #weather-status{display:block}.weather-credit{font-size:7px}#map-canvas{height:230px}.journey-panel{max-height:420px}}
|
||||
@media(max-width:759px){.journey-grid{grid-template-columns:1fr}.journey-panel{max-height:none}#map-canvas{height:320px}.map-actions button{padding:8px}.weather-card{min-height:290px}.theme-grid{gap:10px}.rail nav a{min-height:60px}.leaflet-container .leaflet-control-attribution{font-size:9px}}
|
||||
body.light {
|
||||
--bg: color-mix(in srgb,var(--accent) 3%,#f2f2ee);
|
||||
--surface2: color-mix(in srgb,var(--accent) 4%,#f5f5f2);
|
||||
--border: color-mix(in srgb,var(--accent) 9%,#deded8);
|
||||
}
|
||||
|
||||
.rail, body.light .rail {
|
||||
background: color-mix(in srgb,var(--accent) 4%,var(--bg));
|
||||
}
|
||||
|
||||
.brand span, .gear > span, .text-link span {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.clock-card {
|
||||
background: color-mix(in srgb,var(--accent) 13%,#14201e);
|
||||
}
|
||||
|
||||
.sun {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 80px color-mix(in srgb,var(--accent) 20%,transparent);
|
||||
}
|
||||
|
||||
.mountain {
|
||||
background: color-mix(in srgb,var(--accent) 22%,#23312d);
|
||||
}
|
||||
|
||||
.mountain.two {
|
||||
background: color-mix(in srgb,var(--accent) 8%,#12221e);
|
||||
}
|
||||
|
||||
.road {
|
||||
background: color-mix(in srgb,var(--accent) 45%,#48534c);
|
||||
}
|
||||
|
||||
.album-art {
|
||||
background: linear-gradient(140deg,color-mix(in srgb,var(--accent) 70%,#849484),color-mix(in srgb,var(--accent) 12%,#243a33));
|
||||
}
|
||||
|
||||
.record::after {
|
||||
border-color: color-mix(in srgb,var(--accent) 45%,#314436);
|
||||
}
|
||||
|
||||
.tiny-dot {
|
||||
box-shadow: 0 0 12px color-mix(in srgb,var(--accent) 25%,transparent);
|
||||
}
|
||||
|
||||
body.light .rail nav a.active {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
.weather-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin: 0px 0px 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.weather-actions button {
|
||||
font-size: 10px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.weather-actions a {
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.weather-credit {
|
||||
font-size: 8px;
|
||||
color: var(--muted);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.weather-main strong #weather-temperature {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.weather-card {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.weather-card #weather-status {
|
||||
font-size: 10px;
|
||||
min-height: 26px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.journey-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0px, 1fr) 285px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.map-column {
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
#map-canvas {
|
||||
height: 420px;
|
||||
min-height: 200px;
|
||||
border-radius: 17px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface2);
|
||||
z-index: 0;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.journey-panel {
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
max-height: 680px;
|
||||
}
|
||||
|
||||
.journey-panel label {
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
margin: 12px 0px 7px;
|
||||
}
|
||||
|
||||
.journey-panel input:not([type="checkbox"]):not([type="file"]), .journey-panel select {
|
||||
width: 100%;
|
||||
min-width: 0px;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
color: var(--text);
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
.search-line {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.search-line input {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.search-line button {
|
||||
font-size: 23px;
|
||||
padding: 5px 14px;
|
||||
}
|
||||
|
||||
#place-results button {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#place-results p {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.map-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.map-actions button, .map-actions label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.journey-panel p, #map-status {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.journey-panel details {
|
||||
padding: 14px 0px;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.journey-panel summary {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
min-height: 34px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.provider-credit a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#route-steps {
|
||||
padding-left: 20px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#route-steps li {
|
||||
padding: 8px 0px;
|
||||
}
|
||||
|
||||
#route-steps li.current {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#guidance {
|
||||
padding: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#guidance strong {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#guidance p {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.journey-panel label.check-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.check-label input {
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
#zone-alert {
|
||||
position: fixed;
|
||||
z-index: 1500;
|
||||
bottom: 22px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgb(251, 212, 153);
|
||||
color: rgb(48, 32, 13);
|
||||
border: 2px solid rgb(175, 109, 22);
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: 14px;
|
||||
padding: 18px 65px 18px 22px;
|
||||
max-width: calc(-28px + 100vw);
|
||||
width: 520px;
|
||||
}
|
||||
|
||||
#zone-alert strong, #zone-alert span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#zone-alert span {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
#zone-alert button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
background: rgba(255, 255, 255, 0.267);
|
||||
color: rgb(48, 32, 13);
|
||||
border-color: rgb(175, 109, 22);
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
font-family: inherit;
|
||||
color: rgb(32, 49, 42);
|
||||
}
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.leaflet-container .leaflet-control-attribution a {
|
||||
color: rgb(34, 85, 123);
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
line-height: 40px !important;
|
||||
}
|
||||
|
||||
.leaflet-tooltip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.rail nav {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rail nav a {
|
||||
min-height: 65px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.journey-grid {
|
||||
grid-template-columns: minmax(0px, 1fr) 250px;
|
||||
}
|
||||
|
||||
#map-canvas {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.journey-panel {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 650px) and (min-width: 760px) {
|
||||
.weather-card {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.weather-card .weather-main {
|
||||
margin: 4px 0px;
|
||||
}
|
||||
|
||||
.weather-card #weather-status {
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
.weather-actions {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.weather-credit {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#map-canvas {
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.journey-panel {
|
||||
max-height: 480px;
|
||||
}
|
||||
|
||||
.rail nav a {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.rail nav {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 520px) and (min-width: 760px) {
|
||||
.rail nav a {
|
||||
min-height: 48px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rail nav .icon {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.weather-card {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.weather-actions {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.weather-actions button {
|
||||
font-size: 9px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.weather-card #weather-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.weather-credit {
|
||||
font-size: 7px;
|
||||
}
|
||||
|
||||
#map-canvas {
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
.journey-panel {
|
||||
max-height: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 759px) {
|
||||
.journey-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.journey-panel {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
#map-canvas {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.map-actions button {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.weather-card {
|
||||
min-height: 290px;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.rail nav a {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Local playlist and shared audio controls. Files never leave the browser.
|
||||
export function initMusic({ storage, toast }) {
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const audio = $('#audio');
|
||||
let tracks = [], selected = -1;
|
||||
let shuffle = storage.get('shuffle') === 'true', shuffleBag = [], playbackHistory = [];
|
||||
$('#immersive-shuffle').setAttribute('aria-pressed', String(shuffle));
|
||||
$('#immersive-shuffle').onclick = () => {
|
||||
shuffle = !shuffle; shuffleBag = []; playbackHistory = [];
|
||||
storage.set('shuffle', String(shuffle));
|
||||
$('#immersive-shuffle').setAttribute('aria-pressed', String(shuffle));
|
||||
toast(shuffle ? 'Lecture aléatoire activée' : 'Lecture aléatoire désactivée');
|
||||
};
|
||||
function nextTrack() {
|
||||
if (!shuffle || tracks.length < 2) return selectTrack(selected + 1, true);
|
||||
if (!shuffleBag.length) {
|
||||
shuffleBag = tracks.map((_, index) => index).filter(index => index !== selected);
|
||||
for (let i = shuffleBag.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffleBag[i], shuffleBag[j]] = [shuffleBag[j], shuffleBag[i]];
|
||||
}
|
||||
}
|
||||
selectTrack(shuffleBag.pop(), true);
|
||||
}
|
||||
function renderPlaylist() {
|
||||
$('#playlist').replaceChildren();
|
||||
tracks.forEach((track, index) => {
|
||||
const li = document.createElement('li'), button = document.createElement('button');
|
||||
const number = document.createElement('span'), name = document.createElement('b');
|
||||
number.textContent = String(index + 1).padStart(2, '0'); name.textContent = track.name;
|
||||
button.append(number, name); button.classList.toggle('selected', index === selected);
|
||||
button.setAttribute('aria-pressed', String(index === selected)); button.onclick = () => selectTrack(index, true);
|
||||
li.append(button); $('#playlist').append(li);
|
||||
});
|
||||
$('#track-count').textContent = `${tracks.length} TITRE${tracks.length > 1 ? 'S' : ''}`;
|
||||
$('#empty-library').hidden = tracks.length > 0;
|
||||
['#play', '#previous', '#next'].forEach(id => $(id).disabled = tracks.length === 0);
|
||||
}
|
||||
async function playAudio() {
|
||||
try { await audio.play(); $('#audio-message').textContent = 'Lecture locale · aucun fichier envoyé au serveur.'; }
|
||||
catch { $('#audio-message').textContent = 'Lecture impossible : réessayez ou choisissez un format audio pris en charge.'; }
|
||||
}
|
||||
function selectTrack(index, play = false, remember = true) {
|
||||
if (!tracks.length) return;
|
||||
previousTap = -Infinity;
|
||||
if (remember && selected >= 0 && index !== selected) {
|
||||
playbackHistory.push(selected);
|
||||
if (playbackHistory.length > 256) playbackHistory.shift();
|
||||
}
|
||||
selected = (index + tracks.length) % tracks.length;
|
||||
shuffleBag = shuffleBag.filter(index => index !== selected);
|
||||
audio.src = tracks[selected].url;
|
||||
$('#track-title').textContent = tracks[selected].name;
|
||||
$('#home-track').textContent = tracks[selected].name;
|
||||
$('#track-subtitle').textContent = `Morceau ${selected + 1} sur ${tracks.length} · Bibliothèque locale`;
|
||||
$('#home-track-sub').textContent = 'Bibliothèque locale';
|
||||
renderPlaylist(); if (play) playAudio();
|
||||
}
|
||||
$('#music-files').addEventListener('change', event => {
|
||||
let added = 0;
|
||||
for (const file of event.target.files) {
|
||||
if (!file.type.startsWith('audio/') && !/\.(mp3|flac|ogg|wav|m4a|aac|opus)$/i.test(file.name)) continue;
|
||||
tracks.push({ name: file.name.replace(/\.[^.]+$/, ''), url: URL.createObjectURL(file) }); added++;
|
||||
}
|
||||
shuffleBag = [];
|
||||
renderPlaylist();
|
||||
if (selected === -1 && tracks.length) selectTrack(0);
|
||||
toast(added ? `${added} morceau${added > 1 ? 'x' : ''} ajouté${added > 1 ? 's' : ''}` : 'Aucun fichier audio reconnu');
|
||||
event.target.value = '';
|
||||
});
|
||||
$('#play').onclick = () => { if (selected === -1) return; if (audio.paused) playAudio(); else audio.pause(); };
|
||||
let previousTap = -Infinity;
|
||||
$('#previous').onclick = () => {
|
||||
const now = performance.now(), twice = now - previousTap < 600;
|
||||
previousTap = twice ? -Infinity : now;
|
||||
if (!twice) { audio.currentTime = 0; return; }
|
||||
if (shuffle) { shuffleBag = []; selectTrack(playbackHistory.pop() ?? selected, true, false); }
|
||||
else selectTrack(selected - 1, true);
|
||||
};
|
||||
$('#next').onclick = nextTrack;
|
||||
audio.addEventListener('play', () => $('#play').textContent = 'Pause');
|
||||
audio.addEventListener('pause', () => $('#play').textContent = 'Lecture');
|
||||
audio.addEventListener('ended', () => { if (audio.loop) return; if (shuffle || selected + 1 < tracks.length) nextTrack(); else $('#play').textContent = 'Lecture'; });
|
||||
audio.addEventListener('error', () => $('#audio-message').textContent = 'Ce fichier ne peut pas être lu. Essayez un autre format audio.');
|
||||
window.addEventListener('pagehide', event => { if (!event.persisted) tracks.forEach(track => URL.revokeObjectURL(track.url)); });
|
||||
renderPlaylist();
|
||||
|
||||
}
|
||||
+13
-2
@@ -1,14 +1,25 @@
|
||||
map $uri $caria_backend {
|
||||
default telemetry:8080;
|
||||
/api/weather weather:8080;
|
||||
/api/map-config navigation:8080;
|
||||
/api/search navigation:8080;
|
||||
/api/route navigation:8080;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 4;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/css text/javascript application/javascript application/json image/svg+xml;
|
||||
location /api/ {
|
||||
access_log off;
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
set $backend http://telemetry:8080;
|
||||
proxy_pass $backend;
|
||||
proxy_pass http://$caria_backend;
|
||||
proxy_connect_timeout 2s;
|
||||
proxy_read_timeout 12s;
|
||||
}
|
||||
|
||||
+1440
-12
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user