Files
mabibli/MaBibli.Client/Composants/ArbreSerie.razor
T
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:16 +02:00

134 lines
5.4 KiB
Plaintext

@using MaBibli.Shared.Dtos
@*
L'arbre des séries : rien d'autre que la descendance.
⚠️ Il dépliait auparavant la liste complète des tomes de chaque série, carte de livre
comprise. Un cycle de quatre séries occupait alors plusieurs écrans, et l'on ne voyait
justement plus ce qu'un arbre sert à voir — qui contient quoi. Les tomes ont leur écran,
celui de la série ; ici on ne montre que le titre, l'avancement, et le lien qui y mène.
Corollaire : tout est DÉPLIÉ par défaut, et l'on replie ce dont on ne veut pas. L'inverse
(tout replié) obligeait à ouvrir chaque nœud pour découvrir s'il contenait quelque chose,
c'est-à-dire à faire à la main le travail que l'arbre est censé faire.
*@
<li class="arbre-noeud" @key="Serie.Id">
<div class="arbre-ligne">
@if (Enfants.Count > 0)
{
<button type="button" class="arbre-plier" aria-expanded="@Aria.Etat(Deplie)"
aria-label="@($"{(Deplie ? "Replier" : "Déplier")} « {Serie.Titre} »")"
@onclick="Basculer">
<span aria-hidden="true">@(Deplie ? "▾" : "▸")</span>
</button>
}
else
{
@* Une puce à la place de la bascule : sans elle, les titres sans descendance ne
s'aligneraient pas sur ceux qui en ont. *@
<span class="arbre-puce" aria-hidden="true">•</span>
}
<a class="arbre-titre" href="@($"series/{Serie.Id}")">@Serie.Titre</a>
<span class="arbre-compte">@Avancement()</span>
@if (ActionsActives)
{
<span class="arbre-actions">
<button type="button" class="bouton bouton-compact"
disabled="@(_index <= 0)" title="@MotifBlocage"
aria-label="@($"Monter « {Serie.Titre} »")"
@onclick="Monter">▲</button>
<button type="button" class="bouton bouton-compact"
disabled="@(_index < 0 || _index == Soeurs.Count - 1)" title="@MotifBlocage"
aria-label="@($"Descendre « {Serie.Titre} »")"
@onclick="Descendre">▼</button>
</span>
}
</div>
@* Le chemin déjà rendu coupe la récursion : une boucle résiduelle en base ferait sinon
boucler le rendu Razor à l'infini. Le garde-fou serveur, lui, reste borné par le
nombre de séries. *@
@if (Deplie && Enfants.Count > 0)
{
<ul class="arbre-enfants">
@foreach (var enfant in Enfants.Where(e => !CheminAvecSerie.Contains(e.Id)))
{
<ArbreSerie Serie="enfant" Toutes="Toutes"
Totaux="Totaux" Chemin="@CheminAvecSerie"
ActionsActives="ActionsActives"
MotifBlocage="MotifBlocage"
Replies="Replies"
OnDeplacer="OnDeplacer" />
}
</ul>
}
</li>
@code {
[Parameter, EditorRequired] public SerieDto Serie { get; set; } = default!;
[Parameter, EditorRequired] public IReadOnlyList<SerieDto> Toutes { get; set; } = [];
[Parameter, EditorRequired] public IReadOnlyDictionary<int, (int Possedes, int Total)> Totaux { get; set; } = new Dictionary<int, (int, int)>();
[Parameter] public IReadOnlySet<int> Chemin { get; set; } = new HashSet<int>();
/// <summary>
/// Nœuds REPLIÉS, et non dépliés : le défaut est de tout montrer.
/// </summary>
/// <remarks>
/// État local de l'écran, volontairement non persistant : c'est un confort de lecture,
/// pas une propriété de la série.
/// </remarks>
[Parameter] public ISet<int> Replies { get; set; } = new HashSet<int>();
[Parameter] public bool ActionsActives { get; set; }
[Parameter] public string? MotifBlocage { get; set; }
[Parameter] public EventCallback<DeplacementSerie> OnDeplacer { get; set; }
private List<SerieDto> Soeurs => Toutes
.Where(s => s.SerieParenteId == Serie.SerieParenteId)
.OrderBy(s => s.Position)
.ThenBy(s => s.Id)
.ToList();
private List<SerieDto> Enfants => Toutes
.Where(s => s.SerieParenteId == Serie.Id)
.OrderBy(s => s.Position)
.ThenBy(s => s.Id)
.ToList();
private int _index => Soeurs.FindIndex(s => s.Id == Serie.Id);
private bool Deplie => !Replies.Contains(Serie.Id);
private IReadOnlySet<int> CheminAvecSerie => new HashSet<int>(Chemin) { Serie.Id };
/// <summary>
/// « 3 sur 7 tomes », en comptant les descendants — c'est ce qu'on vient chercher.
/// </summary>
private string Avancement()
{
var total = Totaux.TryGetValue(Serie.Id, out var valeur)
? valeur
: (Possedes: Serie.NombrePossedes, Total: Serie.Elements.Count);
return total.Total == 0
? "aucun tome recensé"
: $"{total.Possedes} sur {total.Total} tome{(total.Total > 1 ? "s" : "")}";
}
private void Basculer()
{
if (!Replies.Add(Serie.Id))
{
Replies.Remove(Serie.Id);
}
}
private Task Monter() => Deplacer(-1);
private Task Descendre() => Deplacer(1);
private Task Deplacer(int delta) =>
Serie.SerieParenteId is { } parent
? OnDeplacer.InvokeAsync(new DeplacementSerie(Serie.Id, parent, delta))
: Task.CompletedTask;
}