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>
123 lines
5.1 KiB
C#
123 lines
5.1 KiB
C#
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;
|
|
}
|
|
}
|