MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace MaBibli.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// État du réseau, tel que l'interface doit le montrer, et date du dernier rafraîchissement
|
||||
/// des données mises en cache.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Deux sources, pas une.</b> <c>navigator.onLine</c> ne vaut que par sa négation : « faux »
|
||||
/// est fiable, « vrai » ne prouve rien (portail captif, serveur arrêté, Wi-Fi sans Internet).
|
||||
/// L'état affiché combine donc l'événement du navigateur et le sort réel des appels HTTP —
|
||||
/// un appel qui échoue bascule l'application en « hors ligne » sans attendre l'accord du
|
||||
/// navigateur, et le premier appel qui repasse la rebascule en ligne.
|
||||
/// <para>
|
||||
/// Ce service est le seul point d'observation de l'interface : la mise en page s'y abonne pour
|
||||
/// afficher le bandeau et la date de synchronisation, les écrans pour désactiver les actions
|
||||
/// d'écriture.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class EtatReseau(CacheHorsLigne cache) : IAsyncDisposable
|
||||
{
|
||||
private DotNetObjectReference<EtatReseau>? _reference;
|
||||
|
||||
private bool _navigateurEnLigne = true;
|
||||
private bool _dernierAppelEchoue;
|
||||
|
||||
/// <summary>Faux dès qu'un appel a échoué ou que le navigateur signale la perte du réseau.</summary>
|
||||
public bool EnLigne => _navigateurEnLigne && !_dernierAppelEchoue;
|
||||
|
||||
/// <summary>
|
||||
/// Faut-il tenter un appel réseau ? <b>Oui dès que le navigateur a une connexion</b>, même
|
||||
/// si le dernier appel a échoué.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// C'est ce qui permet de sortir tout seul du mode hors-ligne quand la panne venait du
|
||||
/// <b>serveur</b> et non de l'appareil : <c>navigator.onLine</c> n'a alors jamais changé, donc
|
||||
/// aucun événement <c>online</c> ne viendra jamais. Sans cette distinction, l'application
|
||||
/// resterait bloquée sur le cache jusqu'au rechargement de la page — constaté en essai.
|
||||
/// Le prix est d'une requête qui échoue par lecture tant que le serveur est absent : elle
|
||||
/// échoue vite, et l'affichage retombe sur le cache comme avant.
|
||||
/// </remarks>
|
||||
public bool TenterLeReseau => _navigateurEnLigne;
|
||||
|
||||
/// <summary>
|
||||
/// Instant du dernier rafraîchissement réussi des données en cache, <c>null</c> si l'on n'a
|
||||
/// jamais rien pu enregistrer sur cet appareil.
|
||||
/// </summary>
|
||||
public DateTimeOffset? DerniereSynchro { get; private set; }
|
||||
|
||||
/// <summary>Vrai quand ce qui est affiché vient du cache et non du réseau.</summary>
|
||||
public bool DonneesDuCache => !EnLigne && DerniereSynchro is not null;
|
||||
|
||||
/// <summary>Raison affichable du blocage des actions d'écriture.</summary>
|
||||
public const string MotifHorsLigne =
|
||||
"Hors ligne : la bibliothèque reste consultable, mais rien ne peut être modifié.";
|
||||
|
||||
/// <summary>
|
||||
/// Bascule en ligne ↔ hors ligne, et <b>elle seule</b>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// La date de synchronisation a son propre événement (<see cref="SynchroChange"/>) : sans
|
||||
/// cette séparation, un écran qui rechargerait ses données sur <c>Change</c> déclencherait
|
||||
/// l'écriture d'un instantané, donc un nouvel événement, donc un rechargement — une boucle
|
||||
/// sans fin.
|
||||
/// </remarks>
|
||||
public event Action? Change;
|
||||
|
||||
/// <summary>Nouvelle date de synchronisation : de quoi rafraîchir un affichage, rien de plus.</summary>
|
||||
public event Action? SynchroChange;
|
||||
|
||||
/// <summary>À appeler une fois au démarrage : lit l'état courant et s'abonne aux bascules.</summary>
|
||||
public async Task DemarrerAsync()
|
||||
{
|
||||
_reference ??= DotNetObjectReference.Create(this);
|
||||
var enLigne = await cache.SurveillerAsync(_reference);
|
||||
Definir(enLigne, false);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void SurChangementReseau(bool enLigne) => Definir(enLigne, _dernierAppelEchoue && !enLigne);
|
||||
|
||||
/// <summary>Un appel HTTP a échoué : on est hors ligne, quoi qu'en dise le navigateur.</summary>
|
||||
public void SignalerEchecReseau() => Definir(_navigateurEnLigne, true);
|
||||
|
||||
/// <summary>Un appel HTTP a abouti : la preuve la plus solide qu'on soit en ligne.</summary>
|
||||
public void SignalerSuccesReseau() => Definir(true, false);
|
||||
|
||||
/// <summary>Enregistre qu'un instantané vient d'être rangé, avec sa date.</summary>
|
||||
public void SignalerSynchro(DateTimeOffset date)
|
||||
{
|
||||
if (DerniereSynchro is { } precedente && precedente >= date)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DerniereSynchro = date;
|
||||
SynchroChange?.Invoke();
|
||||
}
|
||||
|
||||
private void Definir(bool navigateurEnLigne, bool dernierAppelEchoue)
|
||||
{
|
||||
var avant = EnLigne;
|
||||
|
||||
_navigateurEnLigne = navigateurEnLigne;
|
||||
_dernierAppelEchoue = dernierAppelEchoue;
|
||||
|
||||
// L'événement ne parle que de l'état VISIBLE : les écrans s'y abonnent pour se recharger,
|
||||
// et le déclencher pour un changement interne les ferait boucler.
|
||||
if (avant != EnLigne)
|
||||
{
|
||||
Change?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_reference?.Dispose();
|
||||
_reference = null;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user