Première relation entre livres du projet. Une série contient des séries (un cycle EST une série de séries) et des PLACES dans l'ordre de lecture — pas des livres : une place sans livre est le trou qu'on vient voir. L'ordre est stocké, jamais déduit d'une année : une préquelle se lit avant le livre paru dix ans plus tôt. Les tomes non possédés se saisissent à la main, aucune source ne donnant l'ordre de lecture d'une saga. Le titre de la place survit à la suppression du livre (SetNull, pas de cascade), sans quoi perdre un exemplaire effacerait le tome 3 de la série. Les séries sont communes au foyer. Seule exception : la mise en envies d'un tome manquant, qui écrit dans une liste personnelle — c'est le seul point où les deux portées se rencontrent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
4.6 KiB
Plaintext
143 lines
4.6 KiB
Plaintext
@page "/series"
|
|
@inject ServiceLivresApi Api
|
|
@inject EtatReseau Reseau
|
|
@implements IDisposable
|
|
|
|
<PageTitle>MaBibli — séries</PageTitle>
|
|
|
|
<h1 class="titre-page">Séries</h1>
|
|
|
|
<p class="message-discret">
|
|
Sagas, cycles et trilogies, dans leur ordre de <strong>lecture</strong> — qui n'est pas
|
|
toujours celui de publication. Les séries sont communes au foyer, comme le catalogue.
|
|
</p>
|
|
|
|
@if (_erreur is not null)
|
|
{
|
|
<p class="message-erreur" role="alert">@_erreur</p>
|
|
}
|
|
|
|
@* Créer une série depuis la liste : c'est le seul endroit d'où l'on part, et un écran de plus
|
|
pour un unique champ ne se justifierait pas. *@
|
|
<div class="barre-recherche">
|
|
<input class="champ-saisie" type="text" placeholder="Nom d'une nouvelle série…"
|
|
@bind="_nouveau" @bind:event="oninput" @onkeydown="SurTouche"
|
|
aria-label="Nom de la nouvelle série" autocomplete="off" />
|
|
</div>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(_enCours || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_nouveau))"
|
|
title="@MotifBlocage"
|
|
@onclick="CreerAsync">
|
|
Créer la série
|
|
</button>
|
|
</div>
|
|
|
|
@if (_series is null)
|
|
{
|
|
<p class="message-discret" role="status">Chargement…</p>
|
|
}
|
|
else if (_series.Count == 0)
|
|
{
|
|
<p class="message-discret">
|
|
Aucune série pour l'instant. Créez-en une, puis ajoutez-y vos livres et les tomes
|
|
qui vous manquent : c'est ce qui permet de voir les trous.
|
|
</p>
|
|
}
|
|
else
|
|
{
|
|
<ul class="liste-series">
|
|
@foreach (var serie in Racines)
|
|
{
|
|
<li class="carte-serie" @key="serie.Id">
|
|
<a class="carte-titre" href="@($"series/{serie.Id}")">@serie.Titre</a>
|
|
<p class="carte-details">@Avancement(serie)</p>
|
|
|
|
@* Un cycle contient des séries : elles se voient d'ici, sans ouvrir le cycle. *@
|
|
@if (Filles(serie.Id) is { Count: > 0 } filles)
|
|
{
|
|
<ul class="liste-sous-series">
|
|
@foreach (var fille in filles)
|
|
{
|
|
<li @key="fille.Id">
|
|
<a href="@($"series/{fille.Id}")">@fille.Titre</a>
|
|
<span class="carte-details">@Avancement(fille)</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
|
|
@code {
|
|
private IReadOnlyList<SerieDto>? _series;
|
|
private string _nouveau = string.Empty;
|
|
private bool _enCours;
|
|
private string? _erreur;
|
|
|
|
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
|
|
|
|
/// <summary>Séries de premier niveau. Celles rangées dans un cycle s'affichent sous lui.</summary>
|
|
private IEnumerable<SerieDto> Racines =>
|
|
_series?.Where(s => s.SerieParenteId is null) ?? [];
|
|
|
|
private List<SerieDto> Filles(int parenteId) =>
|
|
_series?.Where(s => s.SerieParenteId == parenteId).OrderBy(s => s.Position).ToList() ?? [];
|
|
|
|
/// <summary>
|
|
/// « 4 tomes sur 7 » : le compteur est l'information principale de l'écran.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Une série sans aucun tome ne se décrit pas par « 0 sur 0 », qui ressemble à une erreur :
|
|
/// elle dit ce qu'il faut faire.
|
|
/// </remarks>
|
|
private static string Avancement(SerieDto serie) =>
|
|
serie.Elements.Count == 0
|
|
? "Aucun tome recensé"
|
|
: $"{serie.NombrePossedes} tome{(serie.NombrePossedes > 1 ? "s" : "")} sur {serie.Elements.Count}";
|
|
|
|
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
|
|
|
|
private void SurChangementReseau() => _ = InvokeAsync(async () =>
|
|
{
|
|
await ChargerAsync();
|
|
StateHasChanged();
|
|
});
|
|
|
|
public void Dispose() => Reseau.Change -= SurChangementReseau;
|
|
|
|
protected override Task OnInitializedAsync() => ChargerAsync();
|
|
|
|
private async Task ChargerAsync() => _series = await Api.ListerSeriesAsync();
|
|
|
|
private async Task SurTouche(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_nouveau) && Reseau.EnLigne)
|
|
{
|
|
await CreerAsync();
|
|
}
|
|
}
|
|
|
|
private async Task CreerAsync()
|
|
{
|
|
_enCours = true;
|
|
_erreur = null;
|
|
|
|
var resultat = await Api.CreerSerieAsync(new EnregistrementSerie { Titre = _nouveau });
|
|
|
|
_enCours = false;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
return;
|
|
}
|
|
|
|
_nouveau = string.Empty;
|
|
await ChargerAsync();
|
|
}
|
|
}
|