- 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é.
This commit is contained in:
mathieu
2026-09-09 00:05:26 +02:00
parent bb071c3db9
commit 42dec0dbd1
43 changed files with 4154 additions and 189 deletions
+121
View File
@@ -0,0 +1,121 @@
namespace MaBibli.Shared.Textes;
/// <summary>
/// Compare des libellés qui contiennent des nombres, en lisant les nombres comme des nombres.
/// </summary>
/// <remarks>
/// <b>Le tri ordinaire range les tomes 1, 10, 11, 2, 9</b> — parce que le caractère <c>1</c>
/// précède le <c>2</c>. C'est correct alphabétiquement, et absurde pour une saga. Ce
/// comparateur découpe le libellé en blocs de chiffres et blocs de texte, et compare chaque
/// bloc de chiffres comme un entier.
/// <para>
/// ⚠️ <b>Aucune collation culturelle</b>, comme partout ailleurs dans le projet
/// (<c>GroupementCatalogue</c>, <c>FiltreLivresLocal</c>) : la comparaison des blocs de texte
/// est <b>ordinale</b>, sur leur forme normalisée. C'est ce qui garantit que deux appareils
/// classent une série dans le même ordre, quelle que soit la langue du système.
/// </para>
/// <para>
/// Ordre retenu, tranché avec l'utilisateur le 2026-08-22 :
/// </para>
/// <list type="number">
/// <item>le libellé <b>vide</b> — ce dont on ignore le numéro ;</item>
/// <item>ce qui <b>ne commence pas par un chiffre</b> (« Hors-série », « Intégrale 1-3 »),
/// entre eux de a à z ;</item>
/// <item>ce qui <b>commence par un chiffre</b>, de 0 à 9 en tri naturel.</item>
/// </list>
/// <para>
/// ⚠️ Cet ordre <b>diverge</b> de celui des numéros de revue, où ce qui n'a pas de date
/// <i>ferme</i> la liste. C'est délibéré : devant un kiosque on cherche le dernier paru,
/// devant une étagère on cherche ce qu'il reste à compléter. Ne pas uniformiser les deux au
/// nom de la cohérence — ce serait remplacer deux règles justes par une qui se trompe une
/// fois sur deux.
/// </para>
/// </remarks>
public static class TriNaturel
{
/// <summary>Comparateur prêt à l'emploi pour <c>OrderBy</c>.</summary>
public static IComparer<string?> Comparateur { get; } = new ComparateurNaturel();
/// <summary>
/// Compare deux libellés. <c>null</c> et la chaîne vide sont équivalents et passent en tête.
/// </summary>
public static int Comparer(string? gauche, string? droite)
{
var a = NormalisationTexte.Normaliser(gauche);
var b = NormalisationTexte.Normaliser(droite);
// 1. Le vide d'abord — on ignore le numéro, et c'est ce qui demande une décision.
if (a.Length == 0 || b.Length == 0)
{
return a.Length == b.Length ? 0 : a.Length == 0 ? -1 : 1;
}
// 2. Puis ce qui ne commence pas par un chiffre. Un seul test sur le PREMIER caractère :
// « Intégrale 1-3 » est du texte, « 3.5 » est un nombre, et la frontière est là.
var aChiffre = char.IsAsciiDigit(a[0]);
var bChiffre = char.IsAsciiDigit(b[0]);
if (aChiffre != bChiffre)
{
return aChiffre ? 1 : -1;
}
return ComparerBlocs(a, b);
}
/// <summary>
/// Parcourt les deux libellés en parallèle, bloc de chiffres contre bloc de chiffres,
/// bloc de texte contre bloc de texte.
/// </summary>
private static int ComparerBlocs(string a, string b)
{
int i = 0, j = 0;
while (i < a.Length && j < b.Length)
{
if (char.IsAsciiDigit(a[i]) && char.IsAsciiDigit(b[j]))
{
var debutA = i;
var debutB = j;
while (i < a.Length && char.IsAsciiDigit(a[i])) i++;
while (j < b.Length && char.IsAsciiDigit(b[j])) j++;
// ⚠️ Comparaison par LONGUEUR puis par caractères, jamais par conversion en int :
// un numéro n'est pas borné par int.MaxValue, et « 000000000000000012 » ne doit
// pas faire échouer le tri. Les zéros de tête sont ignorés d'abord.
var chiffresA = a.AsSpan(debutA, i - debutA).TrimStart('0');
var chiffresB = b.AsSpan(debutB, j - debutB).TrimStart('0');
if (chiffresA.Length != chiffresB.Length)
{
return chiffresA.Length < chiffresB.Length ? -1 : 1;
}
var ecart = chiffresA.SequenceCompareTo(chiffresB);
if (ecart != 0)
{
return Math.Sign(ecart);
}
}
else
{
var ecart = a[i].CompareTo(b[j]);
if (ecart != 0)
{
return Math.Sign(ecart);
}
i++;
j++;
}
}
// Le plus court d'abord quand l'un est le préfixe de l'autre : « 3 » avant « 3 bis ».
return (a.Length - i).CompareTo(b.Length - j);
}
private sealed class ComparateurNaturel : IComparer<string?>
{
public int Compare(string? x, string? y) => Comparer(x, y);
}
}