}
+ @* Un enregistrement impossible se dit AVANT le clic : un bouton qui échoue en silence est
+ pire que pas de bouton du tout (CLAUDE.md, stratégie hors-ligne). *@
+ @if (!string.IsNullOrEmpty(MessageBlocage))
+ {
+
@MessageBlocage
+ }
+
@@ -112,6 +120,15 @@
[Parameter] public bool EnCours { get; set; }
+ ///
+ /// Raison pour laquelle l'enregistrement est impossible (typiquement l'absence de réseau).
+ /// Renseignée, elle désactive la validation et s'affiche : la saisie reste lisible, mais
+ /// l'utilisateur sait pourquoi il ne peut pas valider.
+ ///
+ [Parameter] public string? MessageBlocage { get; set; }
+
+ private bool Bloque => !string.IsNullOrEmpty(MessageBlocage);
+
[Parameter] public EventCallback OnValider { get; set; }
[Parameter] public EventCallback OnAnnuler { get; set; }
diff --git a/MaBibli.Client/Composants/PretsLivre.razor b/MaBibli.Client/Composants/PretsLivre.razor
index e5dd8d8..c9e2dc5 100644
--- a/MaBibli.Client/Composants/PretsLivre.razor
+++ b/MaBibli.Client/Composants/PretsLivre.razor
@@ -1,4 +1,6 @@
@inject ServiceLivresApi Api
+@inject EtatReseau Reseau
+@implements IDisposable
@*
Prêts d'un livre : l'état courant, l'action qui s'impose, puis l'historique.
@@ -17,6 +19,38 @@
Un livre numérique ne se prête pas : il n'y a pas d'exemplaire à confier.
}
+ else if (!Reseau.EnLigne)
+ {
+ @*
+ L'historique des prêts n'est pas mis en cache : une requête par livre pour une
+ information rarement consultée. En revanche l'état COURANT vient de la fiche, donc
+ de l'instantané du catalogue — c'est lui qui répond à « où est ce livre ? », la
+ seule question qui se pose vraiment sans réseau, devant l'étagère.
+
+ Afficher une liste vide ferait croire à « jamais prêté » : on dit ce qu'on sait, et
+ on dit ce qu'on ignore.
+ *@
+ @if (PreteA is { } absent)
+ {
+
+ Prêté
+ à @absent
+ @if (PreteDepuis is { } depuis)
+ {
+ @($" depuis le {Jour(depuis)}")
+ }
+
+ }
+ else
+ {
+
Ce livre était à la maison à la dernière synchronisation.
+ }
+
+
+ Hors ligne : l'historique des prêts n'est pas consultable, et prêter ou marquer un
+ retour demande le réseau.
+
+ }
else if (_prets is null)
{
Chargement…
@@ -115,6 +149,15 @@
/// Format du livre : un ebook ne se prête pas.
[Parameter] public Format Format { get; set; }
+ ///
+ /// Emprunteur actuel d'après la fiche. Sert hors-ligne, où l'historique n'est pas
+ /// consultable mais où l'état courant, lui, vient de l'instantané du catalogue.
+ ///
+ [Parameter] public string? PreteA { get; set; }
+
+ /// Date du prêt en cours, même origine que .
+ [Parameter] public DateTime? PreteDepuis { get; set; }
+
/// Prévient la page parente qu'un prêt a changé, pour qu'elle rafraîchisse sa fiche.
[Parameter] public EventCallback OnChangement { get; set; }
@@ -131,6 +174,21 @@
private IReadOnlyList Historique =>
_prets?.Where(p => !p.EstEnCours).ToList() ?? [];
+ protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
+
+ /// Au retour du réseau, l'historique devient consultable : on va le chercher.
+ private void SurChangementReseau() => _ = InvokeAsync(async () =>
+ {
+ if (Reseau.EnLigne)
+ {
+ await ChargerAsync();
+ }
+
+ StateHasChanged();
+ });
+
+ public void Dispose() => Reseau.Change -= SurChangementReseau;
+
protected override async Task OnParametersSetAsync()
{
if (_livreCharge != LivreId)
@@ -143,6 +201,11 @@
private async Task ChargerAsync()
{
+ if (!Reseau.EnLigne)
+ {
+ return; // L'historique n'est pas en cache : rien à demander, l'affichage le dit.
+ }
+
try
{
_prets = await Api.ListerPretsLivreAsync(LivreId) ?? [];
diff --git a/MaBibli.Client/Layout/MainLayout.razor b/MaBibli.Client/Layout/MainLayout.razor
index d27de9c..f740d01 100644
--- a/MaBibli.Client/Layout/MainLayout.razor
+++ b/MaBibli.Client/Layout/MainLayout.razor
@@ -1,11 +1,20 @@
-@inherits LayoutComponentBase
+@inherits LayoutComponentBase
+@implements IDisposable
@inject ServiceLivresApi Api
+@inject EtatReseau Reseau
@* 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. *@
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)
{
@@ -14,13 +23,105 @@
}
+@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;
protected override async Task OnInitializedAsync()
- => _utilisateur = await Api.ObtenirUtilisateurAsync();
+ {
+ Reseau.Change += SurChangementReseau;
+ Reseau.SynchroChange += SurSynchro;
+
+ // É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);
+
+ public void Dispose()
+ {
+ Reseau.Change -= SurChangementReseau;
+ Reseau.SynchroChange -= SurSynchro;
+ }
}
diff --git a/MaBibli.Client/Layout/MainLayout.razor.css b/MaBibli.Client/Layout/MainLayout.razor.css
index 252cba0..be2e48e 100644
--- a/MaBibli.Client/Layout/MainLayout.razor.css
+++ b/MaBibli.Client/Layout/MainLayout.razor.css
@@ -27,6 +27,32 @@
max-width: 55%;
}
+/* L'état du réseau change ce que l'application permet : il se voit dans le bandeau, en
+ permanence, et pas seulement au moment où une action échoue. */
+.pastille-hors-ligne {
+ flex: 0 0 auto;
+ font-size: 0.75rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ padding: 0.15rem 0.5rem;
+ border-radius: 999px;
+ background: #f5d76e;
+ color: #4a3800;
+ white-space: nowrap;
+}
+
+/* Dire d'où viennent les données affichées, et de quand elles datent : sans cette phrase, un
+ catalogue hors-ligne est indiscernable d'un catalogue à jour. */
+.bandeau-reseau {
+ max-width: 46rem;
+ margin: 0 auto;
+ padding: 0.6rem 1rem;
+ background: #fff8e1;
+ border-bottom: 1px solid #e6d28a;
+ color: #6b5200;
+ font-size: 0.9rem;
+}
+
.contenu {
display: block;
width: 100%;
diff --git a/MaBibli.Client/Pages/AjoutIsbn.razor b/MaBibli.Client/Pages/AjoutIsbn.razor
index 114ae9f..bd85885 100644
--- a/MaBibli.Client/Pages/AjoutIsbn.razor
+++ b/MaBibli.Client/Pages/AjoutIsbn.razor
@@ -1,6 +1,8 @@
@page "/ajout/isbn"
@inject ServiceLivresApi Api
@inject NavigationManager Navigation
+@inject EtatReseau Reseau
+@implements IDisposable
MaBibli — ajouter par ISBN
@@ -12,6 +14,19 @@
Scannez le code-barres, ou saisissez l'ISBN imprimé sur le livre.
+ @*
+ Le scan comme le lookup interrogent la BnF puis OpenLibrary : ils EXIGENT le réseau,
+ et rien ne peut être ajouté au catalogue hors-ligne de toute façon. Le dire ici évite
+ une caméra ouverte pour rien, puis un délai d'attente incompréhensible.
+ *@
+ @if (!Reseau.EnLigne)
+ {
+
+ Hors ligne : la recherche par ISBN interroge la BnF et OpenLibrary, et l'ajout au
+ catalogue passe par le serveur. Les deux redeviendront possibles au retour du réseau.
+
+ }
+
@* La saisie manuelle reste le recours quand le code-barres est abîmé, absent,
ou que la caméra est indisponible : elle ne disparaît jamais derrière le scan. *@
-