Traiter le lot A d'IDEES.md : CSS onglets, pastilles, identité visuelle, couverture cassée, cache couvertures

- A1 : forcer la revalidation HTTP (Cache-Control: no-cache) sur les fichiers statiques,
  pour empêcher un navigateur de garder indéfiniment un CSS/JS périmé quand le service
  worker ne s'active pas.
- A2 : le filtre du catalogue (statut, type de document) reprend les mêmes pastilles
  colorées que les cartes (Libelles.ClasseStatut / nouvelle Libelles.ClasseTypeDocument).
- A3 : remplacement du logo par défaut de Blazor par un pictogramme de livre ouvert
  (favicon, icônes PWA, logo du bandeau, logo mabibli_ynh).
- A4 : une couverture cassée désactive aussi son bouton d'agrandissement.
- A5 : cache IndexedDB des couvertures pour la consultation hors-ligne, sans ralentir
  l'affichage en ligne (mise en cache en tâche de fond). L'allègement (redimensionnement,
  WebP) reste hors scope, à mesurer avant de s'y engager.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
LIMONIER Mathieu
2026-08-20 13:03:14 +02:00
co-authored by Copilot
parent ce58294fd1
commit 5d45eb492e
16 changed files with 318 additions and 74 deletions
+21
View File
@@ -250,6 +250,11 @@ body {
.segment.statut-en-cours.segment-actif { background: #b06b00; border-color: #b06b00; }
.segment.statut-lu.segment-actif { background: #1f7a44; border-color: #1f7a44; }
/* Type de document : même vocabulaire que les pastilles de statut, deux couleurs distinctes
des trois du statut et de celle du prêt, pour qu'on reconnaisse ce qu'on filtre (A2, IDEES.md). */
.segment.type-roman.segment-actif { background: #2a6f97; border-color: #2a6f97; }
.segment.type-bd.segment-actif { background: #a13d63; border-color: #a13d63; }
/* --- Boutons --- */
.bouton {
@@ -580,6 +585,12 @@ body {
cursor: zoom-in;
}
/* Image cassée (404/502 intermittent) : le onerror du <img> désactive ce bouton.
Curseur neutre, pour ne plus promettre un agrandissement qui n'ouvrirait rien. */
.couverture-declencheur:disabled {
cursor: default;
}
.calque-couverture {
position: fixed;
inset: 0;
@@ -638,6 +649,16 @@ body {
.etiquette-statut.statut-en-cours { background: #b06b00; }
.etiquette-statut.statut-lu { background: #1f7a44; }
/* Pastille du type de document sur la carte du catalogue : mêmes couleurs que le
filtre (A2, IDEES.md), pour reconnaître d'un coup d'œil ce qui a été filtré. */
.etiquette-type {
color: #fff;
font-weight: 600;
}
.etiquette-type.type-roman { background: #2a6f97; }
.etiquette-type.type-bd { background: #a13d63; }
/* --- Fiche du livre en lecture seule --- */
.couverture-grande { width: 8rem; height: 12rem; }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+48 -1
View File
@@ -13,7 +13,11 @@
const NOM_BASE = 'mabibli';
const MAGASIN = 'instantanes';
const VERSION = 1;
// Couvertures mises en cache (A5, IDEES.md) : magasin séparé, clé = l'URL OpenLibrary,
// valeur = le Blob de l'image. Sert uniquement à la consultation hors-ligne — en ligne,
// l'<img> normale reste la voie rapide, ce cache se remplit en tâche de fond à côté.
const MAGASIN_COUVERTURES = 'couvertures';
const VERSION = 2;
let promesseBase = null;
@@ -29,6 +33,7 @@ function ouvrir() {
requete.onupgradeneeded = () => {
const base = requete.result;
if (!base.objectStoreNames.contains(MAGASIN)) base.createObjectStore(MAGASIN);
if (!base.objectStoreNames.contains(MAGASIN_COUVERTURES)) base.createObjectStore(MAGASIN_COUVERTURES);
};
requete.onsuccess = () => resoudre(requete.result);
requete.onerror = () => rejeter(requete.error);
@@ -84,3 +89,45 @@ export function surveiller(reference) {
self.addEventListener('offline', prevenir);
return navigator.onLine !== false;
}
// --- Couvertures (A5, IDEES.md) ---
//
// En ligne, l'<img> normale reste la voie d'affichage : rien ici ne doit la ralentir. Cette
// fonction tourne en tâche de fond, appelée sans attendre son résultat (fire-and-forget) après
// qu'une couverture s'est affichée avec succès. Le second fetch profite en général du cache HTTP
// du navigateur (même URL que l'<img>) : pas de second téléchargement réel dans le cas courant.
//
// OpenLibrary envoie `Access-Control-Allow-Origin: *` (vérifié) : un fetch cross-origin normal
// suffit, pas besoin du contournement `no-cors`/réponse opaque.
export async function couvertureMettreEnCache(url) {
try {
const base = await ouvrir();
const tx = base.transaction(MAGASIN_COUVERTURES, 'readonly');
const existe = await attendre(tx.objectStore(MAGASIN_COUVERTURES).getKey(url), tx);
if (existe !== undefined) return; // Déjà en cache : pas de re-téléchargement.
const reponse = await fetch(url, { cache: 'force-cache' });
if (!reponse.ok) return; // 404/502 intermittent (CLAUDE.md) : rien à ranger.
const blob = await reponse.blob();
const ecriture = base.transaction(MAGASIN_COUVERTURES, 'readwrite');
await attendre(ecriture.objectStore(MAGASIN_COUVERTURES).put(blob, url), ecriture);
} catch (e) {
// Quota dépassé, stockage refusé : une couverture non mise en cache reste un
// agrément perdu, jamais une raison de faire échouer l'affichage en ligne.
}
}
// Renvoie une URL d'objet (`blob:`) utilisable comme `src`, ou `null` si cette couverture n'a
// jamais été mise en cache — c'est alors à l'appelant de retomber sur l'URL réseau (qui échouera
// simplement si hors-ligne, exactement comme aujourd'hui).
export async function couvertureLire(url) {
try {
const base = await ouvrir();
const tx = base.transaction(MAGASIN_COUVERTURES, 'readonly');
const blob = await attendre(tx.objectStore(MAGASIN_COUVERTURES).get(url), tx);
return blob ? URL.createObjectURL(blob) : null;
} catch (e) {
return null;
}
}
+13
View File
@@ -0,0 +1,13 @@
<!--
Petit logo du bandeau : même livre ouvert que les icônes PWA (favicon.png, icon-*.png),
mais en blanc plutôt qu'en bleu — le bandeau est déjà bleu foncé (--mb-accent), un logo
de la même couleur s'y fondrait.
SVG plutôt qu'un PNG : affiché à une seule petite taille dans le bandeau, mais net sur
tous les écrans (y compris les écrans très denses), sans multiplier les fichiers raster.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24" height="24" aria-hidden="true">
<polygon points="32,15 10,20 10,47 32,44" fill="#fff" />
<polygon points="32,15 54,20 54,47 32,44" fill="#fff" />
<line x1="32" y1="15" x2="32" y2="44" stroke="#1b3a5c" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 727 B