@page "/souhaits/ajout" @inject ServiceLivresApi Api @inject EtatReseau Reseau @inject NavigationManager Navigation @implements IDisposable @* Écran séparé, comme la fiche livre l'est de son édition : la page « Envies » ne montre plus que la liste, et l'ajout a la place de proposer une vraie recherche. Trois façons d'alimenter la liste, dans l'ordre où le besoin se présente : — par titre/auteur, le cas NORMAL : on souhaite un livre qu'on n'a pas en main ; — par ISBN, quand on l'a en main (en librairie, chez quelqu'un) ; — à la main, quand aucune source ne connaît le livre. *@ MaBibli — ajouter une envie

Ajouter une envie

@if (!Reseau.EnLigne) {

Hors ligne : la recherche interroge la BnF et OpenLibrary, et l'enregistrement passe par le serveur. Votre liste reste consultable.

}
@if (_mode == Mode.Titre) {
} @if (_mode == Mode.Isbn) {

Le livre est en main : le code-barres suffit.

@* J2 : le cas d'usage est direct — le livre est en main, en librairie. C'est le même composant de scan que l'écran d'ajout au catalogue, sans création de Livre au bout : un souhait n'est pas une possession. *@
} @if (_scan) { } @* L1 : la BnF met environ une seconde à répondre. Sans indicateur, l'écran ne dit rien et l'on reclique — donc on relance l'appel. *@ @if (_recherche) { } @if (_erreur is not null) { } @foreach (var avertissement in _avertissements) {

@avertissement

} @if (_candidats.Count > 0) { @* On ne choisit jamais à la place de l'utilisateur : ce sont l'éditeur, l'année et l'auteur qui départagent des notices au titre identique. *@

@_candidats.Count notice@(_candidats.Count > 1 ? "s" : "") trouvée@(_candidats.Count > 1 ? "s" : ""). Choisissez celle qui correspond, ou complétez à la main.

} @if (_mode == Mode.Manuel) { @* Le recours quand aucune source ne connaît le livre — un ouvrage étranger, un titre à paraître, un livre d'occasion sans ISBN. Il ne disparaît jamais derrière la recherche. *@
} @if (_mode == Mode.Revue) { @* ⚠️ Aucune recherche ici, et ce n'est pas un manque : l'ISSN désigne un TITRE de périodique, jamais une parution. Interroger la BnF ne dirait donc rien du numéro, qui est justement ce qu'on souhaite. On note ce qu'on a vu en kiosque. *@

Une revue entière, ou un numéro précis. Le numéro peut rester vide : « je voudrais m'abonner à Médor » est une envie comme une autre.

} @code { private enum Mode { Titre, Isbn, Manuel, Revue } private Mode _mode = Mode.Titre; private ElementReference _premierChamp; private bool _focusAFaire = true; private string _titreCherche = string.Empty; private string _auteurCherche = string.Empty; private string _isbn = string.Empty; private EnregistrementSouhait _saisie = new(); private EnregistrementRevueSouhaitee _saisieRevue = new(); private IReadOnlyList _candidats = []; private IReadOnlyList _avertissements = []; private TriResultats _tri = TriResultats.Date; private bool _recherche; private bool _ajout; /// Le scanner est ouvert. Il ne l'est que depuis la voie « par ISBN ». private bool _scan; private string? _erreur; protected override void OnInitialized() => Reseau.Change += SurChangementReseau; private void SurChangementReseau() => _ = InvokeAsync(StateHasChanged); public void Dispose() => Reseau.Change -= SurChangementReseau; private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne; private enum TriResultats { Date, Titre, Editeur, } /// /// Focalise le premier champ à l'ouverture et à chaque changement de mode. /// /// /// Même raison qu'à l'écran d'ajout par ISBN : on arrive ici pour taper, et le mode « Par /// ISBN » doit rester utilisable à la douchette. Le drapeau évite de reprendre le focus à /// chaque rendu, ce qui arracherait le curseur en pleine frappe. /// protected override async Task OnAfterRenderAsync(bool premierRendu) { if (!_focusAFaire) { return; } _focusAFaire = false; try { await _premierChamp.FocusAsync(); } catch (InvalidOperationException) { } } private void OuvrirScanner() { _erreur = null; _candidats = []; _avertissements = []; _scan = true; } /// /// Enchaîne la recherche sur le code lu, sans bouton à confirmer. /// /// /// Le code reste dans le champ : un décodage erroné se corrige et se relance. C'est la /// décision déjà actée pour l'écran d'ajout au catalogue, et la seule cohérente avec une /// douchette, qui envoie Entrée d'elle-même. /// /// ⚠️ L'add-on EAN-2 (numéro de parution) est ignoré ici, et ce n'est pas un oubli : il ne /// concerne que les revues, qui ne se souhaitent pas — cet écran alimente une liste /// d'œuvres. Un code de périodique est d'ailleurs intercepté plus bas et nommé. /// /// private async Task SurCodeDetecteAsync(CodeScanne lecture) { _scan = false; _isbn = lecture.Code; await ChercherParIsbnAsync(); } private void Basculer(Mode mode) { _scan = false; _mode = mode; _focusAFaire = true; _erreur = null; _candidats = []; _avertissements = []; _tri = TriResultats.Date; } private async Task SurTouche(KeyboardEventArgs e) { if (e.Key != "Enter") { return; } if (_mode == Mode.Titre && !string.IsNullOrWhiteSpace(_titreCherche)) { await ChercherParTitreAsync(); } else if (_mode == Mode.Isbn && !string.IsNullOrWhiteSpace(_isbn)) { await ChercherParIsbnAsync(); } } private async Task ChercherParTitreAsync() => await ChercherAsync(() => Api.ChercherNoticesAsync(_titreCherche, _auteurCherche)); private async Task ChercherParIsbnAsync() => await ChercherAsync(() => Api.ChercherIsbnAsync(_isbn.Trim())); private async Task ChercherAsync(Func> appel) { _recherche = true; _erreur = null; _candidats = []; _avertissements = []; try { var resultat = await appel(); if (resultat is null) { _erreur = Reseau.EnLigne ? "La recherche n'a rien pu ramener. Réessayez, ou complétez à la main." : "La recherche interroge la BnF : indisponible hors ligne."; return; } _avertissements = resultat.Avertissements; // Un code de périodique n'est pas un livre : on le dit ici aussi, plutôt que de // laisser un « aucun résultat » qui enverrait chercher un défaut inexistant. if (resultat.Periodique is { } revue) { var nom = revue.Titre is null ? "un magazine" : $"le magazine « {revue.Titre} »"; _erreur = $"Ce code désigne {nom} (ISSN {revue.Issn}), pas un livre."; return; } _candidats = resultat.Candidats; _candidats = Trier(_candidats); if (_candidats.Count == 0) { _erreur = "Aucune notice ne correspond. Vous pouvez l'ajouter à la main."; } } finally { _recherche = false; } } private void ChangerTri(string? valeur) { if (!Enum.TryParse(valeur, out var tri)) { return; } _tri = tri; _candidats = Trier(_candidats); } private IReadOnlyList Trier(IEnumerable candidats) => _tri switch { TriResultats.Titre => candidats .OrderBy(c => c.Titre, StringComparer.CurrentCultureIgnoreCase) .ThenBy(c => c.Editeur, StringComparer.CurrentCultureIgnoreCase) .ToList(), TriResultats.Editeur => candidats .OrderBy(c => c.Editeur ?? string.Empty, StringComparer.CurrentCultureIgnoreCase) .ThenBy(c => c.Titre, StringComparer.CurrentCultureIgnoreCase) .ToList(), _ => candidats .OrderByDescending(c => AnneeTriable(c.Annee)) .ThenBy(c => c.Titre, StringComparer.CurrentCultureIgnoreCase) .ToList(), }; private static int AnneeTriable(string? date) { if (string.IsNullOrWhiteSpace(date)) { return 0; } for (var i = 0; i + 4 <= date.Length; i++) { if (int.TryParse(date.AsSpan(i, 4), out var annee) && annee is >= 1000 and <= 2999) { return annee; } } return 0; } /// /// Enregistre le candidat choisi, couverture comprise. /// /// /// C'est ce qui remplit enfin LivreSouhaite.CoverUrl, jusqu'ici toujours vide : la /// couverture vient d'OpenLibrary dès qu'un ISBN est connu. Sans ISBN il n'y en a pas, et /// l'on n'en invente pas — la vignette retombe sur son substitut à initiale. /// private Task AjouterCandidatAsync(CandidatLivre candidat) => AjouterAsync(new EnregistrementSouhait { Titre = candidat.Titre, Auteur = candidat.Auteur, Editeur = candidat.Editeur, Annee = candidat.Annee, Isbn = candidat.IsbnInterroge, CoverUrl = candidat.CoverUrl, }); private async Task AjouterAsync(EnregistrementSouhait saisie) { _ajout = true; _erreur = null; var resultat = await Api.AjouterSouhaitAsync(saisie); _ajout = false; if (!resultat.EstOk) { _erreur = resultat.Erreur; return; } // Retour à la liste : l'envie vient de s'y poser en dernier, et c'est là qu'on la classe. Navigation.NavigateTo("souhaits"); } private async Task AjouterRevueAsync() { _ajout = true; _erreur = null; var resultat = await Api.AjouterRevueSouhaiteeAsync(_saisieRevue); _ajout = false; if (!resultat.EstOk) { _erreur = resultat.Erreur; return; } Navigation.NavigateTo("souhaits"); } }