Files
vitrine-pixels/8x32 PI/web/page.html
T
bachandClaude Opus 5.5 2d4b0d5a5f 8x32 PI : interface web, matrice verticale, compatibilité Raspbian 11
- Interface web (serveur_web.py, Flask, gabarits dans web/) : dépôt de GIF,
  PNG animés et séquences de PNG, aperçus animés, ordre de lecture,
  activer / désactiver, corbeille, « jouer maintenant » et « suivante ».
  Service systemd vitrine-web, activé par installer.sh.
- Lecteur : réagit aux demandes de la page web (post-it dans
  /run/vitrine-pixels), écrit l'animation en cours, éteint la matrice
  proprement sur SIGTERM.
- Matrice posée debout : réglage ROTATION (90 = image 8 × 32) dans
  matrice.py ; images couchées tournées automatiquement ; exemples,
  animations codées, aperçus et page web adaptés au format vertical.
- Compatibilité Raspbian 11 Bullseye : partition /boot, Pillow 8.1, Flask 1.1.
- Doc : nouveau guide INTERFACE_WEB.md ; README, GUIDE_KRITA, MODE_VITRINE,
  CODER_EN_PYTHON mis à jour (format 8 × 32, adresses Wi-Fi / câble du Pi).

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

209 lines
8.9 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.</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>