142 lines
6.1 KiB
C#
142 lines
6.1 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; }
|
|
|
|
/// <summary>État de progression de la série (en cours, terminée, abandonnée).</summary>
|
|
public EtatSerie Etat { get; set; } = EtatSerie.EnCours;
|
|
|
|
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();
|
|
}
|
|
}
|