Files
mabibli/MaBibli.Shared/Entites/Serie.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

139 lines
6.0 KiB
C#

using MaBibli.Shared.Textes;
namespace MaBibli.Shared.Entites;
/// <summary>
/// Un regroupement ordonné de livres : trilogie, série, cycle, intégrale.
/// </summary>
/// <remarks>
/// C'est la première notion du projet qui porte sur une <b>relation entre livres</b> plutôt que
/// sur un livre. Trois choses en découlent, et un simple champ texte « série » sur
/// <see cref="Livre"/> n'en couvrirait aucune.
/// <para>
/// <b>1. Deux niveaux, au moins.</b> <i>La Légende de Drizzt</i> regroupe plusieurs trilogies.
/// D'où <see cref="SerieParenteId"/>, une auto-référence : un cycle <b>est</b> une série qui
/// contient des séries. Un champ « cycle » séparé figerait la profondeur à deux et obligerait à
/// tout refaire au troisième niveau.
/// </para>
/// <para>
/// <b>2. L'ordre de lecture n'est pas l'ordre de publication.</b> <i>L'Elfe noir</i> est une
/// préquelle écrite après. C'est précisément l'information qu'on vient chercher : elle est
/// <b>stockée</b> (<see cref="ElementSerie.Position"/>), jamais déduite d'une année.
/// </para>
/// <para>
/// <b>3. La portée est COMMUNE au foyer</b>, comme le catalogue et les prêts, contrairement au
/// statut de lecture et à la liste d'envies. L'ordre de lecture d'une saga est une propriété de
/// l'œuvre : il ne change pas selon qui regarde. <see cref="AjoutePar"/> est donc une trace,
/// pas une frontière — <b>ne jamais filtrer dessus</b>, comme <see cref="Livre.AjoutePar"/>.
/// </para>
/// </remarks>
public class Serie
{
public int Id { get; set; }
public string Titre { get; set; } = string.Empty;
/// <summary>
/// Titre mis à plat. Porte un index <b>unique</b> : une série, une fiche.
/// </summary>
/// <remarks>
/// L'unicité est globale et non « par série parente », parce que
/// <see cref="SerieParenteId"/> est nullable et que SQLite tient deux <c>NULL</c> pour
/// distincts : une unicité incluant le parent laisserait passer autant de doublons qu'on
/// veut à la racine — exactement le piège documenté sur <c>LivreSouhaite.AuteurNormalise</c>.
/// </remarks>
public string TitreNormalise { get; set; } = string.Empty;
/// <summary>La série qui contient celle-ci, quand c'en est une partie d'un cycle.</summary>
public int? SerieParenteId { get; set; }
public Serie? SerieParente { get; set; }
public List<Serie> SousSeries { get; set; } = [];
/// <summary>Rang de cette série dans sa série parente. Sans objet à la racine.</summary>
public int Position { get; set; }
public List<ElementSerie> Elements { get; set; } = [];
public DateTime DateAjout { get; set; }
/// <summary>Qui a créé la série. <b>Trace, pas frontière</b> : la série est commune.</summary>
public string? AjoutePar { get; set; }
public void RecalculerFormes()
{
Titre = Titre.Trim();
TitreNormalise = NormalisationTexte.Normaliser(Titre);
}
}
/// <summary>
/// Une <b>position</b> dans une série : le tome n, qu'on le possède ou non.
/// </summary>
/// <remarks>
/// ⚠️ <b>Ce n'est pas « un livre de la série » mais une place dans l'ordre de lecture.</b> La
/// nuance est tout l'intérêt de l'écran : sans position sans livre, on ne pourrait pas montrer
/// les trous — « il vous manque le tome 3 » —, qui est la demande d'origine. Aucune source ne
/// donne l'ordre de lecture d'une saga (la BnF ne l'expose pas, et l'ordre de publication est
/// justement faux pour une préquelle) : les tomes absents n'existent que si on les saisit.
/// <para>
/// <see cref="Titre"/> est donc <b>toujours renseigné</b>, y compris quand
/// <see cref="LivreId"/> l'est : c'est ce qui permet à la suppression d'un livre de laisser un
/// trou nommé plutôt qu'une ligne muette. La clé étrangère est en <c>SetNull</c> pour cette
/// raison précise — supprimer un livre ne doit pas trouer la structure de la saga.
/// </para>
/// </remarks>
public class ElementSerie
{
public int Id { get; set; }
public int SerieId { get; set; }
public Serie? Serie { get; set; }
/// <summary>Rang dans l'ordre de <b>lecture</b>, à partir de 0.</summary>
public int Position { get; set; }
/// <summary>Le livre du catalogue, s'il est possédé. <c>null</c> = un trou.</summary>
public int? LivreId { get; set; }
public Livre? Livre { get; set; }
/// <summary>
/// Titre du tome. Renseigné même quand le livre est là, pour survivre à sa suppression.
/// </summary>
public string Titre { get; set; } = string.Empty;
/// <summary>
/// Le numéro imprimé sur le livre — « 7 », « Hors-série », « 3.5 ». <c>null</c> = inconnu.
/// </summary>
/// <remarks>
/// ⚠️ <b>À ne pas confondre avec <see cref="Position"/>.</b> La position est le rang dans
/// l'ordre de <b>lecture</b> ; le numéro est l'étiquette <b>éditoriale</b>. Les deux
/// coïncident tant qu'on possède la série depuis le début, et divergent <b>par nature</b>
/// pour une préquelle : dans <i>La Légende de Drizzt</i>, le premier livre à lire porte
/// « 4 » sur sa couverture.
/// <para>
/// ⚠️ C'est aussi pourquoi le numéro ne pouvait pas se loger dans <c>Position</c> :
/// <c>PUT /api/series/{id}/ordre</c> renumérote toutes les positions, donc un numéro rangé
/// là serait détruit au premier réordonnancement par flèches.
/// </para>
/// <para>
/// <b>Une chaîne, jamais un entier</b> — même choix que <c>NumeroRevue.Numero</c> : les
/// sagas produisent des « hors-série », des « 3.5 » et des « intégrale 1-3 » qu'un
/// numérique refuserait.
/// </para>
/// </remarks>
public string? Numero { get; set; }
public void RecalculerFormes()
{
Titre = Titre.Trim();
// Une chaîne vide vaut une absence de numéro : sans cela, un champ effacé se
// distinguerait d'un champ jamais rempli, pour rien.
Numero = string.IsNullOrWhiteSpace(Numero) ? null : Numero.Trim();
}
}