@inherits LayoutComponentBase @implements IDisposable @inject ServiceLivresApi Api @inject EtatReseau Reseau @inject NavigationManager Navigation @* 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. *@ @* ⚠️ Bandeau et menu sont dans un MÊME conteneur collant, et ce n'est pas cosmétique : sur PC la rangée du menu doit rester atteignable au défilement (lot R), et deux éléments collants superposés obligeraient à écrire en dur la hauteur du bandeau dans le `top` du second. *@
@* 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 occupent l'écran ENTIER au clic sur la bascule (lot R) — déployé sous le bandeau, le menu partageait l'écran avec la liste qu'on quittait, et l'on choisissait sa destination au milieu d'autre chose. ⚠️ Un calque plein écran sans porte de sortie est un piège : trois en sont offertes, en plus de la fermeture déjà en place sur LocationChanged — la croix, la touche Échap (le calque est focalisé à l'ouverture, sans quoi aucun keydown ne lui parviendrait, comme celui d'agrandissement des couvertures) et le clic hors des liens. Les entrées restent actives hors-ligne : les six écrans se consultent depuis leurs instantanés. Ce sont les écritures qui se désactivent, jamais 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; /// Le calque du menu doit prendre le focus au prochain rendu — sinon pas d'Échap. private bool _menuAFocaliser; private ElementReference _menu; 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); } /// /// Remonte d'un cran dans l'arborescence des écrans, en repliant le menu au passage. /// /// /// ⚠️ Renverse la décision du 2026-08-20 (« le retour passe par l'historique du /// navigateur, jamais par une destination calculée »). L'historique remonte aussi les /// allers-retours — filtre, ordre, édition — et l'on cliquait cinq fois sans quitter le même /// écran. La parenté est désormais explicite et testable : voir /// , qui porte aussi le garde-fou « ne jamais sortir de /// l'application » que history.length tenait mal dans une PWA standalone. /// private void Retour() { FermerMenu(); Navigation.NavigateTo(RemonteeRoutes.Parent(Navigation.ToBaseRelativePath(Navigation.Uri))); } private void BasculerMenu() { _menuOuvert = !_menuOuvert; // Le focus n'est demandé qu'à l'ouverture : c'est lui qui rend Échap opérant sur un // calque, et il n'y a rien à focaliser une fois le menu refermé. _menuAFocaliser = _menuOuvert; } /// /// Referme le menu. Posé sur le calque lui-même : un clic hors des liens ferme, et un clic /// SUR un lien ferme aussi — la navigation qui suit s'en chargerait de toute façon. /// private void FermerMenu() => _menuOuvert = false; private void SurToucheMenu(KeyboardEventArgs e) { if (e.Key is "Escape" or "Esc") { FermerMenu(); } } protected override async Task OnAfterRenderAsync(bool premierRendu) { if (_menuAFocaliser) { _menuAFocaliser = false; await _menu.FocusAsync(); } } public void Dispose() { Reseau.Change -= SurChangementReseau; Reseau.SynchroChange -= SurSynchro; Navigation.LocationChanged -= SurNavigation; } }