Files
mabibli/MaBibli.Client/Pages/Bibliographie.razor
T
mathieuandClaude Opus 5 8b8fe3be6e Ajouter la liste d'envies personnelle, son export et la bibliographie par auteur
La liste d'envies vit dans une table séparée (LivreSouhaite) plutôt que dans un
statut de plus sur Livre : un livre souhaité n'est pas possédé, et le loger dans
Livres l'aurait fait entrer dans le catalogue, les compteurs et les prêts, au
prix d'un « et qui n'est pas souhaité » à répéter dans chaque lecture. La portée
est personnelle, comme le statut de lecture — mais ici Utilisateur est une vraie
frontière : toute lecture filtre dessus.

Bibliographie : SRU BnF interrogé par bib.author, vérifié le 2026-08-18. Deux
filtres mesurés sur des réponses réelles sont indispensables — le type de
document (l'index mêle livres audio, jeux et spectacles) et surtout l'auteur
réel de la notice, « all » rapprochant les mots sur l'ensemble des auteurs :
« Émile Zola » remonte sinon toute l'œuvre de sa fille Denise Le Blond-Zola.
Les rééditions sont regroupées par clé d'œuvre (183 notices Werber -> 51
œuvres). Le rapprochement avec l'étagère se fait par titre, pas par ISBN, qui
désigne une édition et non une œuvre ; ses limites sont dites à l'écran.

Export en deux formats, tous deux du texte sans dépendance : .txt groupé par
auteur pour la librairie, .csv à séparateur point-virgule et BOM pour le
tableur.

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

246 lines
9.3 KiB
Plaintext

@page "/auteurs/{AuteurId:int}/bibliographie"
@inject ServiceLivresApi Api
<PageTitle>MaBibli — bibliographie</PageTitle>
@if (_bibliographie is null && _erreur is null)
{
<h1 class="titre-page">Bibliographie</h1>
<p class="message-discret">Interrogation de la BnF…</p>
}
else if (_erreur is not null)
{
<h1 class="titre-page">Bibliographie</h1>
<p class="message-erreur" role="alert">@_erreur</p>
}
else if (_bibliographie is { } biblio)
{
<h1 class="titre-page">@biblio.Auteur.Nom</h1>
@if (biblio.Avertissement is not null)
{
<p class="message-avertissement" role="alert">@biblio.Avertissement</p>
}
@if (biblio.Oeuvres.Count == 0)
{
<p class="message-discret">
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.
</p>
}
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. *@
<p class="compteur">
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
— @_possedees déjà chez vous
</p>
@* 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 »). *@
<p class="message-discret">
@if (biblio.Tronquee)
{
<text>
D'après le catalogue de la BnF, à partir de @biblio.NoticesLues notices
sur les <strong>@biblio.NoticesAnnoncees</strong> qu'elle annonce :
<strong>cette liste est un extrait</strong>, classé par pertinence.
Un livre absent d'ici peut très bien exister.
</text>
}
else
{
<text>
D'après le catalogue de la BnF, sur
@biblio.NoticesLues notice@(biblio.NoticesLues > 1 ? "s" : "").
</text>
}
</p>
@if (biblio.NoticesEcartees > 0)
{
<p class="message-discret">
@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.
</p>
}
@if (_message is not null)
{
<p class="message-erreur" role="alert">@_message</p>
}
@* Filtre sur une seule question : « qu'est-ce qui me manque ? ». Les livres possédés ne
sont jamais masqués par défaut — le grisage est l'information principale de l'écran. *@
<div class="filtres">
<div class="segments" role="group" aria-label="Filtrer la bibliographie">
<button type="button" class="segment @(_seulementManquants ? null : "segment-actif")"
@onclick="() => _seulementManquants = false">Tout</button>
<button type="button" class="segment @(_seulementManquants ? "segment-actif" : null)"
@onclick="() => _seulementManquants = true">Ce qui me manque</button>
</div>
</div>
<ul class="liste-bibliographie">
@foreach (var oeuvre in Affichees(biblio))
{
@* La classe « oeuvre-possedee » grise la ligne : le livre est déjà sur l'étagère. *@
<li class="carte-oeuvre @(oeuvre.Possede ? "oeuvre-possedee" : null)" @key="oeuvre.Titre">
<div class="carte-corps">
<p class="carte-titre">
@if (oeuvre is { Possede: true, LivreId: { } livreId })
{
<a class="lien-auteur" href="@($"livres/{livreId}")">@oeuvre.Titre</a>
}
else
{
@oeuvre.Titre
}
</p>
<p class="carte-details">
@if (!string.IsNullOrWhiteSpace(oeuvre.Annee))
{
<span>@oeuvre.Annee</span>
}
@if (!string.IsNullOrWhiteSpace(oeuvre.Editeur))
{
<span>@oeuvre.Editeur</span>
}
@if (oeuvre.NombreEditions > 1)
{
<span class="etiquette">@oeuvre.NombreEditions éditions</span>
}
@if (oeuvre.Possede)
{
<span class="etiquette etiquette-possede">Déjà chez vous</span>
}
else if (oeuvre.Souhaite)
{
<span class="etiquette etiquette-souhaite">Dans vos envies</span>
}
</p>
</div>
@if (oeuvre.ADecouvrir)
{
<button type="button" class="bouton bouton-compact bouton-principal"
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
Je le veux
</button>
}
</li>
}
</ul>
@* 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é. *@
<p class="message-discret">
Le rapprochement avec votre bibliothèque se fait <strong>par titre</strong>. 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.
</p>
}
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="auteurs">Retour aux auteurs</a>
@if (_bibliographie is { } b)
{
<a class="bouton bouton-discret" href="@($"?auteur={b.Auteur.Id}")">Ses livres chez vous</a>
}
<a class="bouton bouton-discret" href="souhaits">Ma liste d'envies</a>
</nav>
@code {
[Parameter]
public int AuteurId { get; set; }
private BibliographieDto? _bibliographie;
private bool _seulementManquants;
private bool _enCours;
private string? _erreur;
private string? _message;
private int _possedees;
protected override async Task OnParametersSetAsync() => await ChargerAsync();
private IEnumerable<OeuvreBibliographie> Affichees(BibliographieDto biblio) =>
_seulementManquants ? biblio.Oeuvres.Where(o => o.ADecouvrir) : biblio.Oeuvres;
private async Task ChargerAsync()
{
_erreur = null;
try
{
_bibliographie = await Api.ObtenirBibliographieAsync(AuteurId);
if (_bibliographie is null)
{
_erreur = "Cet auteur n'existe pas dans votre bibliothèque.";
return;
}
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
}
catch (Exception ex)
{
_erreur = $"Impossible d'obtenir la bibliographie : {ex.Message}";
}
}
/// <summary>
/// Ajoute une œuvre à la liste d'envies, en emportant ce que la BnF en sait.
/// </summary>
/// <remarks>
/// 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.
/// <para>
/// Après l'ajout, la bibliographie est <b>relue</b> plutôt que corrigée en mémoire : c'est
/// le serveur qui sait rapprocher l'envie de l'œuvre, et refaire ce calcul ici le
/// dupliquerait — avec le risque qu'il diverge.
/// </para>
/// </remarks>
private async Task SouhaiterAsync(OeuvreBibliographie oeuvre)
{
if (_bibliographie is null)
{
return;
}
_enCours = true;
_message = null;
var resultat = await Api.AjouterSouhaitAsync(new EnregistrementSouhait
{
Titre = oeuvre.Titre,
Auteur = _bibliographie.Auteur.Nom,
Editeur = oeuvre.Editeur,
Annee = oeuvre.Annee,
// 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.
Isbn = oeuvre.Isbn,
});
_enCours = false;
if (!resultat.EstOk)
{
_message = resultat.Erreur;
return;
}
await ChargerAsync();
}
}