@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)
{
@_erreur
}
@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.
ISBN @FormatageIsbn.Afficher(candidat.IsbnInterroge)
}
}
}
@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");
}
}