@page "/auteurs/{AuteurId:int}/bibliographie" @inject ServiceLivresApi Api @inject EtatReseau Reseau MaBibli — bibliographie @if (_bibliographie is null && _erreur is null) {

Bibliographie

Interrogation de la BnF…

} else if (_erreur is not null) {

Bibliographie

@* Une panne de réseau est le plus souvent passagère : sans ce bouton, il fallait recharger la page entière pour retenter. *@ @if (_reessayable) {
} } else if (_bibliographie is { } biblio) {

@if (biblio.EstNouveautes) { Nouveautés de @biblio.Auteur.Nom } else { @biblio.Auteur.Nom }

@* ⚠️ Les deux cas ci-dessous s'excluent, et c'est TOUT l'objet de ce bloc. Avant correction, une BnF muette produisait une liste vide, que l'écran commentait d'un rassurant « la BnF ne connaît aucun livre de cet auteur » — affiché juste sous l'avertissement qui disait le contraire. On affirmait une absence qu'on n'avait pas pu constater, ce qui est la pire des deux erreurs possibles ici : elle ferme la question au lieu de la laisser ouverte. La règle : on n'explique une absence de résultats QUE si la source a répondu. *@ @if (biblio.SourceMuette) {

La bibliographie n'a donc pas pu être établie : cet écran ne dit rien de ce que cet auteur a écrit.

} else if (biblio.Oeuvres.Count == 0) { @if (biblio.EstNouveautes && biblio.SeuilNouveautesInconnu) {

Impossible de dater le livre le plus récent que vous possédez dans les notices reçues. La BnF a répondu, mais aucune nouveauté fiable ne peut être déduite.

} else if (biblio.EstNouveautes) {

Aucune nouveauté trouvée depuis @biblio.AnneeSeuilNouveautes.

} else { @* La source a répondu, et elle ne connaît rien : là, et là seulement, on peut l'expliquer. *@

La BnF ne connaît aucun livre de cet auteur. C'est le cas attendu pour les auteurs étrangers : le dépôt légal français ne couvre que l'édition francophone, et un auteur traduit n'y a souvent pas de notice à son nom.

} } else if (biblio.Avertissement is not null) { @* Source répondue mais bancale (une page sur deux, par exemple) : la liste vaut quelque chose, on la montre en disant qu'elle est peut-être incomplète. *@ } else { @* Ces compteurs disent à l'utilisateur CE QU'IL REGARDE. Sur un auteur très réédité, la liste n'est qu'un extrait : le taire ferait croire qu'un livre absent n'existe pas. *@

@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "") — @_possedees déjà chez vous @if (_masquees > 0) { — @_masquees masquée@(_masquees > 1 ? "s" : "") }

@if (biblio.EstNouveautes) {

Œuvres parues après @biblio.AnneeSeuilNouveautes, d'après les éditions BnF reçues.

} @* Deux phrases distinctes plutôt qu'une seule à trous : enchaîner des fragments conditionnels dans une même phrase produit une ponctuation bancale dès qu'un des fragments manque (« exister. .34 notices ont été écartées »). *@

@if (biblio.Tronquee) { D'après le catalogue de la BnF, à partir de @biblio.NoticesLues notices sur les @biblio.NoticesAnnoncees qu'elle annonce : cette liste est un extrait, classé par pertinence. Un livre absent d'ici peut très bien exister. } else { D'après le catalogue de la BnF, sur @biblio.NoticesLues notice@(biblio.NoticesLues > 1 ? "s" : ""). }

@if (biblio.NoticesEcartees > 0) {

@biblio.NoticesEcartees notice@(biblio.NoticesEcartees > 1 ? "s ont" : " a") été écartée@(biblio.NoticesEcartees > 1 ? "s" : "") : autre support (livre audio, jeu) ou ouvrage signé de quelqu'un d'autre.

} @if (_message is not null) { }
@* Les deux bascules d'écran vivent ICI, à côté du filtre, et non plus dans une barre flottante en bas : « toute la bibliographie / les nouveautés » et « ses livres chez vous » sont trois façons de restreindre la même liste. Les chercher en bas d'un écran qui défile sur cinquante lignes n'avait pas de sens. *@
@if (_filtresOuverts) {
@if (_masquees > 0) {
}
} @* La barre de sélection est EN HAUT, au-dessus de la liste, et non plus flottante en bas : on coche en descendant, et il fallait auparavant redescendre jusqu'au bas de l'écran pour valider — ou pire, la barre recouvrait les dernières lignes qu'on venait justement de vouloir cocher. *@ @if (_selection.Count > 0) {
@_selection.Count sélectionnée@(_selection.Count > 1 ? "s" : "") @if (SelectionADecouvrir(biblio).Count is var aSouhaiter and > 0) { } @if (SelectionParMasquage(biblio, false).Count is var aMasquer and > 0) { } @if (SelectionParMasquage(biblio, true).Count is var aReafficher and > 0) { }
} @* Le rapprochement est fait par titre, et il rate des choses (voir CleOeuvre). Le dire franchement vaut mieux que laisser l'utilisateur découvrir seul qu'un livre qu'il possède n'est pas grisé. *@

Le rapprochement avec votre bibliothèque se fait par titre. Un livre que vous possédez sous un titre différent (traduction, intégrale, édition retitrée) n'apparaîtra donc pas comme possédé. Rien n'est masqué : à vous de trancher.

} } @code { [Parameter] public int AuteurId { get; set; } [SupplyParameterFromQuery(Name = "nouveautes")] public bool Nouveautes { get; set; } private BibliographieDto? _bibliographie; private int _modePossession; private bool _filtresOuverts; private bool _afficherMasquees; private bool _enCours; private bool _chargement; /// Vrai quand l'échec vient du réseau, donc qu'une nouvelle tentative a un sens. private bool _reessayable; private string? _erreur; private string? _message; private int _possedees; private int _masquees; private IReadOnlyList _livresCatalogue = []; private readonly HashSet _oeuvresOuvertes = []; private readonly HashSet _selection = []; protected override async Task OnParametersSetAsync() => await ChargerAsync(); private IEnumerable Affichees(BibliographieDto biblio) => biblio.Oeuvres.Where(o => (_afficherMasquees || !o.Masquee) && (_modePossession == 0 || (_modePossession == 1 && o.Possede) || (_modePossession == 2 && o.ADecouvrir))); private int NombreFiltres => (_modePossession == 0 ? 0 : 1) + (_masquees > 0 && !_afficherMasquees ? 1 : 0); /// Les œuvres cochées qui peuvent effectivement rejoindre la liste d'envies. private List SelectionADecouvrir(BibliographieDto biblio) => biblio.Oeuvres.Where(o => _selection.Contains(o.Titre) && o.ADecouvrir).ToList(); private List SelectionParMasquage(BibliographieDto biblio, bool masquees) => biblio.Oeuvres.Where(o => _selection.Contains(o.Titre) && o.Masquee == masquees).ToList(); private async Task ChargerAsync() { _erreur = null; _reessayable = false; _chargement = true; _selection.Clear(); try { _bibliographie = Nouveautes ? await Api.ObtenirNouveautesAsync(AuteurId) : await Api.ObtenirBibliographieAsync(AuteurId); _livresCatalogue = await Api.ListerAsync(new CritereLivres()); if (_bibliographie is null) { // Celui-là ne se retente pas : l'auteur n'existe pas, réessayer ne le fera // pas apparaître. _erreur = "Cet auteur n'existe pas dans votre bibliothèque."; return; } RecompterAsync(); } catch (Exception) { // ⚠️ Jamais le message brut de l'exception : hors-ligne, c'est un « 404 Not Found » // du service worker que l'utilisateur n'a aucun moyen d'interpréter. La // bibliographie interroge la BnF, elle ne peut PAS venir du cache — il n'y a rien // à réparer, seulement à dire. // // Ce chemin-ci est l'échec de l'appel à NOTRE serveur (délai du navigateur compris) ; // une BnF muette, elle, revient en 200 avec un état, et se traite plus haut. _erreur = Reseau.EnLigne ? "La bibliographie n'a pas pu être obtenue. La BnF est parfois lente à répondre." : "La bibliographie vient de la BnF : indisponible hors ligne."; _reessayable = true; } finally { _chargement = false; } } private void RecompterAsync() { _possedees = _bibliographie?.Oeuvres.Count(o => o.Possede) ?? 0; _masquees = _bibliographie?.Oeuvres.Count(o => o.Masquee) ?? 0; } /// /// Applique en mémoire ce que l'écriture vient de changer, au lieu de tout recharger. /// /// /// ⚠️ C'est LA correction de lenteur signalée en usage : chaque « Je le veux » et chaque /// « Masquer » relançaient un ChargerAsync complet, donc une interrogation de la BnF /// (une seconde par page, jusqu'à dix pages pour les nouveautés, qui ne sont pas mises en /// cache) suivie d'une relecture de tout le catalogue. L'écran attendait une source /// distante pour apprendre un drapeau personnel qu'il connaissait déjà. /// /// Le cache partagé de est corrigé de la même façon de son /// côté ; ici on corrige la copie que cet écran tient en main, celle des nouveautés n'étant /// justement pas dans ce cache. /// /// private void MettreAJourOeuvre(string titre, Func correction) { if (_bibliographie is null) { return; } _bibliographie = _bibliographie with { Oeuvres = _bibliographie.Oeuvres .Select(o => o.Titre == titre ? correction(o) : o) .ToList(), }; RecompterAsync(); } private void Selectionner(string titre, bool selectionnee) { if (selectionnee) { _selection.Add(titre); } else { _selection.Remove(titre); } } private async Task AjouterSelectionAsync() { if (_bibliographie is null) { return; } var oeuvres = SelectionADecouvrir(_bibliographie); var saisies = oeuvres.Select(Saisie).ToList(); _enCours = true; _message = null; var resultats = await Api.AjouterSouhaitsAsync(saisies); _enCours = false; // Chaque envie réussie bascule sa ligne ; les échecs gardent la leur intacte et sont // rapportés. Rien n'est rechargé — voir MettreAJourOeuvre. for (var i = 0; i < oeuvres.Count; i++) { if (resultats[i].EstOk) { var id = resultats[i].Valeur?.Id; MettreAJourOeuvre(oeuvres[i].Titre, o => o with { Souhaite = true, SouhaitId = id }); } } var erreurs = resultats.Where(r => !r.EstOk).Select(r => r.Erreur).Where(e => e is not null).ToList(); _message = erreurs.Count == 0 ? null : string.Join(" ", erreurs); _selection.Clear(); } /// Masque (ou réaffiche) toutes les œuvres cochées qui sont dans l'autre état. private async Task MasquerSelectionAsync(bool masquer) { if (_bibliographie is null) { return; } var oeuvres = SelectionParMasquage(_bibliographie, !masquer); _enCours = true; _message = null; var erreurs = new List(); foreach (var oeuvre in oeuvres) { var motif = masquer ? await Api.MasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre) : await Api.DemasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre); if (motif is null) { MettreAJourOeuvre(oeuvre.Titre, o => o with { Masquee = masquer }); } else { erreurs.Add(motif); } } _enCours = false; _message = erreurs.Count == 0 ? null : string.Join(" ", erreurs.Distinct()); _selection.Clear(); } private async Task ModifierMasquageAsync(OeuvreBibliographie oeuvre, bool masquer) { _enCours = true; _message = masquer ? await Api.MasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre) : await Api.DemasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre); _enCours = false; if (_message is null) { MettreAJourOeuvre(oeuvre.Titre, o => o with { Masquee = masquer }); } } private void BasculerOeuvre(string titre) { if (!_oeuvresOuvertes.Add(titre)) { _oeuvresOuvertes.Remove(titre); } } /// /// Ce que la BnF sait de l'œuvre, mis en forme pour la liste d'envies. /// /// /// L'auteur enregistré est celui de l'écran, pas celui de la notice : c'est lui que /// l'utilisateur a en tête, et c'est sous ce nom qu'il retrouvera l'envie dans l'export. /// /// L'ISBN de la BnF est celui d'UNE édition relevée, souvent à 10 chiffres et parfois /// épuisée. Il est repris tel quel : il aide à commander, sans engager le choix. /// /// private EnregistrementSouhait Saisie(OeuvreBibliographie oeuvre) => new() { Titre = oeuvre.Titre, Auteur = _bibliographie!.Auteur.Nom, Editeur = oeuvre.Editeur, Annee = oeuvre.Annee, Isbn = oeuvre.Isbn, }; private async Task SouhaiterAsync(OeuvreBibliographie oeuvre) { if (_bibliographie is null) { return; } _enCours = true; _message = null; var resultat = await Api.AjouterSouhaitAsync(Saisie(oeuvre)); _enCours = false; if (!resultat.EstOk) { _message = resultat.Erreur; return; } MettreAJourOeuvre( oeuvre.Titre, o => o with { Souhaite = true, SouhaitId = resultat.Valeur?.Id }); } }