Files
mabibli/MaBibli.Client/Services/FiltreLivresLocal.cs
T
mathieu 42dec0dbd1 - Catalogage rapide (douchette, code-barres)
- Cataloguer en rafale — nouvel écran /ajout/rafale : on scanne une pile de livres à la suite dans une zone de texte, chaque code est traité (BnF puis OpenLibrary), les doublons connus sont passés automatiquement. La collecte marche même hors-ligne. Le compte rendu liste maintenant les livres créés, en lien vers leur fiche, et reste consultable en revenant sur l'écran même après une rafale entièrement réussie. Le catalogue reconnaît un ISBN dans sa barre de recherche (13 ou 10 chiffres, avec ou sans tirets) : scanner un livre en main ouvre directement sa fiche s'il n'y en a qu'un. Un bouton « Scanner » l'alimente, actif hors-ligne.
- Ajouter un tome à une série accepte aussi un ISBN dans le champ manuel : le catalogue est cherché d'abord (rattachement direct si un seul exemplaire), sinon la BnF prend le relais.
Séries et sagas
- Numéro de tome distinct de la position de lecture : on peut indiquer « c'est le tome 7 » même si on ne possède pas les six premiers ; l'ordre de lecture reste un réglage séparé (utile pour les préquelles).
- Tri par numéro en plus du tri par ordre de lecture quand des tomes en portent un.
- Panneau « Ajouter » regroupé et repliable sur la fiche d'une série (manuellement / en rafale / depuis le catalogue / depuis les envies), au lieu de quatre formulaires ouverts en permanence.
- Filtre catalogue « sans couverture » pour repérer les livres à illustrer.
Le catalogue groupe les tomes d'une même série sous un bloc repliable, avec un décompte plus clair (affichés / possédés / total).
- Corrections directes sur la fiche
- Effacer un prêt saisi par erreur (bouton ✕ sur chaque ligne, avec confirmation), sans passer par « rendre ».
- Corriger une couverture manquante ou cassée en cliquant dessus : le champ d'adresse s'ouvre focalisé, Entrée enregistre. Étendu aux numéros de revue dans la dernière modification.
- Les thèmes déjà utilisés dans la bibliothèque sont proposés à la frappe.
- La recherche d'un livre à rattacher montre des suggestions dès le focus, sans attendre de taper.

- Visuel : un rendu manquant après une écriture asynchrone dans le formulaire de livre, une bascule de rôle cassée, des débordements à 320 px, et le style d'un bouton-lien qui restait souligné.
2026-09-09 00:05:26 +02:00

111 lines
4.5 KiB
C#

using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Isbn;
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);
}
if (criteres.SansCouverture is { } sansCouverture)
{
// Même convention que côté serveur : une URL vide vaut une URL absente.
livres = livres.Where(l => string.IsNullOrEmpty(l.CoverUrl) == sansCouverture);
}
// Même exclusion que côté serveur : un code désigne un objet, des mots désignent une
// œuvre. Les deux branches ne se mélangent pas.
if (RechercheIsbn.Formes(criteres.Recherche) is { } formes)
{
livres = livres.Where(l =>
l.Isbn is not null
&& formes.Any(forme => l.Isbn.Contains(forme, StringComparison.Ordinal)));
return Ordonner(livres);
}
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)));
}
return Ordonner(livres);
}
/// <summary>
/// Même ordre que le serveur : titre normalisé, puis identifiant pour rendre le tri
/// déterministe.
/// </summary>
/// <remarks>
/// La comparaison est <b>ordinale</b> parce que c'est ce que fait SQLite sur une colonne sans
/// collation — un tri « intelligent » ici ferait diverger les deux vues.
/// </remarks>
private static IReadOnlyList<LivreDto> Ordonner(IEnumerable<LivreDto> livres) =>
livres
.OrderBy(l => NormalisationTexte.Normaliser(l.Titre), StringComparer.Ordinal)
.ThenBy(l => l.Id)
.ToList();
}