Rassemble au catalogue les tomes qui racontent la même histoire

Les livres d'une même série se suivaient au hasard de l'alphabet, et rien
ne disait qu'ils allaient ensemble. Ils tiennent maintenant sous le nom de
leur série, à la place qu'occupait le premier d'entre eux — l'ordre général
ne bouge donc pas — et ce nom mène à la série, seul écran qui montre aussi
les tomes qui manquent.

Le regroupement ne cache ni ne duplique rien : une bascule le défait, et
un livre rattaché à deux séries ne paraît qu'une fois, toujours sous la
même. La règle est une fonction pure, avec ses tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-22 00:16:52 +02:00
co-authored by Claude Opus 5
parent 75f962ab46
commit 7d2f3dc5d9
5 changed files with 459 additions and 4 deletions
+94 -4
View File
@@ -110,6 +110,22 @@
</div>
}
@*
⚠️ Grouper ne CACHE rien — c'est pourquoi cette bascule n'entre pas dans le compteur
du bouton « Filtrer », qui ne compte que ce qui rendrait des livres invisibles. Elle
se voit d'ailleurs dans la liste elle-même : avec ou sans blocs de série.
Décochée, on retrouve exactement le catalogue d'avant : tous les livres à plat.
*@
@if (_series is { Count: > 0 })
{
<label class="bascule">
<input type="checkbox" checked="@_grouper"
@onchange="e => _grouper = (bool?)e.Value ?? true" />
<span>Grouper les livres d'une même série</span>
</label>
}
<p class="champ-aide">
Le statut de lecture est <strong>le vôtre</strong> : filtrer dessus montre votre
lecture, pas celle du foyer. Le prêt, lui, est commun.
@@ -136,12 +152,41 @@ else if (_livres is not null)
{
<p class="compteur">@_livres.Count livre@(_livres.Count > 1 ? "s" : "")</p>
@*
Les tomes d'une série tiennent sous son nom, à la place qu'occuperait le premier d'entre
eux : l'ordre général du catalogue ne bouge pas, on ne perd donc pas un livre qu'on
cherchait à sa lettre. Le nom du bloc MÈNE À LA SÉRIE, où figurent aussi les tomes qu'on
ne possède pas — ce que le catalogue, lui, ne peut pas montrer.
*@
<ul class="liste-livres">
@foreach (var livre in _livres)
@foreach (var entree in Entrees)
{
<li @key="livre.Id">
<CarteLivre Livre="livre" />
</li>
@if (entree.Serie is { } serie)
{
<li class="groupe-serie" @key="serie">
<a class="groupe-serie-titre" href="@($"series/{serie.Id}")">
<span class="groupe-serie-nom">@serie.Titre</span>
<span class="groupe-serie-compte">
@entree.Livres.Count tome@(entree.Livres.Count > 1 ? "s" : "")
</span>
</a>
<ul class="liste-livres">
@foreach (var livre in entree.Livres)
{
<li @key="livre.Id">
<CarteLivre Livre="livre" />
</li>
}
</ul>
</li>
}
else
{
<li @key="entree.Livres[0].Id">
<CarteLivre Livre="entree.Livres[0]" />
</li>
}
}
</ul>
}
@@ -175,6 +220,25 @@ else if (_livres is not null)
public int? AuteurId { get; set; }
private IReadOnlyList<LivreDto>? _livres;
/// <summary>
/// Les séries du foyer, pour regrouper leurs tomes. <c>null</c> = pas encore lues.
/// </summary>
/// <remarks>
/// ⚠️ Elles ne sont lues <b>qu'une fois</b> par visite (et à chaque bascule du réseau), pas à
/// chaque chargement du catalogue : celui-ci se relit à chaque frappe dans la recherche, et
/// une requête de séries par caractère serait du gâchis pour une liste qui ne bouge pas
/// pendant qu'on tape. Le prix est qu'un tome rattaché depuis un autre écran n'apparaît
/// groupé qu'au prochain passage ici.
/// </remarks>
private IReadOnlyList<SerieDto>? _series;
/// <summary>Regroupement actif — l'utilisateur peut le défaire, rien ne disparaît alors.</summary>
private bool _grouper = true;
/// <summary>Le catalogue tel qu'il s'affiche : livres seuls et blocs de série mêlés.</summary>
private IReadOnlyList<EntreeCatalogue> Entrees =>
GroupementCatalogue.Grouper(_livres ?? [], _grouper ? _series : null);
private AuteurDto? _auteur;
private int? _auteurCharge;
private string _recherche = string.Empty;
@@ -238,14 +302,40 @@ else if (_livres is not null)
/// </summary>
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
/// <summary>
/// Lit les séries, et se contente de ne pas grouper si elles manquent.
/// </summary>
/// <remarks>
/// ⚠️ Une lecture ratée ne doit rien coûter au catalogue : sans série, la liste s'affiche à
/// plat, ce qui est exactement l'écran d'avant. Hors-ligne, la lecture retombe d'elle-même
/// sur l'instantané « series ».
/// </remarks>
private async Task ChargerSeriesAsync()
{
try
{
_series = await Api.ListerSeriesAsync();
}
catch (Exception)
{
_series = null;
}
}
private void SurChangementReseau() => _ = InvokeAsync(async () =>
{
await ChargerSeriesAsync();
await ChargerAsync();
StateHasChanged();
});
protected override async Task OnParametersSetAsync()
{
if (_series is null)
{
await ChargerSeriesAsync();
}
if (_auteurCharge != AuteurId)
{
_auteurCharge = AuteurId;