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>
134 lines
5.4 KiB
Plaintext
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;
|
|
}
|