diff --git a/CLAUDE.md b/CLAUDE.md index 0523cff..52ba41b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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** : `` 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'`` 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é, diff --git a/IDEES.md b/IDEES.md index 94ec3c5..7de93fa 100644 --- a/IDEES.md +++ b/IDEES.md @@ -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. --- diff --git a/MaBibli.Api/Program.cs b/MaBibli.Api/Program.cs index 38357ea..12220c7 100644 --- a/MaBibli.Api/Program.cs +++ b/MaBibli.Api/Program.cs @@ -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(); diff --git a/MaBibli.Client/Composants/Couverture.razor b/MaBibli.Client/Composants/Couverture.razor index fff65ec..8914692 100644 --- a/MaBibli.Client/Composants/Couverture.razor +++ b/MaBibli.Client/Composants/Couverture.razor @@ -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. *@
@@ -24,19 +39,19 @@ } else { - Couverture de @Titre @@ -52,7 +67,7 @@ aria-label="Couverture de @Titre" tabindex="-1" @ref="_calque" @onclick="Fermer" @onkeydown="SurToucheAsync"> - Couverture de @Titre @@ -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); + /// + /// URL réellement posée en src : 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é). + /// + 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 @@ } } } + diff --git a/MaBibli.Client/Composants/FormulaireLivre.razor b/MaBibli.Client/Composants/FormulaireLivre.razor index fefc0e9..480ac09 100644 --- a/MaBibli.Client/Composants/FormulaireLivre.razor +++ b/MaBibli.Client/Composants/FormulaireLivre.razor @@ -111,7 +111,7 @@ @foreach (var type in Types) { diff --git a/MaBibli.Client/Composants/Libelles.cs b/MaBibli.Client/Composants/Libelles.cs index b984633..6510825 100644 --- a/MaBibli.Client/Composants/Libelles.cs +++ b/MaBibli.Client/Composants/Libelles.cs @@ -54,4 +54,16 @@ public static class Libelles MaBibli.Shared.Entites.Statut.Lu => "statut-lu", _ => string.Empty, }; + + /// + /// 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. + /// + public static string ClasseTypeDocument(TypeDocument type) => type switch + { + MaBibli.Shared.Entites.TypeDocument.Roman => "type-roman", + MaBibli.Shared.Entites.TypeDocument.BandeDessinee => "type-bd", + _ => string.Empty, + }; } diff --git a/MaBibli.Client/Layout/MainLayout.razor b/MaBibli.Client/Layout/MainLayout.razor index 7643e00..83c73cc 100644 --- a/MaBibli.Client/Layout/MainLayout.razor +++ b/MaBibli.Client/Layout/MainLayout.razor @@ -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. *@
- MaBibli + + + MaBibli + @if (!Reseau.EnLigne) { diff --git a/MaBibli.Client/Layout/MainLayout.razor.css b/MaBibli.Client/Layout/MainLayout.razor.css index 14dfa78..28f597c 100644 --- a/MaBibli.Client/Layout/MainLayout.razor.css +++ b/MaBibli.Client/Layout/MainLayout.razor.css @@ -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 { diff --git a/MaBibli.Client/Pages/Catalogue.razor b/MaBibli.Client/Pages/Catalogue.razor index 84f7d89..e57eab0 100644 --- a/MaBibli.Client/Pages/Catalogue.razor +++ b/MaBibli.Client/Pages/Catalogue.razor @@ -65,7 +65,8 @@ @onclick="() => FiltrerType(null)">Tous @foreach (var type in Enum.GetValues()) { - }
@@ -79,7 +80,8 @@ @onclick="() => FiltrerStatut(null)">Tous @foreach (var statut in Enum.GetValues()) { - } @@ -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) { - @Libelles.TypeDocument(livre.TypeDocument) + + @Libelles.TypeDocument(livre.TypeDocument) + } @* Personnel : ce que montre cette pastille est MA lecture, pas celle du foyer. diff --git a/MaBibli.Client/Services/CacheHorsLigne.cs b/MaBibli.Client/Services/CacheHorsLigne.cs index 46e0835..42de125 100644 --- a/MaBibli.Client/Services/CacheHorsLigne.cs +++ b/MaBibli.Client/Services/CacheHorsLigne.cs @@ -151,6 +151,46 @@ public sealed class CacheHorsLigne(IJSRuntime js) : IAsyncDisposable } } + /// + /// Range une couverture en cache pour la consultation hors-ligne, en tâche de fond. + /// + /// + /// 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. + /// + 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. + } + } + + /// + /// Relit une couverture déjà mise en cache, sous forme d'URL d'objet (blob:) utilisable + /// directement comme src. null si elle n'a jamais été mise en cache. + /// + public async Task LireCouvertureCacheeAsync(string url) + { + try + { + var module = await ModuleAsync(); + return await module.InvokeAsync("couvertureLire", url); + } + catch (Exception) + { + return null; + } + } + public async ValueTask DisposeAsync() { if (_module is null) diff --git a/MaBibli.Client/wwwroot/css/app.css b/MaBibli.Client/wwwroot/css/app.css index 885f64b..5548b0b 100644 --- a/MaBibli.Client/wwwroot/css/app.css +++ b/MaBibli.Client/wwwroot/css/app.css @@ -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 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; } diff --git a/MaBibli.Client/wwwroot/favicon.png b/MaBibli.Client/wwwroot/favicon.png index 8422b59..d36979a 100644 Binary files a/MaBibli.Client/wwwroot/favicon.png and b/MaBibli.Client/wwwroot/favicon.png differ diff --git a/MaBibli.Client/wwwroot/icon-192.png b/MaBibli.Client/wwwroot/icon-192.png index 166f56d..9833617 100644 Binary files a/MaBibli.Client/wwwroot/icon-192.png and b/MaBibli.Client/wwwroot/icon-192.png differ diff --git a/MaBibli.Client/wwwroot/icon-512.png b/MaBibli.Client/wwwroot/icon-512.png index c2dd484..c4d1fc1 100644 Binary files a/MaBibli.Client/wwwroot/icon-512.png and b/MaBibli.Client/wwwroot/icon-512.png differ diff --git a/MaBibli.Client/wwwroot/js/cache-hors-ligne.js b/MaBibli.Client/wwwroot/js/cache-hors-ligne.js index d062bae..b3c0c98 100644 --- a/MaBibli.Client/wwwroot/js/cache-hors-ligne.js +++ b/MaBibli.Client/wwwroot/js/cache-hors-ligne.js @@ -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' 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' 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') : 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; + } +} diff --git a/MaBibli.Client/wwwroot/logo-bandeau.svg b/MaBibli.Client/wwwroot/logo-bandeau.svg new file mode 100644 index 0000000..dff9508 --- /dev/null +++ b/MaBibli.Client/wwwroot/logo-bandeau.svg @@ -0,0 +1,13 @@ + +