- 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>
222 lines
9.5 KiB
HTML
222 lines
9.5 KiB
HTML
<!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>
|