@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)
{
@_erreur
}
@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.
}
@* 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.
*@
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.
*@
@*
⚠️ On SIGNALE, on ne supprime pas : la liste d'envies est
personnelle et le catalogue est commun, donc l'envie n'est
peut-être pas de celui qui a ajouté le livre — et la note
(« demandé à Noël ») disparaîtrait avec elle. Le rapprochement
est en outre faillible : l'étiquette mène à la fiche pour qu'on
puisse vérifier avant de retirer quoi que ce soit.
*@
@if (souhait is { Possede: true, LivreId: { } livreId })
{
Déjà au catalogue
}
@if (!string.IsNullOrWhiteSpace(souhait.Editeur))
{
@souhait.Editeur
}
@if (!string.IsNullOrWhiteSpace(souhait.Annee))
{
@souhait.Annee
}
@if (!string.IsNullOrWhiteSpace(souhait.Isbn))
{
ISBN @FormatageIsbn.Afficher(souhait.Isbn)
}
@if (!string.IsNullOrWhiteSpace(souhait.Note))
{
@souhait.Note
}
@if (!ModeOrdre)
{
}
}
}
}
@* 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();
}
}
}