Files
mabibli/MaBibli.Client/Composants/Couverture.razor
T
LIMONIER MathieuandCopilot 5d45eb492e 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>
2026-08-20 13:03:14 +02:00

159 lines
5.9 KiB
Plaintext

@using MaBibli.Client.Services
@inject CacheHorsLigne Cache
@inject EtatReseau Reseau
@*
Couverture d'un livre, avec substitut.
Les URL de couverture OpenLibrary renvoient des 502 intermittents (mesuré en phase 2 :
200, 200, puis 502 sur le même ISBN). On ne valide donc JAMAIS l'URL par un appel préalable :
on l'affiche, et si l'image casse, l'attribut onerror la masque et laisse apparaître le
substitut qui se trouve déjà dessous. Aucun aller-retour serveur, aucun état C# à gérer.
@key sur l'img : si l'URL change, Blazor recrée l'élément au lieu de réutiliser celui que
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.
⚠️ 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>
@if (ADesPixels)
{
@if (Agrandissable)
{
@* Un vrai <button> et pas une <div> cliquable : cible tactile, focus clavier,
et activation à l'Entrée sans une ligne de code. *@
<button type="button" class="couverture-declencheur"
aria-label="Agrandir la couverture de @Titre"
@onclick="Ouvrir">
<img @key="UrlAffichee"
class="couverture-image"
src="@UrlAffichee"
alt="Couverture de @Titre"
loading="lazy"
onerror="this.classList.add('image-cassee'); const b=this.closest('.couverture-declencheur'); if(b){b.disabled=true;}" />
</button>
}
else
{
<img @key="UrlAffichee"
class="couverture-image"
src="@UrlAffichee"
alt="Couverture de @Titre"
loading="lazy"
onerror="this.classList.add('image-cassee')" />
}
}
</div>
@if (_agrandie)
{
@* Fermeture au clic extérieur (le clic sur l'image ne remonte pas) et à Échap.
Le tabindex rend le calque focusable : sans focus, aucun keydown ne lui parviendrait. *@
<div class="calque-couverture" role="dialog" aria-modal="true"
aria-label="Couverture de @Titre"
tabindex="-1" @ref="_calque"
@onclick="Fermer" @onkeydown="SurToucheAsync">
<img class="calque-image" src="@UrlAffichee" alt="Couverture de @Titre"
@onclick:stopPropagation="true"
onerror="this.classList.add('image-cassee')" />
<button type="button" class="calque-fermer" aria-label="Fermer">✕</button>
</div>
}
@code {
[Parameter] public string? Url { get; set; }
[Parameter] public string? Titre { get; set; }
[Parameter] public string? Classe { get; set; }
/// <summary>Le clic sur l'image l'affiche en grand. Sans effet s'il n'y a pas de couverture.</summary>
[Parameter] public bool Agrandissable { get; set; }
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();
private void Ouvrir()
{
_agrandie = true;
_aFocaliser = true;
}
private void Fermer() => _agrandie = false;
private void SurToucheAsync(KeyboardEventArgs e)
{
if (e.Key is "Escape" or "Esc")
{
Fermer();
}
}
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)
{
_aFocaliser = false;
await _calque.FocusAsync();
}
}
}