Files
vitrine-pixels/8x32 PI/web/page.html
T
bachandClaude Opus 5.5 156f9ce438 8x32 PI : séquences jusqu'à 120 PNG, test du fil de données, montage A validé
- serveur_web.py : séquences limitées à 120 PNG (message clair au-delà),
  envois jusqu'à 200 Mo, fichiers temporaires sur la carte SD (et plus dans
  /run, trop petit), garde-fou si une séquence est envoyée dans le cadre
  « fichiers séparés ».
- outils/test_fil_donnees.py : met GPIO 18 à 1 puis à 0 toutes les 10 s,
  pour suivre le signal au multimètre du Pi jusqu'à DIN.
- README : montage A (Pi en direct, LED en 4,4 V) testé et validé ; montage B
  (transistor 2N3904) déconseillé, trop lent (temps de stockage ≈ 0,2 µs
  face à un « 0 » de 0,4 µs) ; mise en garde sur les convertisseurs HV/LV ;
  dépannage « seule la 1re LED s'allume ».
- INTERFACE_WEB.md, CLAUDE.md : nouvelles limites, accès au Pi en Wi-Fi seul,
  lecteur activé au démarrage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:15:01 +02:00

210 lines
9.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
{#
La page web de la vitrine. C'est un « gabarit » Jinja : Python (serveur_web.py)
remplace les doubles accolades par des valeurs, et répète les blocs « for ».
(Ce commentaire-ci, entre accolades-dièses, n'est pas envoyé au navigateur.)
#}
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vitrine pixels</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="{{ 'debout' if hauteur > largeur else 'couchee' }}">
<header>
<h1><span class="logo" aria-hidden="true"></span>Vitrine pixels</h1>
<div class="statut">
<span class="pastille" aria-hidden="true"></span>
En ce moment : <strong id="en-cours">{{ en_cours or "—" }}</strong>
<form method="post" action="{{ url_for('action') }}">
<input type="hidden" name="action" value="suivante">
<button type="submit" class="bouton">Suivante ⏭</button>
</form>
</div>
</header>
<main>
{# Les messages « flash » : résultat de la dernière action #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="messages" id="messages" role="status">
{% for categorie, message in messages %}
<p class="message {{ categorie }}">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- =================== AJOUTER =================== -->
<section id="ajouter">
<h2>Ajouter des animations</h2>
<div class="deux-colonnes">
<form class="carte" method="post" action="{{ url_for('ajouter') }}" enctype="multipart/form-data">
<h3>Un ou plusieurs fichiers</h3>
<p class="aide">GIF animé, PNG animé, WebP ou image fixe, exportés de Krita en
<strong>{{ largeur }} × {{ hauteur }} pixels</strong>. Chaque fichier devient une animation.</p>
<label class="depot">
<input type="file" name="fichiers" multiple accept="{{ extensions }},image/*">
<span>Choisir des fichiers…</span>
</label>
<button type="submit" class="bouton principal">Envoyer</button>
</form>
<form class="carte" method="post" action="{{ url_for('ajouter_sequence') }}" enctype="multipart/form-data">
<h3>Une séquence de PNG</h3>
<p class="aide">Toutes les images d'un export Krita « séquence d'images »
(<code>feu_0000.png</code>, <code>feu_0001.png</code>…), pour une seule animation :
<strong>{{ max_images }} images au maximum</strong>.</p>
<label class="depot">
<input type="file" name="images" multiple accept=".png,image/png">
<span>Choisir les PNG…</span>
</label>
<div class="champs">
<label>Nom <input type="text" name="nom" placeholder="feu_de_camp" required></label>
<label>Images/s <input type="number" name="ips" value="12" min="0.5" max="60" step="0.5"></label>
</div>
<button type="submit" class="bouton principal">Envoyer</button>
</form>
</div>
<p class="aide">Maximum {{ taille_max }} Mo par envoi. Les nouvelles animations
se placent à la fin de la liste.</p>
</section>
<!-- =================== JOUÉES =================== -->
<section id="jouees">
<h2>Jouées, dans cet ordre <span class="compte">{{ actives|length }}</span></h2>
{% if not actives %}
<p class="vide">Aucune animation active : la matrice affiche un petit point d'attente.</p>
{% endif %}
<ol class="liste">
{% for a in actives %}
<li class="fiche" id="a-{{ loop.index }}" data-relatif="{{ a.relatif }}">
<span class="numero">{{ loop.index }}</span>
<img class="apercu" src="{{ url_for('apercu', relatif=a.relatif) }}?v={{ a.infos.version }}"
alt="Aperçu de {{ a.titre }}" loading="lazy">
<div class="details">
<strong>{{ a.titre }}</strong>
{% include "infos.html" %}
</div>
<div class="boutons">
{% set ancre = "#a-" ~ loop.index %}
{% for quoi, signe, titre, actif in [
("monter", "▲", "Monter", not loop.first),
("descendre", "▼", "Descendre", not loop.last),
("jouer", "▶", "Jouer maintenant", true),
("desactiver", "⏸", "Ne plus jouer", true),
("supprimer", "✕", "Supprimer", true)] %}
<form method="post" action="{{ url_for('action') }}"
{% if quoi == "supprimer" %}data-confirmer="Mettre « {{ a.titre }} » à la corbeille ?"{% endif %}>
<input type="hidden" name="action" value="{{ quoi }}">
<input type="hidden" name="nom" value="{{ a.nom }}">
<input type="hidden" name="relatif" value="{{ a.relatif }}">
<input type="hidden" name="ancre" value="{{ ancre }}">
<button type="submit" class="bouton icone {{ quoi }}" title="{{ titre }}"
aria-label="{{ titre }}" {% if not actif %}disabled{% endif %}>{{ signe }}</button>
</form>
{% endfor %}
</div>
</li>
{% endfor %}
</ol>
</section>
<!-- =================== DÉSACTIVÉES =================== -->
<section id="desactivees">
<h2>Pas jouées <span class="compte">{{ desactivees|length }}</span></h2>
{% if not desactivees %}
<p class="vide">Rien ici. Le bouton ⏸ d'une animation la range ici sans la supprimer.</p>
{% endif %}
<ul class="liste">
{% for a in desactivees %}
<li class="fiche eteinte" data-relatif="{{ a.relatif }}">
<span class="numero">–</span>
<img class="apercu" src="{{ url_for('apercu', relatif=a.relatif) }}?v={{ a.infos.version }}"
alt="Aperçu de {{ a.titre }}" loading="lazy">
<div class="details">
<strong>{{ a.titre }}</strong>
{% include "infos.html" %}
</div>
<div class="boutons">
{% for quoi, signe, titre in [("activer", "+", "Remettre dans la liste"),
("jouer", "▶", "Jouer maintenant (une fois)"),
("supprimer", "✕", "Supprimer")] %}
<form method="post" action="{{ url_for('action') }}"
{% if quoi == "supprimer" %}data-confirmer="Mettre « {{ a.titre }} » à la corbeille ?"{% endif %}>
<input type="hidden" name="action" value="{{ quoi }}">
<input type="hidden" name="nom" value="{{ a.nom }}">
<input type="hidden" name="relatif" value="{{ a.relatif }}">
<input type="hidden" name="ancre" value="#desactivees">
<button type="submit" class="bouton icone {{ quoi }}" title="{{ titre }}"
aria-label="{{ titre }}">{{ signe }}</button>
</form>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</section>
</main>
<footer>
<form method="post" action="{{ url_for('action') }}"
data-confirmer="Effacer DÉFINITIVEMENT les {{ nb_corbeille }} élément(s) de la corbeille ?">
Corbeille : {{ nb_corbeille }} élément(s)
<input type="hidden" name="action" value="vider_corbeille">
<button type="submit" class="bouton" {% if not nb_corbeille %}disabled{% endif %}>Vider</button>
</form>
<p>Adresses de cette page :
{% for a in adresses %}<code>http://{{ a }}{{ port }}</code>{% if not loop.last %} · {% endif %}{% endfor %}
<br>(celle qui commence comme l'adresse de votre appareil marche depuis votre réseau)</p>
<p>Dossier : <code>{{ dossier }}</code></p>
</footer>
<script>
// Demander une confirmation avant les actions irréversibles.
document.querySelectorAll("form[data-confirmer]").forEach(function (form) {
form.addEventListener("submit", function (e) {
if (!confirm(form.dataset.confirmer)) e.preventDefault();
});
});
// Afficher le nom des fichiers choisis dans les zones de dépôt.
document.querySelectorAll(".depot input").forEach(function (champ) {
champ.addEventListener("change", function () {
var n = champ.files.length;
champ.nextElementSibling.textContent =
n === 0 ? "Choisir…" : n === 1 ? champ.files[0].name : n + " fichiers choisis";
});
});
// Les messages disparaissent tout seuls après 6 secondes (sauf les erreurs).
var messages = document.getElementById("messages");
if (messages && !messages.querySelector(".erreur")) {
setTimeout(function () { messages.remove(); }, 6000);
}
// Toutes les 2 secondes, demander au serveur ce que joue la matrice,
// et mettre en valeur la fiche correspondante.
function rafraichir() {
fetch("{{ url_for('etat') }}")
.then(function (r) { return r.json(); })
.then(function (etat) {
document.getElementById("en-cours").textContent = etat.en_cours || "—";
document.querySelectorAll(".fiche").forEach(function (f) {
f.classList.toggle("joue", f.dataset.relatif === etat.en_cours);
});
})
.catch(function () {});
}
rafraichir();
setInterval(rafraichir, 2000);
</script>
</body>
</html>