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>
@@ -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é,
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 230 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |