@page "/souhaits" @page "/souhaits/ordre" @inject ServiceLivresApi Api @inject EtatReseau Reseau @inject NavigationManager Navigation @implements IDisposable MaBibli — liste d'envies

Liste d'envies

@* Le rappel n'est pas décoratif : la bibliothèque est commune, cette liste ne l'est pas. Sans ce mot, on pourrait croire que le foyer entier voit ce qu'on note ici. *@

Les livres que vous aimeriez acquérir. Cette liste est la vôtre : les autres membres du foyer ont la leur, et ne voient pas celle-ci. Ces livres ne sont pas dans le catalogue et ne comptent pas dans la bibliothèque.

@if (_erreur is not null) { } @if (_souhaits is null) {

Chargement…

} else { @if (_souhaits.Count == 0) { @* Hors-ligne, « vide » et « jamais enregistrée » sont indiscernables : une liste jamais synchronisée rendrait exactement le même résultat qu'une liste réellement vide. On dit donc les deux plutôt que d'affirmer la première — même règle que pour une fiche absente de l'instantané. *@

@if (Reseau.EnLigne) { Votre liste est vide. Ajoutez une envie ci-dessous, ou ouvrez la bibliographie d'un auteur depuis la liste des auteurs. } else { Aucune envie dans les données enregistrées sur cet appareil. Votre liste est peut-être vide, ou simplement pas encore synchronisée : le retour du réseau tranchera. }

} else {

@_souhaits.Count livre@(_souhaits.Count > 1 ? "s" : "") souhaité@(_souhaits.Count > 1 ? "s" : "")

@* Un seul bloc d'export, en haut de liste : c'est l'action qu'on vient chercher quand on part en librairie, elle ne doit pas être au bout d'un défilement. *@ @* ⚠️ L'export est produit PAR LE SERVEUR : hors-ligne il n'y a pas de fichier à télécharger, et un lien laissé actif finirait sur une page d'erreur du navigateur. Un lien ne se désactive pas — d'où la bascule vers un bouton inerte qui porte sa raison, comme au catalogue. La consolation est dite juste en dessous : l'usage que servait l'export « .txt », emporter sa liste en librairie, est désormais couvert par la liste elle-même. *@
@if (Reseau.EnLigne) { Exporter en texte Exporter en CSV } else { }
@if (!Reseau.EnLigne) {

Les exports sont fabriqués par le serveur : ils reviendront avec le réseau. La liste ci-dessous, elle, est celle enregistrée sur cet appareil — c'est exactement ce qu'on emportait dans le fichier texte.

} @if (ModeOrdre && _souhaits.Count > 1) {

Du plus désiré au moins désiré. Utilisez les flèches pour déplacer une envie — sur ordinateur, vous pouvez aussi la faire glisser.

} @* ⚠️ Les flèches ne sont PAS un ornement à côté du glisser-déposer : le drag & drop HTML5 ne fonctionne pas au doigt sur mobile. Sans elles, réordonner n'existerait que sur PC — c'est-à-dire pas là où l'on consulte sa liste en librairie. *@ } } @* L'ajout est passé dans son propre écran : la page ne montre plus que la liste, comme la fiche livre ne montre plus son formulaire. *@ @code { private IReadOnlyList? _souhaits; private bool _enCours; private int? _glisse; private string? _erreur; private bool ModeOrdre => Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/') .EndsWith("/souhaits/ordre", StringComparison.OrdinalIgnoreCase); protected override async Task OnInitializedAsync() { Reseau.Change += SurChangementReseau; Navigation.LocationChanged += SurNavigation; await ChargerAsync(); } private void SurNavigation(object? _, LocationChangedEventArgs __) => InvokeAsync(StateHasChanged); /// /// Recharge la liste au retour du réseau, et redessine pour rendre les actions à l'écran. /// /// /// On s'abonne à Change et jamais à SynchroChange : recharger écrit un /// instantané, qui émettrait une nouvelle synchro, qui rechargerait… sans fin. /// private void SurChangementReseau() => _ = InvokeAsync(async () => { await ChargerAsync(); StateHasChanged(); }); public void Dispose() { Reseau.Change -= SurChangementReseau; Navigation.LocationChanged -= SurNavigation; } /// Raison du blocage des écritures, ou null quand tout est possible. private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne; private const string MotifExport = "L'export est produit par le serveur : indisponible hors ligne."; /// /// ⚠️ Aucun message HTTP brut ne doit remonter jusqu'ici. C'était le défaut d'origine : /// hors-ligne, l'écran affichait « net_http_message_not_success_statuscode_reason, 404, /// Not Found ». La liste passe désormais par le même repli sur instantané que le reste de /// l'application, et une panne se dit en français. /// private async Task ChargerAsync() { try { _erreur = null; _souhaits = await Api.ListerSouhaitsAsync(); } catch (Exception) { _souhaits ??= []; _erreur = Reseau.EnLigne ? "La liste d'envies n'a pas pu être chargée. Réessayez dans un instant." : "Liste d'envies indisponible hors ligne : aucune donnée n'a encore pu être " + "enregistrée sur cet appareil."; } } /// /// Déplace une envie d'une position à une autre, puis enregistre l'ordre complet. /// /// /// La nouvelle suite est calculée localement et envoyée en entier : le serveur ne /// reçoit pas « monte de un » mais « voici l'ordre ». C'est la même opération pour les /// flèches et pour le glisser-déposer, donc un seul chemin à vérifier. /// /// La liste affichée est réordonnée avant l'aller-retour réseau : un déplacement qui /// attendrait la réponse donnerait l'impression que la flèche n'a pas répondu. En cas /// d'échec, le rechargement remet l'ordre du serveur. /// /// private async Task DeplacerAsync(int depuis, int vers) { if (_souhaits is null || depuis == vers || vers < 0 || vers >= _souhaits.Count) { return; } var ordonnee = _souhaits.ToList(); var deplacee = ordonnee[depuis]; ordonnee.RemoveAt(depuis); ordonnee.Insert(vers, deplacee); _souhaits = ordonnee; _enCours = true; _erreur = await Api.ReordonnerSouhaitsAsync(ordonnee.Select(s => s.Id).ToList()); _enCours = false; if (_erreur is not null) { // L'ordre affiché n'est pas celui du serveur : on reprend le sien plutôt que de // laisser croire que le déplacement a été enregistré. await ChargerAsync(); } } /// Fin d'un glisser-déposer : la carte saisie prend la place de celle survolée. private async Task DeposerAsync(int cible) { if (!ModeOrdre || _souhaits is null || _glisse is not { } id) { return; } var depuis = _souhaits.ToList().FindIndex(s => s.Id == id); _glisse = null; if (depuis >= 0) { await DeplacerAsync(depuis, cible); } } private void DebuterGlisse(int id) { if (ModeOrdre && Reseau.EnLigne) { _glisse = id; } } private void TerminerGlisse() => _glisse = null; private async Task SupprimerAsync(SouhaitDto souhait) { _enCours = true; _erreur = await Api.SupprimerSouhaitAsync(souhait.Id); _enCours = false; if (_erreur is null) { await ChargerAsync(); } } }