Files
bachandClaude Opus 5.5 5b0e858abe 8x32 PI : vitesse de lecture, QR code et affiche, sens de l'image sur la vitrine
- Vitesse de lecture : réglage VITESSE du lecteur et menu « Vitesse » sur la
  page web (×0,25 à ×2, gardé dans .vitesse.txt du dossier des animations).
- QR code d'accès à la page web et affiche A5 (outils/generer_qrcode.py →
  qrcode/) : une coulée inspirée de « cascade » en haut, une partie de
  « tetris » en bas, couleurs tirées des animations de l'équipe
  (qrcode/sources/), titre en pixels de couleur.
- Matrice de la vitrine : image retournée d'un demi-tour (INVERSER_X et
  INVERSER_Y) et nouveau réglage DECALAGE_X / DECALAGE_Y (ici 1 pixel vers
  la droite).
- Doc : README, INTERFACE_WEB.md et CLAUDE.md à jour.

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

222 lines
9.5 KiB
HTML
Raw Permalink 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>
{# Vitesse : le formulaire s'envoie tout seul dès qu'on change la valeur. #}
<form method="post" action="{{ url_for('action') }}" class="vitesse">
<input type="hidden" name="action" value="vitesse">
<label>Vitesse
<select name="vitesse" onchange="this.form.submit()">
{% for valeur, texte in vitesses %}
<option value="{{ valeur }}" {% if (valeur - vitesse)|abs < 0.01 %}selected{% endif %}>{{ texte }}</option>
{% endfor %}
</select>
</label>
<noscript><button type="submit" class="bouton">OK</button></noscript>
</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>