@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. *@
@* Le déplacement entre écrans était jugé « foireux » : d'un écran profond (bibliographie, fiche de tome, ajout d'envie) il fallait deviner quel onglet ramenait en arrière. Un retour explicite, au même endroit sur tous les écrans, répond à la question sans obliger à connaître l'arborescence. *@ @* Pictogramme dessiné en ligne plutôt que chargé depuis /logo-bandeau.svg : un fichier séparé se sert (ou non) indépendamment de l'application, et c'est précisément ce qui produisait l'icône cassée en haut à gauche — un appareil dont le cache datait d'avant l'ajout du fichier recevait un 404. Un SVG en ligne ne peut pas manquer. *@ MaBibli @if (!Reseau.EnLigne) { @* Une pastille dans le bandeau, visible sur tous les écrans : l'état du réseau change l'usage de l'application, il ne doit pas se découvrir au premier clic qui échoue. *@ Hors ligne } @if (_utilisateur?.Identifiant is not null) { @_utilisateur.Affichage@(_utilisateur.Simule ? " (dev)" : "") }
@* 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; } }