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
+82
View File
@@ -1684,6 +1684,88 @@ extérieur ou par Échap. Le calque est focalisé à l'ouverture (`FocusAsync`)
**Le substitut à initiale n'est jamais cliquable** : le bouton déclencheur n'est rendu que
lorsqu'une URL de couverture existe. Sans couverture, il n'y a rien à agrandir.
## Habillage — retours d'usage du 2026-08-19 traités (2026-08-20)
Lot A d'IDEES.md (4ᵉ série). Quatre items traités ; le cinquième (allègement des couvertures,
WebP) reste volontairement hors scope, voir plus bas.
### A1 : ce n'était pas un bug de style, mais l'absence de revalidation HTTP
Les six onglets s'affichaient parfois en liens bruts, sans aucune règle de
`MainLayout.razor.css`. Cause retenue : `UseStaticFiles()` ne posait **aucun** en-tête
`Cache-Control` — un navigateur peut donc garder `MaBibli.Client.styles.css` en cache
heuristique, **indépendamment du service worker**. Le service worker revalide bien via
`cache: 'no-cache'`, mais seulement quand il parvient à s'activer, ce qui n'est pas garanti
(voir « Le service worker ne s'enregistre pas… »).
**Correction** : `Program.cs` pose désormais `Cache-Control: no-cache, must-revalidate` sur tous
les fichiers statiques (`StaticFileOptions.OnPrepareResponse`). Les empreintes WASM étant
désactivées (noms de fichiers stables, voir plus haut), c'est cette revalidation forcée qui
empêche un navigateur de servir indéfiniment une version périmée — le service worker reste une
seconde ligne de défense, pas la seule.
⚠️ `no-cache` ne veut pas dire « ne jamais mettre en cache » : ETag et Last-Modified, déjà posés
par défaut par `StaticFileMiddleware`, permettent une revalidation conditionnelle (304) sans
retélécharger un fichier inchangé.
### A2 : le filtre reprend les pastilles de couleur des cartes
Le filtre du catalogue (statut de lecture, type de document) affichait des segments neutres,
alors que `ChoixStatut` (statut personnel sur la fiche) colorait déjà ses segments avec
`Libelles.ClasseStatut`. Les segments et l'étiquette de la carte reprennent maintenant la même
classe — le filtre statut réutilise `ClasseStatut`, et une nouvelle méthode
`Libelles.ClasseTypeDocument` (bleu pour Roman, magenta pour BD, aucune couleur pour
« Non précisé », qui n'est pas une valeur choisie) est appliquée aux deux endroits : le segment
du filtre et l'étiquette `etiquette-type` de la carte. Le formulaire de saisie reprend la même
classe, pour ne pas diverger une troisième fois.
### A3 : une identité visuelle, encore le logo par défaut de Blazor
`favicon.png` / `icon-192.png` / `icon-512.png` étaient encore le logo « @ » violet du template
`dotnet new blazorwasm`. Remplacés par un pictogramme de livre ouvert (deux pages en V, tranche
centrale blanche), dans le bleu d'accent existant (`#1b3a5c`) — généré par script (Pillow), pas
par un outil de génération d'images IA, pour rester un simple dessin géométrique. Un petit
`logo-bandeau.svg` (blanc, net à toute résolution) accompagne « MaBibli » dans le bandeau.
`mabibli_ynh/logo.png` (256×256, fond transparent — convention du catalogue d'applications
YunoHost) reprend le même dessin.
### A4 : une couverture cassée ne doit plus rien pouvoir agrandir
Le `onerror` masquait déjà l'image (`image-cassee`), mais le bouton `.couverture-declencheur`
qui l'enveloppe restait cliquable : cliquer une vignette cassée ouvrait quand même le calque
d'agrandissement, avec la même image cassée dedans. Le `onerror` désactive maintenant aussi ce
bouton (`this.closest('.couverture-declencheur').disabled = true`), et `:disabled` porte un
curseur neutre plutôt que `zoom-in`.
### A5 : les couvertures se mettent en cache pour la consultation hors-ligne
Nouveau magasin IndexedDB `couvertures` (clé = URL OpenLibrary, valeur = `Blob`), à côté du
magasin `instantanes` existant — `cache-hors-ligne.js` passe en version 2.
⚠️ **En ligne, l'affichage ne change pas** : `<img src>` pointe directement sur l'URL réseau,
aussi vite qu'avant. La mise en cache (`Cache.MettreEnCacheCouvertureAsync`) est un
fire-and-forget déclenché en parallèle, jamais attendu — elle ne doit rien à la rapidité
d'affichage. Le fetch qu'elle déclenche profite en général du cache HTTP du navigateur (même URL
que l'`<img>` déjà chargée), donc pas de second téléchargement réel dans le cas courant.
**Hors ligne**, `Couverture.razor` tente d'abord `Cache.LireCouvertureCacheeAsync` (URL `blob:`
via `URL.createObjectURL`) et ne retombe sur l'URL réseau — qui échouera simplement, comme avant
cette fonctionnalité — que si rien n'a été mis en cache.
⚠️ **Le jeton de résolution compare `(Url, EnLigne)`, pas seulement `Url`** : passer en ligne →
hors ligne sans que l'URL change (le cas le plus courant, puisqu'on ne change pas de livre en
perdant le réseau) est précisément le cas qui aurait été raté avec une mémoïsation sur la seule
URL — la couverture serait restée invisible hors-ligne malgré un cache rempli.
**Vérifié** : OpenLibrary envoie `Access-Control-Allow-Origin: *` (mesuré via `curl -I`), donc un
`fetch()` cross-origin ordinaire suffit pour lire les octets — pas besoin d'un contournement en
réponse opaque (`mode: 'no-cors'`).
**Ce qui n'est délibérément pas fait** : l'allègement (redimensionner à la taille réellement
affichée, convertir en WebP). IDEES.md posait la question à mesurer avant de s'y engager, même
règle que pour l'AOT WASM — non fait tant que la taille du cache image n'a pas montré de
problème réel en usage.
## Installation YunoHost — éprouvée en production le 2026-08-18
Le paquet vit dans le dépôt `mabibli_ynh` (voir « Deux dépôts distincts »). Installé,
+8 -60
View File
@@ -116,69 +116,17 @@ sans rapport.
## Lot A — L'habillage : la navigation ne ressemble à rien
Le lot qui se voit à chaque écran, et le seul dont le défaut ressemble à une régression.
**A1 à A4 traités le 2026-08-20** (voir `CLAUDE.md`, section « Habillage — retours d'usage du
2026-08-19 traités »). Reste ouvert :
### A1. Les six onglets s'affichent en liens bruts (anomalie, pas une demande)
### A5. Alléger les couvertures (redimensionnement / WebP)
Constaté sur la capture du 2026-08-19 : `CatalogueAuteursSériesRevuesPrêtsEnvies` s'affiche en
bas de page, en bleu souligné, collés les uns aux autres — c'est-à-dire **sans aucune des règles
de `MainLayout.razor.css`** : ni `position: fixed`, ni `display: flex`, ni les libellés espacés.
Le cache hors-ligne des couvertures est fait (voir `CLAUDE.md`), mais chaque affichage en ligne
retélécharge toujours l'image OpenLibrary à sa taille d'origine — rien n'est redimensionné à la
taille réellement affichée, ni converti en WebP.
⚠️ **Ce n'est probablement pas un défaut de mise en forme à refaire, mais une feuille de style
périmée.** Sur la même capture, le bandeau supérieur **est** bleu foncé — or `.bandeau` et
`.onglets` vivent dans le **même fichier** scopé. Une moitié appliquée et l'autre pas ne
s'explique pas par le CSS lui-même : elle s'explique par un `MaBibli.Client.styles.css` **d'avant
l'ajout des onglets**, servi à côté d'un WASM à jour (le balisage, lui, montre bien les six
onglets).
Hypothèse à vérifier **avant** de toucher au style, sous peine de « corriger » du code sain :
1. le nom `MaBibli.Client.styles.css` est **stable** (les empreintes sont désactivées, voir
`CLAUDE.md`) — rien d'autre que le service worker ne le fait expirer ;
2. or le service worker **ne s'enregistre pas** dans certains contextes, et un onglet resté
ouvert diffère l'activation d'un nouveau worker.
Contrôles qui trancheraient : ouvrir `/MaBibli.Client.styles.css` sur le serveur et y chercher
`.onglets` ; comparer avec le fichier du dernier publish ; recharger en vidant le cache.
Si la règle **est** présente et pourtant sans effet, alors seulement chercher du côté de
l'attribut de portée (`b-xxxxx`) que `NavLink` doit relayer via ses attributs additionnels.
### A2. Les filtres reprennent le vocabulaire visuel des cartes
Les états *À lire / En cours / Lu* et les types *Roman / BD* s'affichent en pastilles colorées
sur les cartes du catalogue, mais en segments neutres dans le panneau « Filtrer ». Demander la
**même pastille** dans le filtre, pour qu'on reconnaisse ce qu'on filtre.
### A3. Une identité visuelle : trois images, trois emplacements distincts
À ne pas confondre, les trois ne vivent pas au même endroit :
| Où | Quoi | Porté par |
|---|---|---|
| Catalogue d'applications YunoHost | logo de l'app | `mabibli_ynh` (`doc/`, manifeste) |
| Bandeau de l'application | petit logo à côté de « MaBibli » | `MaBibli.Client/wwwroot` |
| Installation PWA (téléphone, PC) | icône d'accueil et écran de démarrage | `manifest.webmanifest` + icônes 192/512 |
⚠️ La troisième est la seule qui a des contraintes dures (tailles, `purpose: maskable`), et
c'est aussi la seule que le service worker met en cache — donc celle dont un changement se voit
le plus tard.
### A4. Couvertures : ne pas agrandir ce qui n'existe pas
Cliquer une vignette absente ne doit rien ouvrir. `CLAUDE.md` affirme que le bouton n'est rendu
que lorsqu'une `CoverUrl` existe — **le cas remonté est donc probablement l'image *cassée*** :
URL présente, mais 502 ou 404 d'OpenLibrary (intermittence documentée). Le `onerror` remplace
l'image ; il devrait aussi **retirer le déclencheur d'agrandissement**.
### A5. Garder les couvertures en cache, et les alléger
Aujourd'hui chaque affichage retélécharge l'image chez OpenLibrary — donc rien hors-ligne, et
des vignettes manquantes au gré des 502. Piste : stocker l'image (IndexedDB, à côté des
instantanés) et la réduire à la taille réellement affichée.
⚠️ À peser : c'est le premier contenu **binaire et volumineux** du cache hors-ligne, dont
`CLAUDE.md` note qu'il pèse aujourd'hui ~2,4 Ko pour 10 livres. Une conversion (WebP) n'a de sens
que si la mesure montre un gain — même règle que pour l'AOT.
⚠️ À peser avant de s'y engager, même règle que pour l'AOT : une conversion n'a de sens que si la
mesure montre un gain réel. Pas encore mesuré à l'échelle d'un vrai fonds.
---
+14 -1
View File
@@ -73,8 +73,21 @@ if (app.Environment.IsDevelopment())
// Le client Blazor WebAssembly est servi comme fichiers statiques par cette API :
// un seul processus tourne sur le serveur.
//
// ⚠️ Les empreintes WASM sont désactivées (voir CLAUDE.md) : tous ces fichiers portent des noms
// stables. Sans en-tête Cache-Control, un navigateur peut garder une version périmée en cache
// heuristique (par exemple MaBibli.Client.styles.css) même si le service worker, lui, revalide
// déjà à l'installation — mais seulement quand il parvient à s'activer. `no-cache` force une
// revalidation conditionnelle (ETag/Last-Modified, déjà posés par défaut) à chaque requête :
// pas de re-téléchargement si le fichier n'a pas changé, mais jamais de contenu périmé servi
// sans repasser par le serveur.
var optionsFichiersStatiques = new StaticFileOptions
{
OnPrepareResponse = ctx =>
ctx.Context.Response.Headers.CacheControl = "no-cache, must-revalidate",
};
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseStaticFiles(optionsFichiersStatiques);
app.MapIsbnEndpoints();
app.MapLivresEndpoints();
+59 -7
View File
@@ -1,3 +1,6 @@
@using MaBibli.Client.Services
@inject CacheHorsLigne Cache
@inject EtatReseau Reseau
@*
Couverture d'un livre, avec substitut.
@@ -10,7 +13,19 @@
le onerror a marqué comme cassé.
Agrandissement (IDEES.md) : seulement quand il y a une URL. Sans couverture, il n'y a rien
à agrandir le substitut à initiale reste un simple décor, jamais un bouton.
à agrandir - le substitut à initiale reste un simple décor, jamais un bouton.
⚠️ Image cassée après coup (404/502 intermittent d'OpenLibrary) : le onerror désactive aussi
le bouton déclencheur (this.closest('.couverture-declencheur').disabled = true), pas
seulement l'image. Sans ça, cliquer une vignette cassée agrandissait quand même une image
cassée (A4, IDEES.md).
Cache hors-ligne (A5, IDEES.md) : EN LIGNE, l'affichage utilise l'URL réseau telle quelle —
rien ne doit ralentir la voie rapide — et une mise en cache démarre en tâche de fond,
sans attendre son résultat, pour que la même couverture reste visible hors-ligne. HORS
LIGNE, on tente d'abord le cache : l'URL réseau échouerait de toute façon.
⚠️ L'allègement (redimensionner, convertir en WebP) n'est PAS fait ici : à mesurer avant
de s'y engager, comme pour l'AOT — voir IDEES.md.
*@
<div class="couverture @Classe">
<span class="couverture-substitut" aria-hidden="true">@Initiale</span>
@@ -24,19 +39,19 @@
<button type="button" class="couverture-declencheur"
aria-label="Agrandir la couverture de @Titre"
@onclick="Ouvrir">
<img @key="Url"
<img @key="UrlAffichee"
class="couverture-image"
src="@Url"
src="@UrlAffichee"
alt="Couverture de @Titre"
loading="lazy"
onerror="this.classList.add('image-cassee')" />
onerror="this.classList.add('image-cassee'); const b=this.closest('.couverture-declencheur'); if(b){b.disabled=true;}" />
</button>
}
else
{
<img @key="Url"
<img @key="UrlAffichee"
class="couverture-image"
src="@Url"
src="@UrlAffichee"
alt="Couverture de @Titre"
loading="lazy"
onerror="this.classList.add('image-cassee')" />
@@ -52,7 +67,7 @@
aria-label="Couverture de @Titre"
tabindex="-1" @ref="_calque"
@onclick="Fermer" @onkeydown="SurToucheAsync">
<img class="calque-image" src="@Url" alt="Couverture de @Titre"
<img class="calque-image" src="@UrlAffichee" alt="Couverture de @Titre"
@onclick:stopPropagation="true"
onerror="this.classList.add('image-cassee')" />
@@ -73,9 +88,18 @@
private bool _agrandie;
private bool _aFocaliser;
private ElementReference _calque;
private string? _urlCachee;
private (string? Url, bool EnLigne)? _urlResoluePour;
private bool ADesPixels => !string.IsNullOrWhiteSpace(Url);
/// <summary>
/// URL réellement posée en <c>src</c> : l'URL réseau en ligne (voie rapide, inchangée), le
/// blob mis en cache hors-ligne quand il existe, sinon l'URL réseau telle quelle (qui échoue
/// simplement, comme avant cette fonctionnalité).
/// </summary>
private string? UrlAffichee => !Reseau.EnLigne && _urlCachee is not null ? _urlCachee : Url;
private string Initiale =>
string.IsNullOrWhiteSpace(Titre) ? "?" : Titre.Trim()[..1].ToUpperInvariant();
@@ -95,6 +119,33 @@
}
}
protected override async Task OnParametersSetAsync()
{
// ⚠️ Le jeton compare aussi l'état réseau, pas seulement l'URL : sans quoi, passer
// en ligne → hors ligne sans que l'URL change (le cas le plus courant) ne relirait
// jamais le cache, et la couverture resterait invisible hors-ligne.
var jeton = (Url, Reseau.EnLigne);
if (!ADesPixels || _urlResoluePour == jeton)
{
return;
}
_urlResoluePour = jeton;
_urlCachee = null;
if (Reseau.EnLigne)
{
// Tâche de fond, jamais attendue : la voie d'affichage en ligne ne doit rien à
// ce cache, elle reste aussi rapide qu'avant.
_ = Cache.MettreEnCacheCouvertureAsync(Url!);
}
else
{
_urlCachee = await Cache.LireCouvertureCacheeAsync(Url!);
StateHasChanged();
}
}
protected override async Task OnAfterRenderAsync(bool premierRendu)
{
if (_aFocaliser)
@@ -104,3 +155,4 @@
}
}
}
@@ -111,7 +111,7 @@
@foreach (var type in Types)
{
<button type="button"
class="segment @(Saisie.TypeDocument == type ? "segment-actif" : null)"
class="segment @Libelles.ClasseTypeDocument(type) @(Saisie.TypeDocument == type ? "segment-actif" : null)"
@onclick="() => Saisie.TypeDocument = type">
@Libelles.TypeDocument(type)
</button>
+12
View File
@@ -54,4 +54,16 @@ public static class Libelles
MaBibli.Shared.Entites.Statut.Lu => "statut-lu",
_ => string.Empty,
};
/// <summary>
/// Classe CSS associée au type de document, pour la pastille de couleur (A2, IDEES.md) :
/// le filtre doit reprendre le même vocabulaire visuel que la carte, pas des segments neutres.
/// « Non précisé » n'a pas de couleur : ce n'est pas une valeur choisie.
/// </summary>
public static string ClasseTypeDocument(TypeDocument type) => type switch
{
MaBibli.Shared.Entites.TypeDocument.Roman => "type-roman",
MaBibli.Shared.Entites.TypeDocument.BandeDessinee => "type-bd",
_ => string.Empty,
};
}
+4 -1
View File
@@ -6,7 +6,10 @@
@* Mise en page pensée mobile d'abord : un bandeau, une colonne, rien à gauche.
Le PC hérite de la même colonne, simplement centrée et limitée en largeur. *@
<header class="bandeau">
<a class="marque" href="">MaBibli</a>
<a class="marque" href="">
<img src="logo-bandeau.svg" alt="" class="marque-logo" />
MaBibli
</a>
@if (!Reseau.EnLigne)
{
@@ -16,6 +16,15 @@
font-size: 1.15rem;
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.marque-logo {
width: 1.3rem;
height: 1.3rem;
flex: 0 0 auto;
}
.utilisateur {
+7 -3
View File
@@ -65,7 +65,8 @@
@onclick="() => FiltrerType(null)">Tous</button>
@foreach (var type in Enum.GetValues<TypeDocument>())
{
<button type="button" class="segment @(_type == type ? "segment-actif" : null)"
<button type="button"
class="segment @Libelles.ClasseTypeDocument(type) @(_type == type ? "segment-actif" : null)"
@onclick="() => FiltrerType(type)">@Libelles.TypeDocument(type)</button>
}
</div>
@@ -79,7 +80,8 @@
@onclick="() => FiltrerStatut(null)">Tous</button>
@foreach (var statut in Enum.GetValues<Statut>())
{
<button type="button" class="segment @(_statut == statut ? "segment-actif" : null)"
<button type="button"
class="segment @Libelles.ClasseStatut(statut) @(_statut == statut ? "segment-actif" : null)"
@onclick="() => FiltrerStatut(statut)">@Libelles.Statut(statut)</button>
}
</div>
@@ -150,7 +152,9 @@ else if (_livres is not null)
« Non précisé » n'apprendrait rien et serait sur presque chaque ligne. *@
@if (livre.TypeDocument != TypeDocument.NonPrecise)
{
<span class="etiquette">@Libelles.TypeDocument(livre.TypeDocument)</span>
<span class="etiquette etiquette-type @Libelles.ClasseTypeDocument(livre.TypeDocument)">
@Libelles.TypeDocument(livre.TypeDocument)
</span>
}
@* Personnel : ce que montre cette pastille est MA lecture, pas celle du foyer.
+40
View File
@@ -151,6 +151,46 @@ public sealed class CacheHorsLigne(IJSRuntime js) : IAsyncDisposable
}
}
/// <summary>
/// Range une couverture en cache pour la consultation hors-ligne, en tâche de fond.
/// </summary>
/// <remarks>
/// Fire-and-forget par nature (A5, IDEES.md) : appelé après l'affichage réussi d'une
/// couverture, sans jamais bloquer ni faire échouer ce qui s'est déjà bien passé. Une
/// couverture qui ne peut pas être mise en cache (quota, stockage refusé, 502
/// intermittent d'OpenLibrary) reste simplement absente hors-ligne — jamais une erreur
/// visible en ligne.
/// </remarks>
public async Task MettreEnCacheCouvertureAsync(string url)
{
try
{
var module = await ModuleAsync();
await module.InvokeVoidAsync("couvertureMettreEnCache", url);
}
catch (Exception)
{
// Voir la remarque ci-dessus : rien à propager.
}
}
/// <summary>
/// Relit une couverture déjà mise en cache, sous forme d'URL d'objet (<c>blob:</c>) utilisable
/// directement comme <c>src</c>. <c>null</c> si elle n'a jamais été mise en cache.
/// </summary>
public async Task<string?> LireCouvertureCacheeAsync(string url)
{
try
{
var module = await ModuleAsync();
return await module.InvokeAsync<string?>("couvertureLire", url);
}
catch (Exception)
{
return null;
}
}
public async ValueTask DisposeAsync()
{
if (_module is null)
+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