@if (_serie.Elements.Count == 0)
{
Aucun tome recensé. Ajoutez ceux que vous possédez, et ceux qui vous manquent.
}
else
{
@_serie.NombrePossedes sur @_serie.Elements.Count tomes dans la bibliothèque.
}
@if (_erreur is not null)
{
@_erreur
}
@* La bascule vers les envies ne change rien à l'écran : sans un mot, on ne saurait pas
qu'elle a eu lieu, et on cliquerait deux fois. *@
@if (_message is not null)
{
@_message
}
@* Les sous-séries d'un cycle : on les liste, on ne les déplie pas — chacune a son écran. *@
@if (_filles.Count > 0)
{
Séries de ce cycle
@foreach (var fille in _filles)
{
@fille.Titre
@fille.NombrePossedes sur @fille.Elements.Count
}
}
@if (_serie.Elements.Count > 0)
{
Ordre de lecture
@*
⚠️ L'ordre est STOCKÉ, jamais déduit d'une année de publication : une préquelle se
lit avant le livre paru dix ans plus tôt. Les flèches sont le seul moyen de le
changer — le glisser-déposer HTML5 ne fonctionne pas au doigt, et c'est sur
téléphone qu'on consulte une saga en librairie.
*@
@if (!element.Possede)
{
@* Dire ce qui manque, et non laisser deviner par l'absence de lien. *@
Pas dans la bibliothèque
}
@if (element.PreteA is { } emprunteur)
{
Prêté à @emprunteur
}
@if (!element.Possede)
{
@* Le tome manquant est COMMUN au foyer, l'envie qu'il devient est
PERSONNELLE : deux membres peuvent vouloir le même tome. *@
}
else
{
}
@if (_rattachement == element.Id)
{
@foreach (var livre in _resultats)
{
}
}
}
}
Ajouter un tome
@*
Deux voies, parce qu'une saga se recense d'un coup — souvent avant d'en posséder la
moitié — puis se remplit au fil des achats. Un tome saisi sans livre N'EST PAS un
brouillon : c'est ce qui rend le trou visible.
*@
@if (_rattachement == AjoutDepuisCatalogue)
{
@foreach (var livre in _resultats)
{
}
}
La série
@if (_confirmeSuppression)
{
Les livres restent au catalogue ; seules les places disparaissent.
@if (_filles.Count > 0)
{
Les @_filles.Count série(s) de ce cycle deviendront des séries de premier niveau.
}
}
}
@code {
/// Marqueur de la recherche « ajouter depuis le catalogue », qui ne vise aucun élément.
private const int AjoutDepuisCatalogue = -1;
[Parameter] public int Id { get; set; }
private IReadOnlyList _toutes = [];
private SerieDto? _serie;
private SerieDto? _parente;
private List _filles = [];
private bool _chargement = true;
private bool _enCours;
private string? _erreur;
private string _titre = string.Empty;
private int _parenteId;
private bool _confirmeSuppression;
private string _nouveauTome = string.Empty;
private int? _rattachement;
private string _recherche = string.Empty;
private IReadOnlyList _resultats = [];
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
///
/// Séries dans lesquelles celle-ci peut être rangée.
///
///
/// Ni elle-même, ni ses descendantes : l'arbre se refermerait sur lui-même. Le serveur le
/// refuse aussi — cette liste évite seulement de proposer un choix qui sera rejeté.
///
private IEnumerable Candidates =>
_toutes.Where(s => s.Id != Id && !EstDescendante(s.Id));
private bool EstDescendante(int candidate)
{
var courante = _toutes.FirstOrDefault(s => s.Id == candidate);
var garde = _toutes.Count + 1;
while (courante is not null && garde-- > 0)
{
if (courante.Id == Id)
{
return true;
}
courante = courante.SerieParenteId is { } parenteId
? _toutes.FirstOrDefault(s => s.Id == parenteId)
: null;
}
return false;
}
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
private void SurChangementReseau() => _ = InvokeAsync(async () =>
{
await ChargerAsync();
StateHasChanged();
});
public void Dispose() => Reseau.Change -= SurChangementReseau;
protected override Task OnParametersSetAsync() => ChargerAsync();
private async Task ChargerAsync()
{
_chargement = true;
_toutes = await Api.ListerSeriesAsync();
_chargement = false;
Rattacher();
}
/// Recale les vues dérivées après un chargement ou une écriture.
private void Rattacher()
{
_serie = _toutes.FirstOrDefault(s => s.Id == Id);
if (_serie is null)
{
return;
}
_parente = _serie.SerieParenteId is { } parenteId
? _toutes.FirstOrDefault(s => s.Id == parenteId)
: null;
_filles = _toutes.Where(s => s.SerieParenteId == Id).OrderBy(s => s.Position).ToList();
_titre = _serie.Titre;
_parenteId = _serie.SerieParenteId ?? 0;
}
/// Remplace la série courante par celle que vient de rendre le serveur.
private void Appliquer(SerieDto serie)
{
_toutes = _toutes.Select(s => s.Id == serie.Id ? serie : s).ToList();
Rattacher();
}
private async Task SurToucheTome(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_nouveauTome) && Reseau.EnLigne)
{
await AjouterTomeAsync(null);
}
}
private async Task AjouterTomeAsync(int? livreId)
{
await EcrireAsync(() => Api.AjouterElementSerieAsync(
Id, new AjoutElementSerie { LivreId = livreId, Titre = livreId is null ? _nouveauTome : null }));
if (_erreur is null)
{
_nouveauTome = string.Empty;
FermerRecherche();
}
}
private Task RattacherAsync(int elementId, int? livreId) =>
EcrireAsync(async () =>
{
var resultat = await Api.RattacherLivreSerieAsync(elementId, livreId);
FermerRecherche();
return resultat;
});
private async Task RetirerAsync(ElementSerieDto element)
{
_enCours = true;
_erreur = await Api.RetirerElementSerieAsync(element.Id);
_enCours = false;
if (_erreur is null)
{
await ChargerAsync();
}
}
private async Task SouhaiterAsync(ElementSerieDto element)
{
_enCours = true;
_erreur = null;
var resultat = await Api.SouhaiterTomeAsync(element.Id);
_enCours = false;
_erreur = resultat.Erreur;
if (resultat.EstOk)
{
_message = $"« {element.Titre} » est dans votre liste d'envies.";
}
}
private string? _message;
private async Task DeplacerAsync(int depuis, int vers)
{
if (_serie is null || vers < 0 || vers >= _serie.Elements.Count)
{
return;
}
// Réordonné localement d'abord : la flèche doit répondre tout de suite, l'aller-retour
// réseau ne doit pas se voir. Un échec recharge l'ordre du serveur.
var ids = _serie.Elements.Select(e => e.Id).ToList();
(ids[depuis], ids[vers]) = (ids[vers], ids[depuis]);
await EcrireAsync(() => Api.ReordonnerSerieAsync(Id, ids));
}
private async Task EnregistrerAsync()
{
await EcrireAsync(() => Api.ModifierSerieAsync(
Id, new EnregistrementSerie { Titre = _titre, SerieParenteId = _parenteId == 0 ? null : _parenteId }));
// Le rangement dans un cycle change la parenté des AUTRES séries affichées : on relit.
if (_erreur is null)
{
await ChargerAsync();
}
}
private async Task SupprimerAsync()
{
if (!_confirmeSuppression)
{
_confirmeSuppression = true;
return;
}
_enCours = true;
_erreur = await Api.SupprimerSerieAsync(Id);
_enCours = false;
if (_erreur is null)
{
Navigation.NavigateTo("series");
}
}
private void OuvrirRattachement(int cible)
{
_rattachement = cible;
_recherche = string.Empty;
_resultats = [];
}
private void FermerRecherche()
{
_rattachement = null;
_recherche = string.Empty;
_resultats = [];
}
private async Task ChercherAsync(ChangeEventArgs e)
{
_recherche = e.Value?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(_recherche))
{
_resultats = [];
return;
}
// La recherche passe par le service habituel : hors-ligne elle interroge l'instantané,
// en ligne le serveur. Rien de particulier à prévoir ici.
_resultats = (await Api.ListerAsync(new CritereLivres { Recherche = _recherche }))
.Take(8)
.ToList();
}
/// Exécute une écriture qui rend la série, en tenant l'état d'attente et l'erreur.
private async Task EcrireAsync(Func>> appel)
{
_enCours = true;
_erreur = null;
_message = null;
var resultat = await appel();
_enCours = false;
if (!resultat.EstOk)
{
_erreur = resultat.Erreur;
// L'ordre affiché a pu être modifié localement avant l'appel : on reprend celui du
// serveur plutôt que de laisser croire à un enregistrement.
await ChargerAsync();
return;
}
Appliquer(resultat.Valeur!);
}
}