@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. *@
@if (ADesPixels) { @if (Agrandissable) { @* Un vrai } else { Couverture de @Titre } }
@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. *@ } @code { [Parameter] public string? Url { get; set; } [Parameter] public string? Titre { get; set; } [Parameter] public string? Classe { get; set; } /// Le clic sur l'image l'affiche en grand. Sans effet s'il n'y a pas de couverture. [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); /// /// 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(); 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(); } } }