- 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é.
175 lines
7.0 KiB
C#
175 lines
7.0 KiB
C#
namespace MaBibli.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Parenté des routes de l'application : à quel écran remonte la flèche de retour du bandeau.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// ⚠️ <b>Ceci renverse la décision actée le 2026-08-20</b> (« le retour passe par l'historique du
|
|
/// navigateur, jamais par une destination calculée »). Le motif d'alors reste vrai — un même
|
|
/// écran s'atteint par plusieurs chemins — mais l'historique remonte aussi les
|
|
/// <b>allers-retours</b> (filtre, ordre, édition) : on cliquait cinq fois sans quitter le même
|
|
/// écran. Une remontée d'un cran de route répond à « où suis-je ? », qui est la question posée.
|
|
/// </para>
|
|
/// <para>
|
|
/// La table est <b>explicite</b>, et non un découpage naïf de l'URL : toutes les routes n'ont pas
|
|
/// la forme d'une arborescence (<c>/souhaits/ordre</c> remonte à <c>/souhaits</c>, mais
|
|
/// <c>/auteurs/{id}/bibliographie</c> remonte à <c>/auteurs</c> — la fiche d'un auteur n'existe
|
|
/// pas). Une route qui apparaît dans l'application doit apparaître ici.
|
|
/// </para>
|
|
/// <para>
|
|
/// ⚠️ <b>Le retour ne sort jamais de l'application</b> : la fonction rend toujours un chemin
|
|
/// interne, et la racine d'une branche rend la destination de menu correspondante — le catalogue
|
|
/// en dernier ressort. C'est ce qui remplace le test de <c>history.length</c> qui vivait en
|
|
/// JavaScript, et qui ne disait pas ce qu'on croyait dans une PWA <c>standalone</c> : la pile
|
|
/// d'une session y contient aussi ce qui précède l'application.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class RemonteeRoutes
|
|
{
|
|
/// <summary>Le catalogue est la racine : son parent est lui-même, on ne remonte pas plus haut.</summary>
|
|
public const string Racine = "/";
|
|
|
|
/// <summary>
|
|
/// Table de parenté, dans l'ordre de lecture. <c>{id}</c> représente un segment numérique.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// La première ligne dont le modèle correspond gagne : les modèles les plus longs sont donc
|
|
/// écrits <b>avant</b> ceux dont ils sont un prolongement.
|
|
/// </remarks>
|
|
private static readonly (string Modele, string Parent)[] Parents =
|
|
[
|
|
// Fiche livre : l'édition retombe sur la consultation, la consultation sur le catalogue.
|
|
("/livres/{id}/edition", "/livres/{id}"),
|
|
("/livres/{id}", Racine),
|
|
|
|
// Séries : les deux écrans de modification retombent sur la consultation de la série.
|
|
("/series/{id}/edition", "/series/{id}"),
|
|
("/series/{id}/ordre", "/series/{id}"),
|
|
("/series/{id}", "/series"),
|
|
("/series", Racine),
|
|
|
|
// Envies : l'ordre, l'ajout et les deux écrans d'édition sont des écrans de la même
|
|
// liste. ⚠️ « /souhaits/revues/{id}/edition » ne peut pas se confondre avec
|
|
// « /souhaits/{id}/edition » : les modèles n'ont pas le même nombre de segments.
|
|
("/souhaits/revues/{id}/edition", "/souhaits"),
|
|
("/souhaits/{id}/edition", "/souhaits"),
|
|
("/souhaits/ordre", "/souhaits"),
|
|
("/souhaits/ajout", "/souhaits"),
|
|
("/souhaits", Racine),
|
|
|
|
// Ajout d'un ouvrage : on y entre depuis le catalogue comme depuis les revues, mais le
|
|
// catalogue est la destination de menu de ce qu'on y saisit.
|
|
("/ajout/manuel", Racine),
|
|
("/ajout/isbn", Racine),
|
|
|
|
// ⚠️ La rafale remonte à « /ajout », et non au catalogue : on y arrive depuis l'écran
|
|
// d'ajout, et l'on veut souvent y revenir pour saisir autrement ce qui a résisté.
|
|
("/ajout/rafale", "/ajout"),
|
|
("/ajout", Racine),
|
|
|
|
// Revues. ⚠️ « /revues/ajout » est écrit AVANT « /revues/{id} » : « ajout » n'est pas un
|
|
// identifiant, mais l'ordre de lecture est ce qui le garantit sans ambiguïté.
|
|
("/revues/ajout", "/revues"),
|
|
("/revues/{id}/edition", "/revues/{id}"),
|
|
("/revues/{id}", "/revues"),
|
|
("/revues", Racine),
|
|
|
|
// La bibliographie remonte à la LISTE des auteurs : il n'existe pas de fiche auteur.
|
|
("/auteurs/{id}/bibliographie", "/auteurs"),
|
|
("/auteurs", Racine),
|
|
|
|
("/prets", Racine),
|
|
|
|
// « À propos » est une annexe du menu : elle remonte au catalogue, comme toute racine de
|
|
// branche. Elle n'a pas d'enfants, et n'en aura pas.
|
|
("/a-propos", Racine),
|
|
("/not-found", Racine),
|
|
];
|
|
|
|
/// <summary>
|
|
/// Écran d'où l'on vient hiérarchiquement, à partir d'un chemin d'application.
|
|
/// </summary>
|
|
/// <param name="chemin">
|
|
/// Chemin absolu ou relatif à la base, avec ou sans requête (<c>?auteur=3</c>) ni fragment.
|
|
/// </param>
|
|
/// <returns>Un chemin interne, toujours ; jamais <c>null</c>, jamais une URL absolue.</returns>
|
|
public static string Parent(string? chemin)
|
|
{
|
|
var segments = Segments(chemin);
|
|
|
|
if (segments.Length == 0)
|
|
{
|
|
return Racine;
|
|
}
|
|
|
|
foreach (var (modele, parent) in Parents)
|
|
{
|
|
if (Correspond(Segments(modele), segments))
|
|
{
|
|
return Rendre(parent, segments);
|
|
}
|
|
}
|
|
|
|
// Route inconnue : on retombe sur la destination de menu de sa branche, plutôt que sur
|
|
// un chemin deviné. Une route ajoutée sans sa ligne de table reste ainsi utilisable —
|
|
// mais elle doit être ajoutée : c'est un repli, pas le mécanisme.
|
|
return Parents.FirstOrDefault(p => p.Modele == $"/{segments[0]}").Parent is not null
|
|
? $"/{segments[0]}"
|
|
: Racine;
|
|
}
|
|
|
|
/// <summary>Découpe en segments non vides, requête et fragment retirés.</summary>
|
|
private static string[] Segments(string? chemin)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(chemin))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var utile = chemin.Split('?', '#')[0];
|
|
|
|
return utile.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
}
|
|
|
|
private static bool Correspond(string[] modele, string[] segments)
|
|
{
|
|
if (modele.Length != segments.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < modele.Length; i++)
|
|
{
|
|
var attendu = modele[i];
|
|
|
|
var ok = attendu == "{id}"
|
|
? int.TryParse(segments[i], out _)
|
|
: string.Equals(attendu, segments[i], StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!ok)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Remplace <c>{id}</c> du parent par l'identifiant lu dans le chemin d'origine.</summary>
|
|
private static string Rendre(string parent, string[] segments)
|
|
{
|
|
if (!parent.Contains("{id}", StringComparison.Ordinal))
|
|
{
|
|
return parent;
|
|
}
|
|
|
|
var identifiant = segments.FirstOrDefault(s => int.TryParse(s, out _));
|
|
|
|
// Sans identifiant lisible, on ne fabrique pas une route bancale : la racine est sûre.
|
|
return identifiant is null
|
|
? Racine
|
|
: parent.Replace("{id}", identifiant, StringComparison.Ordinal);
|
|
}
|
|
}
|