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>
85 lines
3.5 KiB
C#
85 lines
3.5 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.TypeDocument is { } type)
|
|
{
|
|
livres = livres.Where(l => l.TypeDocument == type);
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
if (criteres.Prete is { } prete)
|
|
{
|
|
// LivreDto.PreteA porte l'emprunteur du prêt EN COURS, ou null : le serveur a déjà
|
|
// écarté les prêts clos. C'est exactement ce que le filtre serveur exprime par
|
|
// « une ligne sans date de retour », d'où l'équivalence des deux implémentations.
|
|
livres = livres.Where(l => (l.PreteA is not null) == prete);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|