Auteurs cherchables et triables, filtre de prêt, rang dans les exports, couvertures d'envies
Auteurs - Recherche et tri (par nom, ou du plus présent au moins présent), en mémoire : la liste est déjà chargée en entier — c'est elle qui sert l'instantané hors-ligne — et la filtrer côté serveur aurait rendu la page inutilisable sans réseau. - Trois colonnes dont une seule est élastique : compteur à gauche sur une largeur figée, nom au milieu, actions à droite. Les compteurs et les boutons s'alignent alors d'une ligne à l'autre au lieu de suivre la longueur du nom. Catalogue - Filtre « À la maison / Prêtés », posé dans les deux implémentations (serveur et hors-ligne) et dans le test qui les confronte, avec un cas de prêt CLOS — c'est exactement là que les deux auraient pu diverger en silence. - La ligne disparaît tant que rien n'est prêté, comme celles des formats et des types. Envies - ⚠️ Les couvertures manquaient parce que seul l'écran « ajouter une envie » remplissait CoverUrl : la bibliographie et les tomes manquants transmettent un ISBN mais aucune image. Le repli est posé à la LECTURE, donc il vaut aussi pour les envies déjà enregistrées, sans migration. - Les exports .txt et .csv portent le rang d'envie. Le .txt en a le plus besoin : son groupement par auteur détruit précisément cet ordre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,21 +67,71 @@ else if (_auteurs.Count == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="compteur">@_auteurs.Count auteur@(_auteurs.Count > 1 ? "s" : "")</p>
|
||||
@*
|
||||
Recherche et tri d'une seule pièce : la question qu'on se pose devant une liste
|
||||
d'auteurs est soit « où est untel ? » (recherche), soit « qui ai-je le plus ? » (tri).
|
||||
|
||||
⚠️ Tout se fait en mémoire, sans nouvel appel : la liste des auteurs est déjà
|
||||
entièrement chargée — c'est elle qui sert l'instantané hors-ligne — et la filtrer
|
||||
côté serveur aurait rendu la page inutilisable sans réseau. La normalisation passe
|
||||
par NormalisationTexte, comme partout : « emile » retrouve « Émile ».
|
||||
*@
|
||||
<div class="barre-recherche">
|
||||
<input class="champ-saisie" type="search" placeholder="Nom d'auteur…"
|
||||
value="@_recherche" @oninput="SurRecherche" aria-label="Rechercher un auteur" />
|
||||
</div>
|
||||
|
||||
<div class="segments" role="group" aria-label="Tri">
|
||||
<button type="button" class="segment @(_tri == TriAuteurs.Nom ? "segment-actif" : null)"
|
||||
@onclick="() => _tri = TriAuteurs.Nom">Par nom</button>
|
||||
<button type="button" class="segment @(_tri == TriAuteurs.NombreDeLivres ? "segment-actif" : null)"
|
||||
@onclick="() => _tri = TriAuteurs.NombreDeLivres">Les plus présents</button>
|
||||
</div>
|
||||
|
||||
@* Le compteur dit ce qu'on regarde : sur une liste filtrée, afficher le total ferait
|
||||
croire que la recherche n'a rien fait. *@
|
||||
<p class="compteur">
|
||||
@Affiches.Count auteur@(Affiches.Count > 1 ? "s" : "")
|
||||
@if (Affiches.Count != _auteurs.Count)
|
||||
{
|
||||
<text> sur @_auteurs.Count</text>
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (Affiches.Count == 0)
|
||||
{
|
||||
<p class="message-discret">Aucun auteur ne correspond à « @_recherche ».</p>
|
||||
}
|
||||
|
||||
<ul class="liste-auteurs">
|
||||
@foreach (var auteur in _auteurs)
|
||||
@foreach (var auteur in Affiches)
|
||||
{
|
||||
@*
|
||||
Trois colonnes, dont une seule est élastique.
|
||||
|
||||
Le nombre de livres passe à GAUCHE du nom, sur une largeur figée de trois
|
||||
chiffres : les compteurs s'alignent alors les uns sous les autres et se
|
||||
comparent d'un coup d'œil. Trois chiffres suffisent — personne ne possède
|
||||
mille livres d'un même auteur. Les actions se rangent tout à droite, elles
|
||||
aussi sur une largeur constante, pour que seul le nom absorbe la place
|
||||
restante au lieu de décaler les boutons ligne après ligne.
|
||||
*@
|
||||
<li class="carte-auteur-ligne" @key="auteur.Id">
|
||||
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
||||
<span class="etiquette">@Livres(auteur.NombreDeLivres)</span>
|
||||
<span class="auteur-compte" title="@Livres(auteur.NombreDeLivres)">
|
||||
@auteur.NombreDeLivres
|
||||
<span class="lecture-ecran">@Livres(auteur.NombreDeLivres)</span>
|
||||
</span>
|
||||
|
||||
<a class="lien-auteur auteur-nom" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
|
||||
|
||||
<span class="auteur-actions">
|
||||
@* Le declencheur decrit dans IDEES.md : depuis un auteur qu'on a deja, voir
|
||||
tout ce qu'il a ecrit et marquer ce qui manque. *@
|
||||
<a class="bouton bouton-compact bouton-discret"
|
||||
href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
|
||||
@if (_auteurRenomme == auteur.Id)
|
||||
{
|
||||
<input class="champ-texte champ-auteur-renomme" @bind="_nomRenomme" />
|
||||
<input class="champ-saisie champ-auteur-renomme" @bind="_nomRenomme" />
|
||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||
disabled="@_renommageEnCours" @onclick="RenommerAsync">
|
||||
Enregistrer
|
||||
@@ -100,6 +150,7 @@ else
|
||||
Renommer
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@@ -127,7 +178,19 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>Les deux façons de trier une liste d'auteurs, et il n'y en a pas de troisième.</summary>
|
||||
private enum TriAuteurs
|
||||
{
|
||||
/// <summary>Ordre alphabétique : « où est untel ? ».</summary>
|
||||
Nom = 0,
|
||||
|
||||
/// <summary>Du plus présent au moins présent : « qui ai-je le plus ? ».</summary>
|
||||
NombreDeLivres = 1,
|
||||
}
|
||||
|
||||
private IReadOnlyList<AuteurDto>? _auteurs;
|
||||
private string _recherche = string.Empty;
|
||||
private TriAuteurs _tri = TriAuteurs.Nom;
|
||||
private IReadOnlyList<SuggestionRapprochement>? _suggestions;
|
||||
private bool _enCours;
|
||||
private bool _renommageEnCours;
|
||||
@@ -153,6 +216,47 @@ else
|
||||
|
||||
private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";
|
||||
|
||||
private void SurRecherche(ChangeEventArgs e) => _recherche = e.Value?.ToString() ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// La liste telle qu'elle s'affiche : filtrée par la recherche, puis triée.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// La recherche est une <b>sous-chaîne du nom normalisé</b>, comme celle du catalogue :
|
||||
/// « zola » retrouve « Émile Zola », et « emile » aussi. Elle ne cherche pas mot à mot,
|
||||
/// contrairement au rapprochement d'auteurs, qui répond à une autre question — « ces deux
|
||||
/// noms désignent-ils la même personne ? » — et n'a rien à faire ici.
|
||||
/// <para>
|
||||
/// Le tri par nom est <b>ordinal sur la forme normalisée</b>, pour ne pas dépendre de la
|
||||
/// culture du navigateur : c'est la même règle que <c>FiltreLivresLocal</c>, et elle évite
|
||||
/// qu'un appareil classe « Éluard » avant « Duras » et un autre après.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private IReadOnlyList<AuteurDto> Affiches
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_auteurs is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var terme = NormalisationTexte.Normaliser(_recherche);
|
||||
var retenus = string.IsNullOrEmpty(terme)
|
||||
? _auteurs
|
||||
: _auteurs.Where(a => NormalisationTexte.Normaliser(a.Nom).Contains(terme)).ToList();
|
||||
|
||||
return _tri == TriAuteurs.NombreDeLivres
|
||||
? retenus
|
||||
.OrderByDescending(a => a.NombreDeLivres)
|
||||
.ThenBy(a => NormalisationTexte.Normaliser(a.Nom), StringComparer.Ordinal)
|
||||
.ToList()
|
||||
: retenus
|
||||
.OrderBy(a => NormalisationTexte.Normaliser(a.Nom), StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ChargerAsync()
|
||||
{
|
||||
_erreur = null;
|
||||
|
||||
Reference in New Issue
Block a user