Files
mabibli/MaBibli.Client/Pages/AjoutIsbn.razor
T
mathieuandClaude Opus 5 ebc5f95d49 Consulter la bibliotheque hors-ligne depuis un cache IndexedDB
Le piege que CLAUDE.md signale : en Blazor WebAssembly le code tourne dans le
navigateur alors que SQLite vit sur le serveur, et le service worker ne met en
cache que les assets. Sans travail explicite, l'application demarre hors-ligne
et affiche une bibliotheque vide.

- js/cache-hors-ligne.js : instantanes JSON dans IndexedDB, et surveillance des
  bascules online/offline. Aucune logique metier.
- CacheHorsLigne / EtatReseau : lecture-ecriture des instantanes, et etat reseau
  combinant navigator.onLine (fiable seulement par sa negation) avec le sort
  reel des appels HTTP.
- ServiceLivresApi : les lectures retombent sur le cache, les ecritures sont
  refusees. Le catalogue entier est memorise, pas les reponses filtrees : c'est
  ce qui rend la recherche hors-ligne possible sur tout le fonds.
- FiltreLivresLocal : pendant navigateur de FiltreLivres, avec un test qui
  confronte les deux implementations sur les memes donnees.
- Interface : pastille et bandeau d'etat avec la date de synchronisation, et
  actions d'ecriture desactivees avec leur raison plutot que boutons morts.
- js/mise-a-jour.js : les empreintes WASM etant desactivees, le service worker
  est le seul cache-busting du projet. L'enregistrement journalise desormais ses
  echecs, et un bandeau propose la nouvelle version sans attendre la fermeture
  de tous les onglets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:59:19 +02:00

269 lines
8.5 KiB
Plaintext

@page "/ajout/isbn"
@inject ServiceLivresApi Api
@inject NavigationManager Navigation
@inject EtatReseau Reseau
@implements IDisposable
<PageTitle>MaBibli — ajouter par ISBN</PageTitle>
<h1 class="titre-page">Ajouter par ISBN</h1>
@if (_etape == Etape.Saisie)
{
<p class="message-discret">
Scannez le code-barres, ou saisissez l'ISBN imprimé sur le livre.
</p>
@*
Le scan comme le lookup interrogent la BnF puis OpenLibrary : ils EXIGENT le réseau,
et rien ne peut être ajouté au catalogue hors-ligne de toute façon. Le dire ici évite
une caméra ouverte pour rien, puis un délai d'attente incompréhensible.
*@
@if (!Reseau.EnLigne)
{
<p class="message-avertissement" role="status">
Hors ligne : la recherche par ISBN interroge la BnF et OpenLibrary, et l'ajout au
catalogue passe par le serveur. Les deux redeviendront possibles au retour du réseau.
</p>
}
<div class="barre-recherche">
<input class="champ-saisie" type="text" inputmode="numeric" placeholder="978…"
@bind="_isbn" @bind:event="oninput" @onkeydown="SurTouche"
aria-label="ISBN" autocomplete="off" />
</div>
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
disabled="@(_chargement || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_isbn))"
title="@MotifBlocage"
@onclick="ChercherAsync">
@(_chargement ? "Recherche…" : "Chercher")
</button>
@* La saisie manuelle reste le recours quand le code-barres est abîmé, absent,
ou que la caméra est indisponible : elle ne disparaît jamais derrière le scan. *@
<button type="button" class="bouton bouton-discret"
disabled="@(!Reseau.EnLigne)" title="@MotifBlocage"
@onclick="OuvrirScanner">
Scanner
</button>
<a class="bouton bouton-discret" href="ajout/manuel">Saisir à la main</a>
</div>
}
@if (_etape == Etape.Scan)
{
<ScannerCodeBarres OnCodeDetecte="SurCodeDetecteAsync" OnAnnuler="Recommencer" />
}
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@foreach (var avertissement in _avertissements)
{
<p class="message-avertissement">@avertissement</p>
}
@if (_etape == Etape.Choix)
{
@* Décision actée dans CLAUDE.md : plusieurs notices pour un même ISBN (rééditions
successives partageant l'ISBN), on ne choisit JAMAIS à la place de l'utilisateur.
Les candidats partagent titre et auteur : ce sont l'éditeur et l'année qui départagent,
donc ils sont mis en avant. *@
<p class="message-discret">
@_candidats.Count notices correspondent à cet ISBN. Elles se distinguent par l'éditeur et l'année.
</p>
<ul class="liste-candidats">
@foreach (var (candidat, index) in _candidats.Select((c, i) => (c, i)))
{
<li class="carte-candidat" @key="index">
<Couverture Url="@candidat.CoverUrl" Titre="@candidat.Titre" Classe="couverture-petite" />
<div class="carte-corps">
<p class="carte-titre">@candidat.Titre</p>
@if (!string.IsNullOrWhiteSpace(candidat.Auteur))
{
<p class="carte-auteur">@candidat.Auteur</p>
}
<p class="candidat-distinction">
<span class="candidat-editeur">@(candidat.Editeur ?? "éditeur inconnu")</span>
<span class="candidat-annee">@(candidat.Annee ?? "année inconnue")</span>
</p>
<p class="carte-details">
<span class="etiquette">@candidat.Source</span>
@if (!string.IsNullOrWhiteSpace(candidat.IsbnInterroge))
{
<span>trouvé via @candidat.IsbnInterroge</span>
}
</p>
<button type="button" class="bouton bouton-principal"
@onclick="() => Choisir(candidat)">
Choisir cette édition
</button>
</div>
</li>
}
</ul>
<div class="actions-formulaire">
<button type="button" class="bouton bouton-discret" @onclick="Recommencer">Aucune : saisir à la main</button>
</div>
}
@if (_etape == Etape.Formulaire)
{
<p class="message-discret">
Vérifiez et complétez la fiche : tout reste modifiable.
</p>
<FormulaireLivre Saisie="_saisie"
LibelleValidation="Ajouter au catalogue"
Erreur="@_erreurFormulaire"
EnCours="_enregistrement"
MessageBlocage="@MotifBlocage"
OnValider="EnregistrerAsync"
OnAnnuler="Recommencer" />
}
@code {
private enum Etape { Saisie, Scan, Choix, Formulaire }
private Etape _etape = Etape.Saisie;
private string _isbn = string.Empty;
private bool _chargement;
private bool _enregistrement;
private string? _erreur;
private string? _erreurFormulaire;
private IReadOnlyList<CandidatLivre> _candidats = [];
private IReadOnlyList<string> _avertissements = [];
private EnregistrementLivre _saisie = new();
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
private void SurChangementReseau() => _ = InvokeAsync(StateHasChanged);
public void Dispose() => Reseau.Change -= SurChangementReseau;
/// <summary>Raison du blocage des actions, ou <c>null</c> quand tout est possible.</summary>
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
private void OuvrirScanner()
{
_erreur = null;
_avertissements = [];
_etape = Etape.Scan;
}
// Le scan enchaîne directement sur le flux de lookup existant : l'utilisateur
// ne retape jamais ce qui vient d'être scanné.
private async Task SurCodeDetecteAsync(string isbn)
{
_isbn = isbn;
_etape = Etape.Saisie;
await ChercherAsync();
}
private async Task SurTouche(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_isbn))
{
await ChercherAsync();
}
}
private async Task ChercherAsync()
{
_chargement = true;
_erreur = null;
_avertissements = [];
try
{
var resultat = await Api.ChercherIsbnAsync(_isbn.Trim());
if (resultat is null)
{
_erreur = $"« {_isbn} » n'est pas un ISBN valide.";
return;
}
_avertissements = resultat.Avertissements;
_candidats = resultat.Candidats;
if (_candidats.Count == 0)
{
// Aucune notice : on ne bloque pas, le formulaire manuel reste la porte de sortie.
_erreur = "Aucune notice trouvée pour cet ISBN. Complétez la fiche à la main.";
_saisie = new EnregistrementLivre { Isbn = resultat.IsbnDemande };
_etape = Etape.Formulaire;
return;
}
if (_candidats.Count == 1)
{
Choisir(_candidats[0]);
return;
}
_etape = Etape.Choix;
}
catch (Exception ex)
{
_erreur = $"La recherche a échoué : {ex.Message}";
}
finally
{
_chargement = false;
}
}
private void Choisir(CandidatLivre candidat)
{
_saisie = new EnregistrementLivre
{
Isbn = _isbn.Trim(),
Titre = candidat.Titre,
Auteur = candidat.Auteur,
Editeur = candidat.Editeur,
CoverUrl = candidat.CoverUrl,
Format = Format.Physique,
Statut = Statut.ALire,
};
_erreur = null;
_etape = Etape.Formulaire;
}
private void Recommencer()
{
_etape = Etape.Saisie;
_erreur = null;
_erreurFormulaire = null;
_candidats = [];
}
private async Task EnregistrerAsync()
{
_enregistrement = true;
_erreurFormulaire = null;
var resultat = await Api.CreerAsync(_saisie);
_enregistrement = false;
if (!resultat.EstOk)
{
_erreurFormulaire = resultat.Erreur;
return;
}
Navigation.NavigateTo("/");
}
}