Le piege que CLAUDE.md signale : en Blazor WebAssembly le code tourne dans le navigateur alors que SQLite vit sur le serveur, et le service worker ne met en cache que les assets. Sans travail explicite, l'application demarre hors-ligne et affiche une bibliotheque vide. - js/cache-hors-ligne.js : instantanes JSON dans IndexedDB, et surveillance des bascules online/offline. Aucune logique metier. - CacheHorsLigne / EtatReseau : lecture-ecriture des instantanes, et etat reseau combinant navigator.onLine (fiable seulement par sa negation) avec le sort reel des appels HTTP. - ServiceLivresApi : les lectures retombent sur le cache, les ecritures sont refusees. Le catalogue entier est memorise, pas les reponses filtrees : c'est ce qui rend la recherche hors-ligne possible sur tout le fonds. - FiltreLivresLocal : pendant navigateur de FiltreLivres, avec un test qui confronte les deux implementations sur les memes donnees. - Interface : pastille et bandeau d'etat avec la date de synchronisation, et actions d'ecriture desactivees avec leur raison plutot que boutons morts. - js/mise-a-jour.js : les empreintes WASM etant desactivees, le service worker est le seul cache-busting du projet. L'enregistrement journalise desormais ses echecs, et un bandeau propose la nouvelle version sans attendre la fermeture de tous les onglets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
3.0 KiB
C#
72 lines
3.0 KiB
C#
using MaBibli.Shared.Catalogue;
|
|
using MaBibli.Shared.Dtos;
|
|
using MaBibli.Shared.Textes;
|
|
|
|
namespace MaBibli.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Recherche, filtrage et tri du catalogue <b>hors-ligne</b>, appliqués à l'instantané complet
|
|
/// rangé en IndexedDB.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// C'est le pendant navigateur de <see cref="FiltreLivres"/>, qui s'exécute côté base. Les deux
|
|
/// ne peuvent pas être le même code : le serveur travaille sur des entités aux colonnes
|
|
/// normalisées déjà calculées, le client sur des <see cref="LivreDto"/> qui n'en portent pas.
|
|
/// La normalisation est donc refaite ici à la volée — sans coût perceptible sur une bibliothèque
|
|
/// de foyer, et surtout par <b>les mêmes fonctions</b> (<see cref="NormalisationTexte"/>,
|
|
/// <see cref="RapprochementAuteurs.Cle"/>), ce qui est la seule garantie que « emile » trouve
|
|
/// « Émile » des deux côtés.
|
|
/// <para>
|
|
/// Un test compare les deux implémentations sur les mêmes données : une divergence silencieuse
|
|
/// donnerait une bibliothèque qui ne se cherche pas pareil selon qu'on a du réseau ou non.
|
|
/// </para>
|
|
/// <para>
|
|
/// Aucun filtre sur <c>AjoutePar</c>, ici comme côté serveur : la bibliothèque est commune.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class FiltreLivresLocal
|
|
{
|
|
public static IReadOnlyList<LivreDto> Appliquer(
|
|
IEnumerable<LivreDto> source, CritereLivres criteres)
|
|
{
|
|
var livres = source;
|
|
|
|
if (criteres.Format is { } format)
|
|
{
|
|
livres = livres.Where(l => l.Format == format);
|
|
}
|
|
|
|
if (criteres.Statut is { } statut)
|
|
{
|
|
// LivreDto.Statut est déjà celui de l'utilisateur courant : le serveur l'a résolu
|
|
// avant de l'envoyer. Le client n'a donc personne à désigner, et ne le pourrait pas.
|
|
livres = livres.Where(l => l.Statut == statut);
|
|
}
|
|
|
|
if (criteres.AuteurId is { } auteurId)
|
|
{
|
|
livres = livres.Where(l => l.Auteurs.Any(a => a.Id == auteurId));
|
|
}
|
|
|
|
var terme = NormalisationTexte.Normaliser(criteres.Recherche);
|
|
if (terme.Length > 0)
|
|
{
|
|
var cle = RapprochementAuteurs.Cle(criteres.Recherche);
|
|
|
|
livres = livres.Where(l =>
|
|
NormalisationTexte.Normaliser(l.Titre).Contains(terme, StringComparison.Ordinal)
|
|
|| l.Auteurs.Any(a =>
|
|
NormalisationTexte.Normaliser(a.Nom).Contains(terme, StringComparison.Ordinal)
|
|
|| RapprochementAuteurs.Cle(a.Nom).Contains(cle, StringComparison.Ordinal)));
|
|
}
|
|
|
|
// Même ordre que le serveur : titre normalisé, puis identifiant pour rendre le tri
|
|
// déterministe. La comparaison est ordinale parce que c'est ce que fait SQLite sur une
|
|
// colonne sans collation — un tri « intelligent » ici ferait diverger les deux vues.
|
|
return livres
|
|
.OrderBy(l => NormalisationTexte.Normaliser(l.Titre), StringComparer.Ordinal)
|
|
.ThenBy(l => l.Id)
|
|
.ToList();
|
|
}
|
|
}
|