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>
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
@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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user