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:
FabriceandClaude Opus 5.5 committed 2026-10-09 11:31:31 +00:00
1 parent 97b936fd3e
commit f0e1c2dfce
23 files changed
+3083 -1

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
{#- Page d'accueil : une rubrique par section (Voyages, Balades…), avec les
voyages les plus récents et un lien vers la liste complète. -#}
{% extends "base_accueil.html" %}
{% from "base_accueil.html" import carte %}
{% block sous_titre %}{{ sous_titre }}{% endblock %}
{% block contenu %}
{% if not rubriques %}
<p class="vide">Aucun voyage pour l'instant.<br>Lance <code>genere_site.sh</code> pour en générer.</p>
{% else %}
<div class="rubriques">
{% for r in rubriques %}
<section>
<div class="rubrique-titre">
<h2>{{ r.titre }}</h2>
<a href="{{ r.page }}">Tout voir ({{ r.voyages|length }}) →</a>
</div>
<div class="grid compact">
{% for v in r.voyages[:nb_recents] %}
{{ carte(v) }}
{% endfor %}
</div>
</section>
{% endfor %}
</div>
{% endif %}
{% if carte_globale %}
<section class="carte-externe" id="carte">
<div class="rubrique-titre">
<h2>La carte de nos principaux voyages</h2>
<a href="{{ carte_globale }}">Ouvrir en grand ↗</a>
</div>
<iframe src="{{ carte_globale }}" loading="lazy" title="Carte de nos principaux voyages"></iframe>
</section>
{% endif %}
{% endblock %}
+90
View File
@@ -0,0 +1,90 @@
{#- Gabarit commun à la page d'accueil (accueil.html) et aux listes complètes
(liste.html). Les pages filles remplissent les blocs titre et contenu. -#}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block titre %}{{ titre_site }}{% endblock %}</title>
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f4f4f9; margin: 0; padding: 20px; color: #222;
}
header { text-align: center; margin-bottom: 30px; }
h1 { margin: 0 0 6px; font-size: clamp(1.6rem, 4vw, 2.4rem); }
.sous-titre { color: #777; font-size: 0.9rem; }
main { max-width: 1200px; margin: 0 auto; }
a.retour { display: inline-block; margin-bottom: 10px; color: #3498db; text-decoration: none; font-weight: bold; }
/* Rubriques de l'accueil, côte à côte ; empilées quand la place manque. */
.rubriques {
display: grid; gap: 40px;
grid-template-columns: repeat(auto-fit, minmax(min(480px, 100%), 1fr));
}
.rubrique-titre {
display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
margin: 0 0 16px; padding-bottom: 6px; border-bottom: 2px solid #ddd;
}
.rubrique-titre h2 { margin: 0; font-size: 1.3rem; }
.rubrique-titre a, a.tout-voir { color: #3498db; text-decoration: none; font-weight: bold; font-size: 0.9rem; white-space: nowrap; }
a.tout-voir { display: block; margin-top: 14px; text-align: right; }
/* min() : sur un écran étroit, une colonne fixe déborderait. */
.grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fill, minmax(min(var(--carte, 400px), 100%), 1fr)); }
.compact { --carte: 220px; }
.card {
background: #fff; border-radius: 15px; overflow: hidden;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
transition: transform .2s, box-shadow .2s;
}
.card:hover { transform: translateY(-4px); box-shadow: 0 14px 28px rgba(0,0,0,0.15); }
/* Toute la carte est cliquable ; l'iframe ne capte pas les clics. */
.card > a { display: block; color: inherit; text-decoration: none; }
.map-container { width: 100%; height: 300px; border: none; pointer-events: none; display: block; }
.compact .map-container { height: 190px; }
.card-content { padding: 16px 20px 20px; text-align: center; }
.card h3 { text-transform: capitalize; margin: 0 0 4px; font-size: 1.2rem; }
.compact .card h3 { font-size: 1.05rem; }
.periode { color: #888; font-size: 0.85rem; }
.carte-externe { margin-top: 50px; scroll-margin-top: 20px; }
.carte-externe iframe {
display: block; width: 100%; height: min(600px, 75vh); border: none;
border-radius: 15px; box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.vide { text-align: center; color: #777; margin-top: 60px; line-height: 1.7; }
code { background: #e8e8ef; padding: 2px 6px; border-radius: 4px; }
@media (prefers-color-scheme: dark) {
body { background: #16161a; color: #e8e8ea; }
.card { background: #232329; box-shadow: 0 10px 20px rgba(0,0,0,0.4); }
.sous-titre, .vide, .periode { color: #9a9aa2; }
.rubrique-titre { border-color: #333; }
code { background: #2e2e36; }
}
</style>
</head>
<body>
<header>
<h1>{% block entete %}🌍 {{ titre_site }}{% endblock %}</h1>
<div class="sous-titre">{% block sous_titre %}{% endblock %}</div>
</header>
<main>
{% block contenu %}{% endblock %}
</main>
</body>
</html>
{% macro carte(v) -%}
<article class="card">
<a href="{{ v.chemin }}/index.html">
{# loading="lazy" : sinon chaque miniature charge une carte Leaflet complète dès l'ouverture #}
<iframe class="map-container" src="{{ v.chemin }}/mini.html" loading="lazy" title="Carte de {{ v.titre }}" tabindex="-1"></iframe>
<div class="card-content">
<h3>{{ v.titre }}</h3>
{% if v.periode %}<div class="periode">{{ v.periode }}</div>{% endif %}
</div>
</a>
</article>
{%- endmacro %}
+131
View File
@@ -0,0 +1,131 @@
/* =============================================================================
Pages d'un voyage : carte générale (index.html) et cartes du jour (html/).
Copié dans <voyage>/assets/ à chaque génération.
============================================================================= */
/* --- Contrôles Leaflet : zoom en bas à gauche, la barre latérale occupe le haut */
.leaflet-top.leaflet-left { top: auto !important; bottom: 20px !important; left: 15px !important; }
.leaflet-popup-content-wrapper { border-radius: 12px; max-width: 95vw !important; }
/* --- Épingles des médias --------------------------------------------------- */
.pin { position: relative; height: 40px; width: 24px; }
.pin-tete {
position: absolute; top: 0; z-index: 2;
width: 24px; height: 24px; border-radius: 50%;
background: var(--c); border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.4);
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 12px;
}
.pin-tige {
position: absolute; top: 22px; left: 10.5px; z-index: 1;
width: 3px; height: 18px; background: var(--c);
}
/* --- Boutons flottants ----------------------------------------------------- */
.btn-flottant {
position: fixed; right: 15px; z-index: 10001; display: block;
padding: 12px 20px; border: 2px solid #fff; border-radius: 50px;
color: #fff; text-decoration: none; font: bold 14px sans-serif;
box-shadow: 0 4px 15px rgba(0,0,0,0.4); cursor: pointer;
}
.btn-journal { bottom: 25px; z-index: 10000; background: #FFD700; color: #000; }
.btn-gpx { bottom: 85px; background: #28a745; }
.btn-accueil {
top: 15px; left: 15px; right: auto; border-radius: 10px;
background: #333; box-shadow: 0 4px 10px rgba(0,0,0,0.5);
}
/* --- Barre latérale de la carte générale ------------------------------------ */
.sidebar {
position: fixed; top: 15px; left: 15px; z-index: 9999;
width: 160px; max-height: 85vh; overflow-y: auto; padding: 12px;
background: rgba(255,255,255,0.9); border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2); font-family: sans-serif;
}
.sidebar a {
display: block; margin-bottom: 8px; padding: 12px; border-radius: 8px;
color: #fff; text-decoration: none; font-weight: bold; text-align: center;
}
.sidebar .btn-retour { margin-bottom: 15px; background: #444; border: 2px solid #666; }
.sidebar .jour { background: var(--c); }
.sidebar-titre {
border-top: 1px solid #ccc; margin-bottom: 10px; padding-top: 10px;
font-size: 12px; font-weight: bold; color: #666; text-align: center;
}
/* --- Popup d'une étape (clic sur la trace) ---------------------------------- */
.popup-trace { text-align: center; font-family: sans-serif; }
.popup-trace .btn-jour {
display: inline-block; margin-top: 14px; padding: 8px 12px; border-radius: 4px;
background: var(--c); color: #fff; text-decoration: none; font-weight: bold;
}
/* --- Popup vidéo ------------------------------------------------------------ */
.popup-video { width: calc(85vw - 40px); max-width: 1000px; text-align: center; }
.popup-video video {
display: block; width: 100%; max-height: 70vh;
border-radius: 8px; background: #000;
}
.header-row {
display: flex; justify-content: space-between; align-items: center;
background: #f0f0f0; padding: 5px 10px; border-radius: 4px; margin-bottom: 5px;
}
.file-name {
font: bold 11px monospace; color: #333; max-width: 60%;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.quality-controls { display: flex; gap: 6px; }
.quality-btn {
padding: 3px 8px; border: 1px solid #ccc; border-radius: 12px; background: #fff;
font: bold 10px sans-serif; text-transform: uppercase;
cursor: pointer; opacity: 0.6; transition: 0.2s;
}
.quality-btn:hover { opacity: 1; background: #eee; }
.active-720 { background: #2ECC71 !important; color: #fff; border-color: #27ae60 !important; opacity: 1 !important; }
.active-1080 { background: #007AFF !important; color: #fff; border-color: #0056b3 !important; opacity: 1 !important; }
.qbtn-disabled, .qbtn-disabled:hover {
opacity: 0.35 !important; background: #eee !important;
cursor: not-allowed !important; text-decoration: line-through;
}
.hevc-note {
display: none; margin-top: 6px; padding: 6px 10px; border-radius: 6px;
font: 11px sans-serif; color: #8a6d3b; background: #fcf8e3; text-align: left;
}
/* --- Popup photos (diaporama) ----------------------------------------------- */
.slider-container { position: relative; width: calc(85vw - 60px); max-width: 800px; min-height: 200px; text-align: center; }
.file-header { font: bold 11px monospace; color: #555; margin-bottom: 4px; }
.slide img { display: block; margin: auto; max-width: 100%; max-height: 65vh; border-radius: 8px; }
.nav-btn {
position: absolute; top: 50%; transform: translateY(-50%); z-index: 10;
padding: 12px; border: 2px solid #fff; border-radius: 50%;
background: rgba(0,0,0,0.6); color: #fff; font-size: 18px; cursor: pointer;
}
.prev { left: -10px; }
.next { right: -10px; }
.slide-counter { margin-top: 8px; font: bold 13px sans-serif; color: #333; }
/* --- Carnet de route -------------------------------------------------------- */
#journal-panel {
position: fixed; top: 0; right: -100%; z-index: 10005;
width: 400px; height: 100%; padding: 25px; overflow-y: auto;
background: #fff; box-shadow: -5px 0 15px rgba(0,0,0,0.2);
font-family: sans-serif; transition: 0.4s;
}
#journal-panel.open { right: 0; }
@media (max-width: 600px) { #journal-panel { width: 85%; } }
.close-btn {
position: absolute; top: 15px; right: 15px;
border: none; background: none; font-size: 24px; cursor: pointer;
}
.journal-content a.lien-jour {
display: inline-block; margin: 4px 0 10px; padding: 5px 12px; border-radius: 14px;
background: #007AFF; color: #fff !important; text-decoration: none;
font-size: 12px; font-weight: bold;
}
.journal-content a.lien-jour:hover { background: #0056b3; }
.journal-content a.lien-jour.jour-actif { background: #2ECC71; box-shadow: 0 0 0 3px rgba(46,204,113,0.3); }
.journal-content a.lien-media {
color: #007AFF; text-decoration: underline;
text-decoration-style: dotted; text-underline-offset: 2px;
}
+183
View File
@@ -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); });
+95
View File
@@ -0,0 +1,95 @@
{#- =============================================================================
Morceaux de HTML insérés par genere_carte.py dans les pages folium.
Échappement automatique : seules les variables marquées |safe passent telles
quelles. Les chemins arrivent déjà encodés pour les URL (quote()).
Les styles sont dans carte.css, le comportement dans carte.js.
============================================================================= -#}
{# Feuille de style et script communs, avec un numéro de version pour que les
navigateurs ne gardent pas une ancienne copie en cache. #}
{% macro entete(prefixe, version) -%}
<link rel="stylesheet" href="{{ prefixe }}assets/carte.css?v={{ version }}">
<script src="{{ prefixe }}assets/carte.js?v={{ version }}"></script>
{%- endmacro %}
{% macro epingle(couleur, icone) -%}
<div class="pin" style="--c:{{ couleur }}"><div class="pin-tete"><i class="fa fa-{{ icone }}"></i></div><div class="pin-tige"></div></div>
{%- endmacro %}
{# Popup affichée au clic sur la trace d'une journée (carte générale). #}
{% macro popup_trace(jour, couleur, lien) -%}
<div class="popup-trace"><b>📅 {{ jour }}</b><br><a class="btn-jour" style="--c:{{ couleur }}" href="{{ lien }}" target="_top">Détails</a></div>
{%- endmacro %}
{# photos : liste de {nom, src (version web), href (original)} #}
{% macro popup_photos(photos) -%}
<div class="slider-container">
<div class="slides">
{%- for p in photos %}
<div class="slide"{% if not loop.first %} style="display:none"{% endif %}>
<div class="file-header">{{ p.nom }}</div>
<a href="{{ p.href }}" target="_blank"><img src="{{ p.src }}" alt="{{ p.nom }}" loading="lazy"></a>
</div>
{%- endfor %}
</div>
{%- if photos|length > 1 %}
<button class="nav-btn prev" onclick="moveSlide(this, -1)">&#10094;</button>
<button class="nav-btn next" onclick="moveSlide(this, 1)">&#10095;</button>
<div class="slide-counter">1/{{ photos|length }}</div>
{%- endif %}
</div>
{%- endmacro %}
{# 720p H.264 chargé par défaut (lisible partout), 1080p HEVC à la demande.
preload="none" : rien n'est téléchargé avant l'ouverture de la popup. #}
{% macro popup_video(nom, mobile, hd, apercu) -%}
<div class="popup-video">
<div class="header-row">
<span class="file-name" title="{{ nom }}">{{ nom }}</span>
<div class="quality-controls">
<span class="quality-btn active-720" data-src="{{ mobile }}" onclick="changeQuality(this)">720p</span>
<span class="quality-btn" data-hevc="1" data-src="{{ hd }}" onclick="changeQuality(this)">1080p</span>
</div>
</div>
<video controls playsinline webkit-playsinline preload="none" src="{{ mobile }}" data-fallback="{{ mobile }}"
{%- if apercu %} data-poster="{{ apercu }}"{% endif %}></video>
<div class="hevc-note">Le 1080p est encodé en H.265/HEVC, que ce navigateur ne sait pas décoder.
La lecture en 720p fonctionne normalement ; pour la haute définition, utilise Safari, Chrome ou Edge.</div>
</div>
{%- endmacro %}
{# contenu : HTML du récit (voyage.md converti), déjà prêt. #}
{% macro journal(contenu) -%}
<div id="journal-panel">
<button class="close-btn" onclick="toggleJournal(false)">&times;</button>
<div class="journal-content">{{ contenu|safe }}</div>
</div>
<button class="btn-flottant btn-journal" onclick="toggleJournal(true)">📖 Journal</button>
{%- endmacro %}
{% macro bouton_accueil(lien) -%}
<a class="btn-flottant btn-accueil" href="{{ lien }}" target="_top">🏠 Accueil</a>
{%- endmacro %}
{% macro bouton_gpx(lien, libelle) -%}
<a class="btn-flottant btn-gpx" href="{{ lien }}" download>{{ libelle }}</a>
{%- endmacro %}
{# jours : liste de {libelle, lien, couleur} #}
{% macro barre_jours(retour, jours) -%}
<nav class="sidebar">
<a class="btn-retour" href="{{ retour }}" target="_top">⬅ Retour</a>
<div class="sidebar-titre">PAR JOURS</div>
{%- for j in jours %}
<a class="jour" style="--c:{{ j.couleur }}" href="{{ j.lien }}" target="_top">{{ j.libelle }}</a>
{%- endfor %}
</nav>
{%- endmacro %}
+41
View File
@@ -0,0 +1,41 @@
{#- Morceaux de HTML de la carte globale (genere_carte_globale.py).
Les liens visent target="_top" : la carte est affichée dans un cadre
de la page d'accueil, c'est toute la page qui doit changer. -#}
{% macro popup_voyage(titre, periode, km, lien) -%}
<div style="font-family:sans-serif; text-align:center; min-width:160px">
<b style="font-size:14px; text-transform:capitalize">{{ titre }}</b><br>
<span style="color:#777; font-size:12px">{{ periode }}{% if km %} · {{ "%.0f"|format(km) }} km{% endif %}</span><br>
<a href="{{ lien }}" target="_top" style="display:inline-block; margin-top:10px; padding:7px 14px; border-radius:16px; background:#3498db; color:#fff; text-decoration:none; font-weight:bold">Voir le voyage</a>
</div>
{%- endmacro %}
{% macro popup_mymaps(nom, description) -%}
<div style="font-family:sans-serif; max-height:240px; overflow:auto">
<b style="font-size:14px">{{ nom }}</b>
{%- if description %}<div style="margin-top:6px; font-size:12px; color:#444">{{ description|liens_et_lignes }}</div>{% endif %}
</div>
{%- endmacro %}
{# Visible seulement quand la carte est ouverte seule (voir script_page). #}
{% macro bouton_accueil() -%}
<a id="retour-accueil" href="index.html" style="display:none; position:fixed; bottom:25px; left:15px; z-index:1000; padding:10px 16px; border-radius:10px; background:#333; color:#fff; font:bold 14px sans-serif; text-decoration:none; box-shadow:0 4px 10px rgba(0,0,0,.4)">🏠 Accueil</a>
{%- endmacro %}
{# Exécuté au chargement complet : folium crée la carte plus bas dans la page. #}
{% macro script_page(carte) -%}
window.addEventListener('load', function () {
if (window.self !== window.top) return;
{{ carte }}.scrollWheelZoom.enable();
document.getElementById('retour-accueil').style.display = 'block';
});
{%- endmacro %}
{# Libellé d'un voyage dans la légende (sélecteur de calques). #}
{% macro legende(titre, couleur) -%}
<span style="display:inline-block; width:18px; height:4px; margin:0 6px 3px 0; border-radius:2px; vertical-align:middle; background:{{ couleur }}"></span><span style="text-transform:capitalize">{{ titre }}</span>
{%- endmacro %}
+16
View File
@@ -0,0 +1,16 @@
{#- Liste complète d'une rubrique (voyages.html, balades.html…). -#}
{% extends "base_accueil.html" %}
{% from "base_accueil.html" import carte %}
{% block titre %}{{ r.titre }} — {{ titre_site }}{% endblock %}
{% block entete %}{{ r.titre }}{% endblock %}
{% block sous_titre %}{{ r.voyages|length }} {{ r.unite }}{% endblock %}
{% block contenu %}
<a class="retour" href="index.html">← {{ titre_site }}</a>
<div class="grid">
{% for v in r.voyages %}
{{ carte(v) }}
{% endfor %}
</div>
{% endblock %}