Phase 1 : moteur Python et commande rtg
- Paquet roadtripsgenerator (pyproject.toml), commande rtg : generer, etat, analyser, compresser, accueil, migrer - Réglages site.toml / voyage.toml (anciens mymaps.txt et moto.txt encore lus) ; qualité vidéo en trois crans - Sources laissées en place : traces découpées par journée d'après leurs horodatages (coupure à 4 h) ou la date du nom de fichier, photos copiées, HEIC converties en JPEG, position déduite de la trace pour les photos sans GPS - extraction_infos.sh et compress.sh portés en Python (plus de bash) ; sorties vidéo écrites sous un nom temporaire ; positions des rushs inchangés reprises du dernier export ; export vidéo conservé quand le disque des rushs est débranché - Pages, accueil et carte globale repris dans le paquet : sortie identique aux scripts (123 fichiers comparés sur 7 voyages) - Tests pytest sur un voyage d'exemple fabriqué à la volée Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
1 parent
97b936fd3e
commit
f0e1c2dfce
23 files changed
+3083
-1
No files matched your search
@@ -0,0 +1,183 @@
|
||||
/* =============================================================================
|
||||
Pages d'un voyage : vidéos, diaporamas photo et carnet de route.
|
||||
Copié dans <voyage>/assets/ à chaque génération.
|
||||
|
||||
Les gestionnaires sont appelés par des onclick="" dans les popups : Leaflet
|
||||
bloque la propagation des clics hors des popups, une délégation au niveau
|
||||
du document ne les verrait pas.
|
||||
============================================================================= */
|
||||
|
||||
// --- Vidéos ------------------------------------------------------------------
|
||||
// Le 720p (H.264) est chargé par défaut et se lit partout. Le 1080p est en
|
||||
// HEVC : si le navigateur ne le décode pas (Firefox surtout), on grise le bouton.
|
||||
var HEVC_OK = (function () {
|
||||
try {
|
||||
var t = document.createElement('video').canPlayType('video/mp4; codecs="hvc1.1.6.L120.B0"');
|
||||
return t === 'probably' || t === 'maybe';
|
||||
} catch (e) { return false; }
|
||||
})();
|
||||
|
||||
function applyCodecSupport(popup) {
|
||||
if (HEVC_OK) return;
|
||||
var btns = popup.querySelectorAll('.quality-btn[data-hevc="1"]');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
btns[i].classList.add('qbtn-disabled');
|
||||
btns[i].title = "Le 1080p est encodé en HEVC, que ce navigateur ne sait pas décoder. Essaie Safari, Chrome ou Edge.";
|
||||
}
|
||||
var note = popup.querySelector('.hevc-note');
|
||||
if (note) note.style.display = 'block';
|
||||
}
|
||||
|
||||
function changeQuality(btn) {
|
||||
if (btn.classList.contains('qbtn-disabled')) return;
|
||||
var box = btn.closest('.popup-video');
|
||||
var video = box.querySelector('video');
|
||||
var newSrc = btn.getAttribute('data-src');
|
||||
|
||||
// video.src est une URL absolue, data-src est relatif : on compare les noms.
|
||||
var nomActuel = decodeURIComponent(video.src).split('/').pop();
|
||||
var nomVoulu = decodeURIComponent(newSrc).split('/').pop();
|
||||
if (nomActuel === nomVoulu) return;
|
||||
|
||||
var savedTime = video.currentTime;
|
||||
var wasPlaying = !video.paused;
|
||||
video.src = newSrc;
|
||||
video.onloadedmetadata = function () {
|
||||
this.currentTime = savedTime;
|
||||
if (wasPlaying) {
|
||||
var p = this.play();
|
||||
if (p !== undefined) p.catch(function () { console.log("Lecture bloquée"); });
|
||||
}
|
||||
this.onloadedmetadata = null;
|
||||
};
|
||||
video.load();
|
||||
|
||||
var buttons = box.querySelectorAll('.quality-btn');
|
||||
for (var i = 0; i < buttons.length; i++) buttons[i].classList.remove('active-720', 'active-1080');
|
||||
btn.classList.add(btn.getAttribute('data-hevc') === '1' ? 'active-1080' : 'active-720');
|
||||
}
|
||||
|
||||
// À l'ouverture d'une popup vidéo : codec, image d'aperçu, puis lecture.
|
||||
// L'aperçu n'est posé qu'à ce moment-là : en attribut poster="" direct, le
|
||||
// navigateur chargerait d'un coup les aperçus de toutes les vidéos du jour.
|
||||
function nomFichier(u) { return decodeURIComponent(u || '').split('/').pop(); }
|
||||
|
||||
// Filet de sécurité : si une version ne se lit pas (1080p HEVC malgré la
|
||||
// détection, fichier abîmé ou absent), on essaie l'autre.
|
||||
function surErreurVideo() {
|
||||
var video = this, box = video.closest('.popup-video');
|
||||
video._essais = video._essais || {};
|
||||
video._essais[nomFichier(video.src)] = true;
|
||||
var candidats = [video.getAttribute('data-fallback')];
|
||||
var hd = box.querySelector('.quality-btn[data-hevc="1"]');
|
||||
if (hd && HEVC_OK) candidats.push(hd.getAttribute('data-src'));
|
||||
for (var i = 0; i < candidats.length; i++) {
|
||||
var c = candidats[i];
|
||||
if (c && !video._essais[nomFichier(c)]) {
|
||||
var btn = box.querySelector('.quality-btn[data-src="' + c + '"]');
|
||||
if (btn) changeQuality(btn); else { video.src = c; video.load(); }
|
||||
var p = video.play();
|
||||
if (p !== undefined) p.catch(function () {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function preparePopup(node) {
|
||||
applyCodecSupport(node);
|
||||
var video = node.querySelector('video');
|
||||
if (!video) return;
|
||||
// Leaflet réinsère la même popup à chaque ouverture : une seule initialisation.
|
||||
if (!video._pret) {
|
||||
video._pret = true;
|
||||
var apercu = video.getAttribute('data-poster');
|
||||
if (apercu) video.setAttribute('poster', apercu);
|
||||
video.addEventListener('error', surErreurVideo);
|
||||
}
|
||||
var p = video.play();
|
||||
if (p !== undefined) p.catch(function (e) { console.log("Autoplay bloqué par le navigateur", e); });
|
||||
}
|
||||
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (m) {
|
||||
m.addedNodes.forEach(function (node) {
|
||||
if (node.nodeType === 1 && node.classList && node.classList.contains('leaflet-popup')) {
|
||||
preparePopup(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
|
||||
// --- Diaporama photos ----------------------------------------------------------
|
||||
function moveSlide(btn, step) {
|
||||
var slides = btn.parentElement.querySelectorAll('.slides .slide');
|
||||
var actif = Array.prototype.findIndex.call(slides, function (s) { return s.style.display !== 'none'; });
|
||||
if (actif === -1) actif = 0;
|
||||
slides[actif].style.display = 'none';
|
||||
var suivant = (actif + step + slides.length) % slides.length;
|
||||
slides[suivant].style.display = 'block';
|
||||
btn.parentElement.querySelector('.slide-counter').innerText = (suivant + 1) + '/' + slides.length;
|
||||
}
|
||||
|
||||
// --- Carnet de route -----------------------------------------------------------
|
||||
// localStorage lève une exception sur file:// dans certains navigateurs
|
||||
// (Safari, navigation privée) : on l'enveloppe.
|
||||
function memLire(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
||||
function memEcrire(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||||
|
||||
// Journée à afficher : celle de l'URL (#jour-AAAA-MM-JJ) ou celle de la page.
|
||||
function journalCible() {
|
||||
var m = /#jour-(\d{4}-\d{2}-\d{2})/.exec(window.location.hash || '');
|
||||
return m ? m[1] : (window.JOUR_COURANT || null);
|
||||
}
|
||||
|
||||
function journalDefile(jour) {
|
||||
var panel = document.getElementById('journal-panel');
|
||||
if (!panel || !jour) return false;
|
||||
var el = document.getElementById('ancre-' + jour);
|
||||
if (!el) return false;
|
||||
var r = el.getBoundingClientRect(), pr = panel.getBoundingClientRect();
|
||||
panel.scrollTop += (r.top - pr.top) - 70; // laisse voir le titre du jour
|
||||
var actif = panel.querySelector('.jour-actif');
|
||||
if (actif) actif.classList.remove('jour-actif');
|
||||
el.classList.add('jour-actif');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rejoué plusieurs fois : les CSS distantes (Leaflet, Font Awesome) arrivent
|
||||
// après DOMContentLoaded et décalent les hauteurs.
|
||||
function journalRecale() {
|
||||
var j = journalCible();
|
||||
journalDefile(j);
|
||||
[150, 500, 1200].forEach(function (d) { setTimeout(function () { journalDefile(j); }, d); });
|
||||
}
|
||||
|
||||
function toggleJournal(open) {
|
||||
var panel = document.getElementById('journal-panel');
|
||||
if (!panel) return;
|
||||
panel.classList.toggle('open', open);
|
||||
memEcrire('journalOpen', open ? 'true' : 'false');
|
||||
if (open) journalRecale();
|
||||
}
|
||||
|
||||
function journalInit() {
|
||||
var panel = document.getElementById('journal-panel');
|
||||
if (!panel) return;
|
||||
var vientDuCarnet = /#jour-\d{4}-\d{2}-\d{2}/.test(window.location.hash || '');
|
||||
if (memLire('journalOpen') === 'true' || vientDuCarnet) {
|
||||
panel.classList.add('open');
|
||||
if (vientDuCarnet) memEcrire('journalOpen', 'true');
|
||||
}
|
||||
journalRecale();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
window.addEventListener('DOMContentLoaded', journalInit);
|
||||
} else {
|
||||
journalInit();
|
||||
}
|
||||
window.addEventListener('load', journalRecale);
|
||||
window.addEventListener('hashchange', function () { toggleJournal(true); });
|
||||
Reference in new issue
Block a user