@inherits LayoutComponentBase
@implements IDisposable
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@inject NavigationManager Navigation
@inject IJSRuntime JS
@*
Mise en page pensée mobile d'abord : un bandeau, une colonne, rien à gauche.
Le PC hérite de la même colonne, simplement centrée et limitée en largeur.
⚠️ La navigation est passée EN HAUT le 2026-08-20, à la demande de l'utilisateur, après
que la barre d'onglets du bas se soit affichée « toujours très mal » sur son téléphone.
Cela renverse la décision du 2026-08-18 (« en bas, le pouce atteint le bas de l'écran ») :
le raisonnement sur le pouce reste juste, mais il ne vaut rien face à une barre qui ne
s'affiche pas correctement. Le repli retenu est celui du gabarit Blazor par défaut — un
bouton bascule et une liste qui se déploie — parce qu'il ne dépend d'AUCUNE mise en page
exotique : sans la moindre feuille de style, il reste une suite de liens lisibles les uns
sous les autres, au lieu d'une rangée écrasée.
*@
@*
Les six destinations de l'application. Sur PC elles tiennent en une rangée sous le
bandeau ; sur téléphone elles se déploient en liste au clic sur la bascule.
Les entrées restent actives hors-ligne : les six écrans se consultent depuis leurs
instantanés. Ce sont les écritures qui se désactivent, pas la navigation.
*@
@if (!Reseau.EnLigne)
{
@*
Dire d'où viennent les données et de quand elles datent. Sans cette phrase, une
bibliothèque affichée hors-ligne est indiscernable d'une bibliothèque à jour — et un
livre ajouté depuis un autre appareil manquerait sans explication.
*@
Hors ligne.
@(Reseau.DerniereSynchro is { } synchro
? $" Données enregistrées {Quand(synchro)}. "
: " Aucune donnée n'a encore pu être enregistrée sur cet appareil. ")
Consultation et recherche fonctionnent ; les modifications sont impossibles.
}
@Body
@code {
private UtilisateurCourant? _utilisateur;
private bool _etaitEnLigne = true;
private bool _synchroEnCours;
/// Menu déployé (téléphone). Sur PC la rangée est visible en permanence.
private bool _menuOuvert;
protected override async Task OnInitializedAsync()
{
Reseau.Change += SurChangementReseau;
Reseau.SynchroChange += SurSynchro;
// ⚠️ Sans cela, le menu resterait déployé par-dessus l'écran qu'on vient d'atteindre :
// NavLink ne referme rien de lui-même, et un clic sur « Auteurs » laisserait les six
// entrées empilées au-dessus de la liste des auteurs.
Navigation.LocationChanged += SurNavigation;
// Écoute des bascules online/offline avant tout appel : un démarrage hors-ligne doit
// aller directement au cache, sans attendre l'échec d'une requête.
await Reseau.DemarrerAsync();
_etaitEnLigne = Reseau.EnLigne;
_utilisateur = await Api.ObtenirUtilisateurAsync();
// Rafraîchit tout le fonds, pas seulement l'écran ouvert : c'est ce qui rend la
// bibliothèque entière consultable et cherchable après la coupure.
await SynchroniserAsync();
}
///
/// Au retour du réseau, on recharge : la bibliothèque a pu changer depuis un autre appareil,
/// et les actions d'écriture redeviennent disponibles dans la foulée.
///
private void SurChangementReseau()
{
var revenu = Reseau.EnLigne && !_etaitEnLigne;
_etaitEnLigne = Reseau.EnLigne;
_ = InvokeAsync(async () =>
{
StateHasChanged();
if (revenu)
{
_utilisateur = await Api.ObtenirUtilisateurAsync();
await SynchroniserAsync();
StateHasChanged();
}
});
}
private async Task SynchroniserAsync()
{
if (_synchroEnCours)
{
return;
}
_synchroEnCours = true;
try
{
await Api.SynchroniserAsync();
}
finally
{
_synchroEnCours = false;
}
}
/// Date de synchronisation en clair : l'heure suffit le jour même.
private static string Quand(DateTimeOffset instant)
{
var local = instant.ToLocalTime();
return local.Date == DateTimeOffset.Now.Date
? $"aujourd'hui à {local:HH:mm}"
: $"le {local:dd/MM/yyyy} à {local:HH:mm}";
}
/// La date affichée vient de changer : rien à recharger, juste à redessiner.
private void SurSynchro() => _ = InvokeAsync(StateHasChanged);
private void SurNavigation(object? _, LocationChangedEventArgs __)
{
if (!_menuOuvert)
{
return;
}
_menuOuvert = false;
_ = InvokeAsync(StateHasChanged);
}
///
/// Revient à l'écran précédent, en repliant le menu au passage.
///
///
/// Le retour passe par l'historique du navigateur, pas par une destination calculée :
/// c'est le seul moyen de ramener là d'où l'on vient réellement, un même écran étant
/// atteignable par plusieurs chemins (une fiche livre s'ouvre depuis le catalogue, une
/// bibliographie, une série ou une recherche). Le repli sur le catalogue quand la pile est
/// vide est traité en JavaScript, history.length n'ayant pas d'équivalent côté C#.
///
private async Task RetourAsync()
{
_menuOuvert = false;
await JS.InvokeVoidAsync("mabibliRetour");
}
public void Dispose()
{
Reseau.Change -= SurChangementReseau;
Reseau.SynchroChange -= SurSynchro;
Navigation.LocationChanged -= SurNavigation;
}
}