Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d371fa3a | ||
|
|
61d4fb1678 | ||
|
|
c5b4e117ba | ||
|
|
d76fb10ee0 |
@@ -214,13 +214,17 @@ public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
|
|||||||
souhait.Isbn = isbn;
|
souhait.Isbn = isbn;
|
||||||
souhait.Note = Vide(saisie.Note);
|
souhait.Note = Vide(saisie.Note);
|
||||||
|
|
||||||
// ⚠️ La couverture n'est écrite QUE si la charge utile en porte une, contrairement aux
|
// ⚠️ Écrite SEULEMENT quand le geste de correction a été utilisé (CoverUrlModifiee) :
|
||||||
// autres champs. Raison : aucun écran n'offre de champ « URL de couverture » pour une
|
// « Modifier une envie » enregistre aussi le titre, l'ISBN, etc. sans qu'on ait touché à
|
||||||
// envie — décision actée, un tel champ inviterait à coller des liens morts. Un
|
// la couverture, et cette valeur affichée peut être une DÉDUCTION à la lecture (formule
|
||||||
// remplacement inconditionnel effacerait donc, à chaque correction de titre, l'image
|
// OpenLibrary à ISBN connu, voir Projeter) plutôt que ce qu'il y a réellement en base.
|
||||||
// que la bibliographie avait fournie. Il n'y a rien à « vider » puisqu'il n'y a rien à
|
// La matérialiser à chaque enregistrement figerait cette déduction, et une correction
|
||||||
// saisir, et la lecture retombe de toute façon sur la formule OpenLibrary à ISBN connu.
|
// d'ISBN qui suit ne la referait plus jouer. Le drapeau distingue « je n'y ai pas
|
||||||
souhait.CoverUrl = Vide(saisie.CoverUrl) ?? souhait.CoverUrl;
|
// touché » de « je viens de vider le champ », ce qu'un simple null ne pourrait pas dire.
|
||||||
|
if (saisie.CoverUrlModifiee)
|
||||||
|
{
|
||||||
|
souhait.CoverUrl = Vide(saisie.CoverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// ⚠️ Indispensable : TitreNormalise est la CLÉ D'ŒUVRE, celle qui sert au rapprochement
|
// ⚠️ Indispensable : TitreNormalise est la CLÉ D'ŒUVRE, celle qui sert au rapprochement
|
||||||
// « déjà au catalogue ». Sans ce recalcul, corriger un titre laisserait le rapprochement
|
// « déjà au catalogue ». Sans ce recalcul, corriger un titre laisserait le rapprochement
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
@inject ServiceLivresApi Api
|
||||||
|
|
||||||
|
@*
|
||||||
|
Panneau de modification en lot, ouvert depuis la fiche série une fois des tomes cochés.
|
||||||
|
|
||||||
|
⚠️ Reprend le DESIGN de FormulaireLivre.razor (mêmes composants, même ordre de champs), mais
|
||||||
|
chaque champ est précédé d'une case « Modifier… » : sans elle, la valeur par défaut d'un champ
|
||||||
|
(« NonPrecise », « Physique »…) est déjà une vraie valeur, et l'appliquer à tous les livres
|
||||||
|
sélectionnés écraserait un champ qu'on ne voulait pas toucher (CLAUDE.md, invariant 10).
|
||||||
|
Un champ non coché ne s'affiche pas — c'est ce qui rend visible ce qui va être écrasé.
|
||||||
|
*@
|
||||||
|
<div class="formulaire">
|
||||||
|
<p class="message-avertissement" role="status">
|
||||||
|
Les champs cochés remplacent la valeur existante sur chacun des @NombreLivres livres
|
||||||
|
sélectionnés — pour les auteurs, une liste vide les retire tous.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<fieldset class="champ">
|
||||||
|
<legend class="champ-libelle">
|
||||||
|
<label class="bascule">
|
||||||
|
<input type="checkbox" @bind="Saisie.ModifierType" />
|
||||||
|
<span>Type</span>
|
||||||
|
</label>
|
||||||
|
</legend>
|
||||||
|
@if (Saisie.ModifierType)
|
||||||
|
{
|
||||||
|
<div class="segments">
|
||||||
|
@foreach (var type in Types)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="segment @Libelles.ClasseTypeDocument(type) @(Saisie.TypeDocument == type ? "segment-actif" : null)"
|
||||||
|
@onclick="() => Saisie.TypeDocument = type">
|
||||||
|
@Libelles.TypeDocument(type)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="champ">
|
||||||
|
<label class="bascule">
|
||||||
|
<input type="checkbox" @bind="Saisie.ModifierThemes" />
|
||||||
|
<span>Thèmes</span>
|
||||||
|
</label>
|
||||||
|
@if (Saisie.ModifierThemes)
|
||||||
|
{
|
||||||
|
<SaisieListe Valeurs="Saisie.Themes" Libelle="Thèmes" Aide=" (un par un : tapez, puis Entrée)"
|
||||||
|
Placeholder="dark fantasy" MessageDoublon="Ce thème est déjà dans la liste."
|
||||||
|
Suggestions="_themesConnus"
|
||||||
|
OnChangement="StateHasChanged" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="champ">
|
||||||
|
<label class="bascule">
|
||||||
|
<input type="checkbox" @bind="Saisie.ModifierAuteurs" />
|
||||||
|
<span>Auteurs</span>
|
||||||
|
</label>
|
||||||
|
@if (Saisie.ModifierAuteurs)
|
||||||
|
{
|
||||||
|
<SaisieListe Valeurs="Noms" Libelle="Auteurs" Aide=" (un par un : tapez, puis Entrée)"
|
||||||
|
Placeholder="Prénom Nom, ou Nom, Prénom"
|
||||||
|
MessageDoublon="Cet auteur est déjà dans la liste."
|
||||||
|
OnChangement="StateHasChanged">
|
||||||
|
<Complement Context="index">
|
||||||
|
@if (Saisie.Auteurs.Count > 1)
|
||||||
|
{
|
||||||
|
<div class="segments segments-role" role="group"
|
||||||
|
aria-label="@($"Rôle de {Saisie.Auteurs[index].Nom}")">
|
||||||
|
@foreach (var role in Roles)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="segment @(Saisie.Auteurs[index].Role == role ? "segment-actif" : null)"
|
||||||
|
@onclick="() => PoserRole(index, role)">
|
||||||
|
@Libelles.RoleAuteur(role)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Complement>
|
||||||
|
</SaisieListe>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="champ">
|
||||||
|
<legend class="champ-libelle">
|
||||||
|
<label class="bascule">
|
||||||
|
<input type="checkbox" @bind="Saisie.ModifierFormat" />
|
||||||
|
<span>Format</span>
|
||||||
|
</label>
|
||||||
|
</legend>
|
||||||
|
@if (Saisie.ModifierFormat)
|
||||||
|
{
|
||||||
|
<div class="segments">
|
||||||
|
@foreach (var format in Formats)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="segment @(Saisie.Format == format ? "segment-actif" : null)"
|
||||||
|
@onclick="() => Saisie.Format = format">
|
||||||
|
@Libelles.Format(format)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="champ">
|
||||||
|
<label class="bascule">
|
||||||
|
<input type="checkbox" @bind="Saisie.ModifierEditeur" />
|
||||||
|
<span>Éditeur</span>
|
||||||
|
</label>
|
||||||
|
@if (Saisie.ModifierEditeur)
|
||||||
|
{
|
||||||
|
<input class="champ-saisie" type="text" @bind="Saisie.Editeur" @bind:event="oninput"
|
||||||
|
autocomplete="off" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="champ">
|
||||||
|
<legend class="champ-libelle">
|
||||||
|
<label class="bascule">
|
||||||
|
<input type="checkbox" @bind="Saisie.ModifierStatut" />
|
||||||
|
<span>Statut de lecture <span class="champ-aide">(le vôtre)</span></span>
|
||||||
|
</label>
|
||||||
|
</legend>
|
||||||
|
@if (Saisie.ModifierStatut)
|
||||||
|
{
|
||||||
|
<ChoixStatut Valeur="Saisie.Statut" ValeurChanged="s => Saisie.Statut = s" />
|
||||||
|
}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Erreur))
|
||||||
|
{
|
||||||
|
<p class="message-erreur" role="alert">@Erreur</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(MessageBlocage))
|
||||||
|
{
|
||||||
|
<p class="message-avertissement" role="status">@MessageBlocage</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="actions-formulaire">
|
||||||
|
<button type="button" class="bouton bouton-principal"
|
||||||
|
disabled="@(EnCours || Bloque || Saisie.AucunChamp)"
|
||||||
|
title="@(Saisie.AucunChamp ? "Cochez au moins un champ à modifier." : MessageBlocage)"
|
||||||
|
@onclick="Valider">
|
||||||
|
@(EnCours ? "Enregistrement…" : LibelleValidation)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (OnAnnuler.HasDelegate)
|
||||||
|
{
|
||||||
|
<button type="button" class="bouton bouton-discret" disabled="@EnCours"
|
||||||
|
@onclick="OnAnnuler">
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private static readonly Format[] Formats = Enum.GetValues<Format>();
|
||||||
|
|
||||||
|
private static readonly TypeDocument[] Types = Enum.GetValues<TypeDocument>();
|
||||||
|
|
||||||
|
private static readonly RoleAuteur[] Roles = Enum.GetValues<RoleAuteur>();
|
||||||
|
|
||||||
|
private void PoserRole(int index, RoleAuteur role) =>
|
||||||
|
Saisie.Auteurs[index] = Saisie.Auteurs[index] with { Role = role };
|
||||||
|
|
||||||
|
/// <summary>Voir la remarque équivalente sur FormulaireLivre.razor : référence stable tant
|
||||||
|
/// que la saisie ne change pas, sans quoi SaisieListe se remet à zéro à chaque frappe.</summary>
|
||||||
|
private NomsAuteurs? _noms;
|
||||||
|
|
||||||
|
private NomsAuteurs Noms => _noms ??= new NomsAuteurs(Saisie.Auteurs);
|
||||||
|
|
||||||
|
private SaisieLot? _saisiePrecedente;
|
||||||
|
|
||||||
|
private IReadOnlyList<string> _themesConnus = [];
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_themesConnus =
|
||||||
|
[
|
||||||
|
.. (await Api.ListerAsync(new CritereLivres()))
|
||||||
|
.SelectMany(l => l.Themes)
|
||||||
|
.DistinctBy(NormalisationTexte.Normaliser, StringComparer.Ordinal)
|
||||||
|
.OrderBy(NormalisationTexte.Normaliser, StringComparer.Ordinal),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_themesConnus = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(_saisiePrecedente, Saisie))
|
||||||
|
{
|
||||||
|
_saisiePrecedente = Saisie;
|
||||||
|
_noms = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Parameter, EditorRequired] public SaisieLot Saisie { get; set; } = new();
|
||||||
|
|
||||||
|
[Parameter] public int NombreLivres { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string LibelleValidation { get; set; } = "Appliquer";
|
||||||
|
|
||||||
|
[Parameter] public string? Erreur { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public bool EnCours { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string? MessageBlocage { get; set; }
|
||||||
|
|
||||||
|
private bool Bloque => !string.IsNullOrEmpty(MessageBlocage);
|
||||||
|
|
||||||
|
[Parameter] public EventCallback OnValider { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public EventCallback OnAnnuler { get; set; }
|
||||||
|
|
||||||
|
private Task Valider() => OnValider.InvokeAsync();
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
Un bloc de série au catalogue, replié ou déplié — et récursif, parce qu'un cycle est une
|
Un bloc de série au catalogue, replié ou déplié — et récursif, parce qu'un cycle est une
|
||||||
série de séries : chaque niveau porte sa propre bascule.
|
série de séries : chaque niveau porte sa propre bascule.
|
||||||
|
|
||||||
⚠️ Tout est DÉPLIÉ par défaut, et l'on replie ce dont on ne veut pas. C'est la règle déjà
|
⚠️ Tout est REPLIÉ par défaut ici (2026-09-10), à l'inverse de l'arbre des séries : le
|
||||||
tenue par l'arbre des séries : l'inverse obligerait à ouvrir chaque bloc pour retrouver un
|
catalogue liste D'ABORD les livres, une série n'y est qu'un détail qu'on ouvre au besoin —
|
||||||
livre qu'on voyait très bien avant.
|
alors que l'écran des séries EST la liste des séries, où les masquer toutes viderait l'écran.
|
||||||
|
|
||||||
⚠️ Replié, le bloc annonce toujours ce qu'il contient : sans ce compte, le compteur du haut
|
⚠️ Replié, le bloc annonce toujours ce qu'il contient : sans ce compte, le compteur du haut
|
||||||
du catalogue paraîtrait mentir.
|
du catalogue paraîtrait mentir.
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<ul class="liste-livres">
|
<ul class="liste-livres">
|
||||||
@foreach (var sous in Entree.SousEntrees)
|
@foreach (var sous in Entree.SousEntrees)
|
||||||
{
|
{
|
||||||
<GroupeCatalogue Entree="sous" Replies="Replies" Totaux="Totaux" />
|
<GroupeCatalogue Entree="sous" Ouverts="Ouverts" Totaux="Totaux" />
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
@@ -66,15 +66,14 @@
|
|||||||
[Parameter, EditorRequired] public EntreeCatalogue Entree { get; set; } = default!;
|
[Parameter, EditorRequired] public EntreeCatalogue Entree { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Blocs REPLIÉS, par identifiant de série — et non dépliés : le défaut est de tout montrer.
|
/// Blocs OUVERTS, par identifiant de série — et non repliés : le défaut est de tout replier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// État local de l'écran, volontairement non persistant, comme celui de l'arbre des séries :
|
/// État local de l'écran, volontairement non persistant : c'est un confort de lecture, pas
|
||||||
/// c'est un confort de lecture, pas une propriété de la série. Il survit en revanche aux
|
/// une propriété de la série. Il survit en revanche aux rechargements du catalogue (frappe
|
||||||
/// rechargements du catalogue (frappe dans la recherche, filtre), le jeu appartenant à
|
/// dans la recherche, filtre), le jeu appartenant à l'écran et non au bloc.
|
||||||
/// l'écran et non au bloc.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Parameter, EditorRequired] public ISet<int> Replies { get; set; } = new HashSet<int>();
|
[Parameter, EditorRequired] public ISet<int> Ouverts { get; set; } = new HashSet<int>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compteurs par série, descendance comprise : ce qu'on possède, et ce que la série recense.
|
/// Compteurs par série, descendance comprise : ce qu'on possède, et ce que la série recense.
|
||||||
@@ -95,13 +94,13 @@
|
|||||||
|
|
||||||
private int Total => Compte.Total;
|
private int Total => Compte.Total;
|
||||||
|
|
||||||
private bool Deplie => !Replies.Contains(Entree.Serie!.Id);
|
private bool Deplie => Ouverts.Contains(Entree.Serie!.Id);
|
||||||
|
|
||||||
private void Basculer()
|
private void Basculer()
|
||||||
{
|
{
|
||||||
if (!Replies.Add(Entree.Serie!.Id))
|
if (!Ouverts.Add(Entree.Serie!.Id))
|
||||||
{
|
{
|
||||||
Replies.Remove(Entree.Serie!.Id);
|
Ouverts.Remove(Entree.Serie!.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using MaBibli.Shared.Dtos;
|
||||||
|
using MaBibli.Shared.Entites;
|
||||||
|
|
||||||
|
namespace MaBibli.Client.Composants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saisie du panneau « Modifier en lot » (fiche série).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// ⚠️ Chaque champ porte sa propre case <c>Modifier…</c> : sans elle, la valeur par défaut d'un
|
||||||
|
/// champ (« NonPrecise », « Physique »…) est déjà une vraie valeur, et l'appliquer à tous les
|
||||||
|
/// livres sélectionnés écraserait un champ qu'on ne voulait pas toucher. Seuls les champs cochés
|
||||||
|
/// sont envoyés ; les autres livres restent inchangés sur les champs non cochés (CLAUDE.md,
|
||||||
|
/// invariant 10 : on n'affiche — et n'applique — que ce qui a été choisi).
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class SaisieLot
|
||||||
|
{
|
||||||
|
public bool ModifierType { get; set; }
|
||||||
|
|
||||||
|
public TypeDocument TypeDocument { get; set; }
|
||||||
|
|
||||||
|
public bool ModifierThemes { get; set; }
|
||||||
|
|
||||||
|
public List<string> Themes { get; set; } = [];
|
||||||
|
|
||||||
|
public bool ModifierAuteurs { get; set; }
|
||||||
|
|
||||||
|
public List<AuteurSaisi> Auteurs { get; set; } = [];
|
||||||
|
|
||||||
|
public bool ModifierFormat { get; set; }
|
||||||
|
|
||||||
|
public Format Format { get; set; }
|
||||||
|
|
||||||
|
public bool ModifierEditeur { get; set; }
|
||||||
|
|
||||||
|
public string? Editeur { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statut de lecture <b>de l'appelant</b>, comme partout ailleurs : appliquer un statut en
|
||||||
|
/// lot pose la lecture de la personne qui agit, jamais celle d'un tiers.
|
||||||
|
/// </summary>
|
||||||
|
public bool ModifierStatut { get; set; }
|
||||||
|
|
||||||
|
public Statut? Statut { get; set; }
|
||||||
|
|
||||||
|
public bool AucunChamp =>
|
||||||
|
!ModifierType && !ModifierThemes && !ModifierAuteurs
|
||||||
|
&& !ModifierFormat && !ModifierEditeur && !ModifierStatut;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
@inject ServiceLivresApi Api
|
@inject ServiceLivresApi Api
|
||||||
@inject EtatReseau Reseau
|
@inject EtatReseau Reseau
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject CacheHorsLigne Cache
|
||||||
|
|
||||||
@*
|
@*
|
||||||
Mise en page pensée mobile d'abord : un bandeau, une colonne, rien à gauche.
|
Mise en page pensée mobile d'abord : un bandeau, une colonne, rien à gauche.
|
||||||
@@ -93,6 +94,16 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@*
|
||||||
|
Une rafale non vidée reste consultable ailleurs sur le site — sans quoi y revenir
|
||||||
|
depuis un autre écran demande de refaire tout le chemin d'« Ajouter ». L'entrée
|
||||||
|
disparaît d'elle-même une fois la file effacée (bouton « Nouvelle rafale »).
|
||||||
|
*@
|
||||||
|
@if (_rafale is { Total: > 0 })
|
||||||
|
{
|
||||||
|
<NavLink class="menu-lien menu-lien-rafale" href="ajout/rafale">@LibelleRafale</NavLink>
|
||||||
|
}
|
||||||
|
|
||||||
<NavLink class="menu-lien" href="" Match="NavLinkMatch.All">Catalogue</NavLink>
|
<NavLink class="menu-lien" href="" Match="NavLinkMatch.All">Catalogue</NavLink>
|
||||||
<NavLink class="menu-lien" href="auteurs">Auteurs</NavLink>
|
<NavLink class="menu-lien" href="auteurs">Auteurs</NavLink>
|
||||||
<NavLink class="menu-lien" href="series">Séries</NavLink>
|
<NavLink class="menu-lien" href="series">Séries</NavLink>
|
||||||
@@ -148,6 +159,12 @@
|
|||||||
|
|
||||||
private ElementReference _menu;
|
private ElementReference _menu;
|
||||||
|
|
||||||
|
/// <summary>Rafale non vidée, s'il y en a une. Relue à chaque navigation (lot AB).</summary>
|
||||||
|
private Rafale? _rafale;
|
||||||
|
|
||||||
|
private string LibelleRafale =>
|
||||||
|
_rafale is { Crees: > 0 } r ? $"Compte rendu ({r.Crees})" : "Rafale à reprendre";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Reseau.Change += SurChangementReseau;
|
Reseau.Change += SurChangementReseau;
|
||||||
@@ -158,6 +175,8 @@
|
|||||||
// entrées empilées au-dessus de la liste des auteurs.
|
// entrées empilées au-dessus de la liste des auteurs.
|
||||||
Navigation.LocationChanged += SurNavigation;
|
Navigation.LocationChanged += SurNavigation;
|
||||||
|
|
||||||
|
_rafale = await Cache.LireRafaleAsync();
|
||||||
|
|
||||||
// Écoute des bascules online/offline avant tout appel : un démarrage hors-ligne doit
|
// Écoute des bascules online/offline avant tout appel : un démarrage hors-ligne doit
|
||||||
// aller directement au cache, sans attendre l'échec d'une requête.
|
// aller directement au cache, sans attendre l'échec d'une requête.
|
||||||
await Reseau.DemarrerAsync();
|
await Reseau.DemarrerAsync();
|
||||||
@@ -226,13 +245,19 @@
|
|||||||
|
|
||||||
private void SurNavigation(object? _, LocationChangedEventArgs __)
|
private void SurNavigation(object? _, LocationChangedEventArgs __)
|
||||||
{
|
{
|
||||||
if (!_menuOuvert)
|
if (_menuOuvert)
|
||||||
{
|
{
|
||||||
return;
|
_menuOuvert = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_menuOuvert = false;
|
// ⚠️ Relu à CHAQUE navigation, pas seulement au quitter de l'écran de rafale : c'est ce
|
||||||
_ = InvokeAsync(StateHasChanged);
|
// qui fait apparaître l'entrée du menu dès le premier code scanné, et disparaître dès
|
||||||
|
// la file effacée, où qu'on soit rendu entre-temps.
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
_rafale = await Cache.LireRafaleAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -32,6 +32,14 @@
|
|||||||
Filtrer@(FiltresActifs > 0 ? $" ({FiltresActifs})" : "")
|
Filtrer@(FiltresActifs > 0 ? $" ({FiltresActifs})" : "")
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@if (_grouper && Entrees.Any(e => e.EstGroupe))
|
||||||
|
{
|
||||||
|
<button type="button" class="bouton bouton-discret bouton-compact"
|
||||||
|
@onclick="() => { if (AuMoinsUneSerieOuverte) { ToutReplier(); } else { ToutDeplier(); } }">
|
||||||
|
@(AuMoinsUneSerieOuverte ? "Replier les séries" : "Déplier les séries")
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
@if (Reseau.EnLigne)
|
@if (Reseau.EnLigne)
|
||||||
{
|
{
|
||||||
<a class="bouton bouton-principal bouton-compact" href="ajout">Ajouter un ouvrage</a>
|
<a class="bouton bouton-principal bouton-compact" href="ajout">Ajouter un ouvrage</a>
|
||||||
@@ -228,13 +236,18 @@ else if (_livres is not null)
|
|||||||
tomes occupait tout l'écran alors qu'on cherchait un livre rangé après lui. Replier ne
|
tomes occupait tout l'écran alors qu'on cherchait un livre rangé après lui. Replier ne
|
||||||
retire rien de la liste — le compteur du haut ne bouge pas, et le bloc replié dit
|
retire rien de la liste — le compteur du haut ne bouge pas, et le bloc replié dit
|
||||||
toujours ce qu'il contient.
|
toujours ce qu'il contient.
|
||||||
|
|
||||||
|
⚠️ Ici, contrairement à l'arbre des séries, tout est REPLIÉ par défaut (2026-09-10) :
|
||||||
|
le catalogue liste des livres, pas des séries — une série n'y est qu'un regroupement
|
||||||
|
qu'on ouvre au besoin. Le bouton qui bascule tout l'arbre est dans l'en-tête, à côté
|
||||||
|
de « Filtrer », pas ici : c'est un réglage d'affichage, pas une action sur la liste.
|
||||||
*@
|
*@
|
||||||
<ul class="liste-livres">
|
<ul class="liste-livres">
|
||||||
@foreach (var entree in Entrees)
|
@foreach (var entree in Entrees)
|
||||||
{
|
{
|
||||||
@if (entree.EstGroupe)
|
@if (entree.EstGroupe)
|
||||||
{
|
{
|
||||||
<GroupeCatalogue Entree="entree" Replies="_replies" Totaux="_totauxSeries" />
|
<GroupeCatalogue Entree="entree" Ouverts="_ouverts" Totaux="_totauxSeries" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -269,15 +282,45 @@ else if (_livres is not null)
|
|||||||
private bool _grouper = true;
|
private bool _grouper = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Blocs repliés à la main, par identifiant de série. Tout est déplié par défaut.
|
/// Blocs ouverts à la main, par identifiant de série. Tout est replié par défaut.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// ⚠️ Le jeu appartient à l'ÉCRAN, et non aux blocs : le catalogue se relit à chaque frappe
|
/// ⚠️ Le jeu appartient à l'ÉCRAN, et non aux blocs : le catalogue se relit à chaque frappe
|
||||||
/// dans la recherche, et un état porté par les composants se perdrait à chaque caractère.
|
/// dans la recherche, et un état porté par les composants se perdrait à chaque caractère.
|
||||||
/// Il n'est pas persistant d'une visite à l'autre pour autant — c'est un confort de
|
/// Il n'est pas persistant d'une visite à l'autre pour autant — c'est un confort de
|
||||||
/// lecture, pas une propriété de la série, même règle que l'arbre des séries.
|
/// lecture, pas une propriété de la série.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private readonly HashSet<int> _replies = [];
|
private readonly HashSet<int> _ouverts = [];
|
||||||
|
|
||||||
|
private bool AuMoinsUneSerieOuverte => _ouverts.Count > 0;
|
||||||
|
|
||||||
|
private void ToutReplier() => _ouverts.Clear();
|
||||||
|
|
||||||
|
private void ToutDeplier()
|
||||||
|
{
|
||||||
|
foreach (var id in IdentifiantsSeries(Entrees))
|
||||||
|
{
|
||||||
|
_ouverts.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<int> IdentifiantsSeries(IEnumerable<EntreeCatalogue> entrees)
|
||||||
|
{
|
||||||
|
foreach (var entree in entrees)
|
||||||
|
{
|
||||||
|
if (!entree.EstGroupe)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return entree.Serie!.Id;
|
||||||
|
|
||||||
|
foreach (var id in IdentifiantsSeries(entree.SousEntrees))
|
||||||
|
{
|
||||||
|
yield return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ce que chaque série possède et recense, descendance comprise.
|
/// Ce que chaque série possède et recense, descendance comprise.
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ else if (_saisie is null)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="formulaire">
|
<div class="formulaire">
|
||||||
|
@* Même ligne que dans le formulaire d'un ouvrage (FormulaireLivre) : un aperçu quand il y
|
||||||
|
a quelque chose à montrer, puis un champ d'adresse ordinaire, au même titre que les
|
||||||
|
autres. *@
|
||||||
|
@if (!string.IsNullOrEmpty(_saisie.CoverUrl) || !string.IsNullOrWhiteSpace(_saisie.Titre))
|
||||||
|
{
|
||||||
|
<div class="formulaire-apercu">
|
||||||
|
<Couverture Url="@_saisie.CoverUrl" Titre="@_saisie.Titre" Classe="couverture-moyenne" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<label class="champ">
|
<label class="champ">
|
||||||
<span class="champ-libelle">Titre <span class="obligatoire" aria-hidden="true">*</span></span>
|
<span class="champ-libelle">Titre <span class="obligatoire" aria-hidden="true">*</span></span>
|
||||||
<input class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput"
|
<input class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput"
|
||||||
@@ -73,6 +83,12 @@ else
|
|||||||
@bind="_saisie.Isbn" @bind:event="oninput" placeholder="978…" autocomplete="off" />
|
@bind="_saisie.Isbn" @bind:event="oninput" placeholder="978…" autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="champ">
|
||||||
|
<span class="champ-libelle">URL de couverture</span>
|
||||||
|
<input class="champ-saisie" type="url" @bind="_saisie.CoverUrl" @bind:event="oninput"
|
||||||
|
placeholder="https://…" autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="champ">
|
<label class="champ">
|
||||||
<span class="champ-libelle">Note</span>
|
<span class="champ-libelle">Note</span>
|
||||||
<input class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
|
<input class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
|
||||||
@@ -104,6 +120,13 @@ else
|
|||||||
private bool _enCours;
|
private bool _enCours;
|
||||||
private string? _erreur;
|
private string? _erreur;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La couverture telle que chargée — éventuellement déduite de l'ISBN (voir <c>Projeter</c>
|
||||||
|
/// côté serveur), pas forcément ce qu'il y a réellement en base. Sert uniquement à détecter,
|
||||||
|
/// à l'enregistrement, si l'utilisateur a touché au champ.
|
||||||
|
/// </summary>
|
||||||
|
private string? _couvertureChargee;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Reseau.Change += SurChangementReseau;
|
Reseau.Change += SurChangementReseau;
|
||||||
@@ -135,9 +158,8 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠️ La couverture n'est PAS reprise ici : elle n'a pas de champ de saisie, et le
|
_couvertureChargee = souhait.CoverUrl;
|
||||||
// service la conserve donc telle qu'elle est en base. La valeur portée par le DTO peut
|
|
||||||
// en outre être déduite de l'ISBN — la figer serait écrire en base ce qui se calcule.
|
|
||||||
_saisie = new EnregistrementSouhait
|
_saisie = new EnregistrementSouhait
|
||||||
{
|
{
|
||||||
Titre = souhait.Titre,
|
Titre = souhait.Titre,
|
||||||
@@ -145,6 +167,7 @@ else
|
|||||||
Editeur = souhait.Editeur,
|
Editeur = souhait.Editeur,
|
||||||
Annee = souhait.Annee,
|
Annee = souhait.Annee,
|
||||||
Isbn = souhait.Isbn,
|
Isbn = souhait.Isbn,
|
||||||
|
CoverUrl = souhait.CoverUrl,
|
||||||
Note = souhait.Note,
|
Note = souhait.Note,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -156,6 +179,16 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ Envoyée seulement si elle a changé depuis le chargement, et non à chaque
|
||||||
|
// enregistrement : la valeur affichée peut être une DÉDUCTION à la lecture (formule
|
||||||
|
// OpenLibrary à ISBN connu, voir `Projeter`) plutôt que ce qu'il y a réellement en base.
|
||||||
|
// La renvoyer telle quelle à chaque correction de titre la figerait en base, et une
|
||||||
|
// correction d'ISBN qui suit ne la referait plus jouer.
|
||||||
|
_saisie.CoverUrlModifiee = !string.Equals(
|
||||||
|
_saisie.CoverUrl?.Trim() ?? string.Empty,
|
||||||
|
_couvertureChargee?.Trim() ?? string.Empty,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
_enCours = true;
|
_enCours = true;
|
||||||
_erreur = null;
|
_erreur = null;
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,20 @@ else
|
|||||||
{
|
{
|
||||||
<a class="bouton bouton-compact bouton-discret" href="@($"series/{Id}/ordre")">Changer l'ordre</a>
|
<a class="bouton bouton-compact bouton-discret" href="@($"series/{Id}/ordre")">Changer l'ordre</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@*
|
||||||
|
« Modifier en lot » n'apparaît qu'à partir de DEUX tomes possédés : sur un
|
||||||
|
seul, l'écran de la fiche fait aussi bien. Même seuil que « Changer l'ordre ».
|
||||||
|
*@
|
||||||
|
@if (TomesModifiablesEnLot > 1)
|
||||||
|
{
|
||||||
|
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||||
|
aria-expanded="@Aria.Etat(_lotOuvert)" aria-controls="lot-tomes"
|
||||||
|
@onclick="BasculerLot">
|
||||||
|
Modifier en lot
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<a class="bouton bouton-compact bouton-principal" href="@($"series/{Id}/edition")">Modifier</a>
|
<a class="bouton bouton-compact bouton-principal" href="@($"series/{Id}/edition")">Modifier</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -332,6 +346,38 @@ else
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@*
|
||||||
|
Modification en lot : on coche des tomes possédés, puis on choisit ce qu'on leur
|
||||||
|
applique. Même patron que la sélection de la bibliographie (barre en haut, collante),
|
||||||
|
sauf que l'action n'est pas un simple bouton mais un formulaire — il reste donc
|
||||||
|
affiché sous la barre plutôt que de se déclencher au clic.
|
||||||
|
*@
|
||||||
|
@if (LotActif)
|
||||||
|
{
|
||||||
|
<div id="lot-tomes">
|
||||||
|
@if (_selectionLot.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="barre-selection" role="group" aria-label="Actions sur la sélection">
|
||||||
|
<span class="selection-compte">
|
||||||
|
@_selectionLot.Count sélectionné@(_selectionLot.Count > 1 ? "s" : "")
|
||||||
|
</span>
|
||||||
|
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||||
|
disabled="@_enCours" @onclick="() => _selectionLot.Clear()">
|
||||||
|
Tout décocher
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormulaireLotLivres Saisie="_saisieLot" NombreLivres="_selectionLot.Count"
|
||||||
|
Erreur="@_erreur" EnCours="_enCours" MessageBlocage="@MotifBlocage"
|
||||||
|
OnValider="EnregistrerLotAsync" OnAnnuler="AnnulerLot" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="message-discret">Cochez les tomes à modifier ensemble.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@*
|
@*
|
||||||
⚠️ L'ordre est STOCKÉ, jamais déduit d'une année de publication : une préquelle se
|
⚠️ L'ordre est STOCKÉ, jamais déduit d'une année de publication : une préquelle se
|
||||||
lit avant le livre paru dix ans plus tôt. Les flèches sont le seul moyen de le
|
lit avant le livre paru dix ans plus tôt. Les flèches sont le seul moyen de le
|
||||||
@@ -342,6 +388,17 @@ else
|
|||||||
@foreach (var (element, index) in TomesAffiches.Select((e, i) => (e, i)))
|
@foreach (var (element, index) in TomesAffiches.Select((e, i) => (e, i)))
|
||||||
{
|
{
|
||||||
<li class="carte-tome @(element.Possede ? null : "tome-manquant")" @key="element.Id">
|
<li class="carte-tome @(element.Possede ? null : "tome-manquant")" @key="element.Id">
|
||||||
|
@if (LotActif && element.LivreId is { } livreLot
|
||||||
|
&& _livresCatalogue.Any(l => l.Id == livreLot))
|
||||||
|
{
|
||||||
|
<label class="oeuvre-case">
|
||||||
|
<input type="checkbox"
|
||||||
|
checked="@_selectionLot.Contains(livreLot)"
|
||||||
|
@onchange="e => SelectionnerLot(livreLot, e.Value is true)" />
|
||||||
|
<span class="lecture-ecran">Sélectionner « @element.Titre »</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="rang-actions">
|
<div class="rang-actions">
|
||||||
@if (ModeOrdre)
|
@if (ModeOrdre)
|
||||||
{
|
{
|
||||||
@@ -641,6 +698,14 @@ else
|
|||||||
private bool _enCours;
|
private bool _enCours;
|
||||||
private string? _erreur;
|
private string? _erreur;
|
||||||
|
|
||||||
|
/// <summary>Le panneau de modification en lot est déplié.</summary>
|
||||||
|
private bool _lotOuvert;
|
||||||
|
|
||||||
|
/// <summary>Identifiants des LIVRES (pas des places) cochés pour la modification en lot.</summary>
|
||||||
|
private readonly HashSet<int> _selectionLot = [];
|
||||||
|
|
||||||
|
private SaisieLot _saisieLot = new();
|
||||||
|
|
||||||
private string _titre = string.Empty;
|
private string _titre = string.Empty;
|
||||||
private int _parenteId;
|
private int _parenteId;
|
||||||
private bool _confirmeSuppression;
|
private bool _confirmeSuppression;
|
||||||
@@ -765,6 +830,11 @@ else
|
|||||||
{
|
{
|
||||||
Ouvrir(VoieAjout.Aucune);
|
Ouvrir(VoieAjout.Aucune);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Les deux panneaux se marcheraient dessus sur un petit écran : un seul à la fois.
|
||||||
|
_lotOuvert = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Vrai quand ce qui est tapé dans la saisie manuelle est un code et non un titre.</summary>
|
/// <summary>Vrai quand ce qui est tapé dans la saisie manuelle est un code et non un titre.</summary>
|
||||||
@@ -860,6 +930,116 @@ else
|
|||||||
/// <summary>Ni édition de la fiche, ni réordonnancement : l'écran qu'on ouvre pour regarder.</summary>
|
/// <summary>Ni édition de la fiche, ni réordonnancement : l'écran qu'on ouvre pour regarder.</summary>
|
||||||
private bool ModeConsultation => !ModeEdition && !ModeOrdre;
|
private bool ModeConsultation => !ModeEdition && !ModeOrdre;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Le panneau de lot n'a de sens qu'en consultation : la case à cocher se confondrait avec
|
||||||
|
/// les flèches de l'écran d'ordre, qui reste dédié au déplacement des tomes.
|
||||||
|
/// </summary>
|
||||||
|
private bool LotActif => _lotOuvert && ModeConsultation;
|
||||||
|
|
||||||
|
/// <summary>Tomes de la série effectivement rattachés à un livre du catalogue local.</summary>
|
||||||
|
private int TomesModifiablesEnLot =>
|
||||||
|
_serie?.Elements.Count(e => e.LivreId is { } id && _livresCatalogue.Any(l => l.Id == id)) ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ouvre ou ferme le panneau de modification en lot. Le refermer vide aussi la sélection et
|
||||||
|
/// la saisie en cours — même règle que <see cref="BasculerAjout"/> : un panneau qui se
|
||||||
|
/// rouvrirait sur l'état d'avant laisserait croire à une saisie encore active.
|
||||||
|
/// </summary>
|
||||||
|
private void BasculerLot()
|
||||||
|
{
|
||||||
|
_lotOuvert = !_lotOuvert;
|
||||||
|
_erreur = null;
|
||||||
|
_selectionLot.Clear();
|
||||||
|
_saisieLot = new SaisieLot();
|
||||||
|
|
||||||
|
// Les deux panneaux se marcheraient dessus sur un petit écran : un seul à la fois.
|
||||||
|
if (_lotOuvert)
|
||||||
|
{
|
||||||
|
_ajoutOuvert = false;
|
||||||
|
Ouvrir(VoieAjout.Aucune);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectionnerLot(int livreId, bool selectionne)
|
||||||
|
{
|
||||||
|
if (selectionne)
|
||||||
|
{
|
||||||
|
_selectionLot.Add(livreId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectionLot.Remove(livreId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnnulerLot()
|
||||||
|
{
|
||||||
|
_selectionLot.Clear();
|
||||||
|
_saisieLot = new SaisieLot();
|
||||||
|
_erreur = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applique la saisie en lot à chacun des livres cochés, un appel à la fois — même patron que
|
||||||
|
/// <c>MasquerSelectionAsync</c> de la bibliographie : pas de point d'entrée dédié, la boucle
|
||||||
|
/// réutilise <see cref="ServiceLivresApi.ModifierAsync"/>, en ne remplaçant que les champs
|
||||||
|
/// cochés et en reprenant tels quels ceux qui ne le sont pas.
|
||||||
|
/// </summary>
|
||||||
|
private async Task EnregistrerLotAsync()
|
||||||
|
{
|
||||||
|
_enCours = true;
|
||||||
|
_erreur = null;
|
||||||
|
|
||||||
|
var erreurs = new List<string>();
|
||||||
|
|
||||||
|
foreach (var livreId in _selectionLot)
|
||||||
|
{
|
||||||
|
var livre = _livresCatalogue.FirstOrDefault(l => l.Id == livreId);
|
||||||
|
if (livre is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saisie = new EnregistrementLivre
|
||||||
|
{
|
||||||
|
Isbn = livre.Isbn,
|
||||||
|
Titre = livre.Titre,
|
||||||
|
Auteurs = _saisieLot.ModifierAuteurs
|
||||||
|
? _saisieLot.Auteurs
|
||||||
|
: livre.Auteurs.Select(a => new AuteurSaisi(a.Nom, a.Role)).ToList(),
|
||||||
|
Themes = _saisieLot.ModifierThemes ? _saisieLot.Themes : [.. livre.Themes],
|
||||||
|
Editeur = _saisieLot.ModifierEditeur ? _saisieLot.Editeur : livre.Editeur,
|
||||||
|
Format = _saisieLot.ModifierFormat ? _saisieLot.Format : livre.Format,
|
||||||
|
TypeDocument = _saisieLot.ModifierType ? _saisieLot.TypeDocument : livre.TypeDocument,
|
||||||
|
Statut = _saisieLot.ModifierStatut ? _saisieLot.Statut : livre.Statut,
|
||||||
|
NombrePages = livre.NombrePages,
|
||||||
|
CoverUrl = livre.CoverUrl,
|
||||||
|
UrlNotice = livre.UrlNotice,
|
||||||
|
};
|
||||||
|
|
||||||
|
var resultat = await Api.ModifierAsync(livre.Id, saisie);
|
||||||
|
if (!resultat.EstOk)
|
||||||
|
{
|
||||||
|
erreurs.Add(resultat.Erreur!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_enCours = false;
|
||||||
|
|
||||||
|
if (erreurs.Count > 0)
|
||||||
|
{
|
||||||
|
_erreur = string.Join(" ", erreurs.Distinct());
|
||||||
|
await ChargerAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lotOuvert = false;
|
||||||
|
_selectionLot.Clear();
|
||||||
|
_saisieLot = new SaisieLot();
|
||||||
|
_message = "Les livres sélectionnés ont été modifiés.";
|
||||||
|
await ChargerAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private bool Suffixe(string fin) =>
|
private bool Suffixe(string fin) =>
|
||||||
Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/')
|
Navigation.ToAbsoluteUri(Navigation.Uri).AbsolutePath.TrimEnd('/')
|
||||||
.EndsWith(fin, StringComparison.OrdinalIgnoreCase);
|
.EndsWith(fin, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -164,6 +164,11 @@ body {
|
|||||||
|
|
||||||
.entete-fiche-actions {
|
.entete-fiche-actions {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
/* Sans elle, un flex-basis « auto » prend la largeur de TOUS les boutons sur une seule
|
||||||
|
ligne (leur taille de contenu), et le flex-wrap ci-dessous ne peut jamais s'en servir :
|
||||||
|
la ligne déborde au lieu de se replier. Quatre boutons (dont « Modifier en lot ») y
|
||||||
|
tiennent tout juste à 1280px, mais débordent à 375px sans ce plafond. */
|
||||||
|
max-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -1664,6 +1669,20 @@ body {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Le compte rendu de la dernière rafale (lot AB) : une entrée transitoire, présente
|
||||||
|
seulement tant qu'une file non vidée existe. Le même jaune que la pastille hors-ligne
|
||||||
|
la distingue des six destinations permanentes — c'est une chose à finir, pas un lieu.
|
||||||
|
*/
|
||||||
|
.menu-lien-rafale {
|
||||||
|
border-left-color: #f5d76e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-lien-rafale.active {
|
||||||
|
border-left-color: #f5d76e;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
À partir de 40rem la rangée tient sans se comprimer, et la bascule n'a plus
|
À partir de 40rem la rangée tient sans se comprimer, et la bascule n'a plus
|
||||||
de raison d'être. Le point de rupture est répété dans `MainLayout.razor.css`
|
de raison d'être. Le point de rupture est répété dans `MainLayout.razor.css`
|
||||||
@@ -1705,6 +1724,10 @@ body {
|
|||||||
border-bottom-color: #f5d76e;
|
border-bottom-color: #f5d76e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-lien-rafale {
|
||||||
|
border-bottom-color: #f5d76e;
|
||||||
|
}
|
||||||
|
|
||||||
/* En rangée, un filet au-dessus ne séparerait rien : le trait passe à GAUCHE, et la marge
|
/* En rangée, un filet au-dessus ne séparerait rien : le trait passe à GAUCHE, et la marge
|
||||||
supérieure disparaît sous peine de désaligner l'entrée du reste de la rangée. */
|
supérieure disparaît sous peine de désaligner l'entrée du reste de la rangée. */
|
||||||
.menu-lien-annexe {
|
.menu-lien-annexe {
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ public record EnregistrementSouhait
|
|||||||
|
|
||||||
public string? CoverUrl { get; set; }
|
public string? CoverUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vrai seulement si l'utilisateur vient d'utiliser le geste de correction de la couverture.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// ⚠️ Sans ce signal, <see cref="CoverUrl"/> à <c>null</c> serait ambigu entre « je n'y ai
|
||||||
|
/// pas touché » (garder ce qu'il y a en base, y compris rien du tout, et laisser jouer la
|
||||||
|
/// formule OpenLibrary à ISBN connu) et « je viens de vider le champ » (retirer la
|
||||||
|
/// couverture pour de bon). Modifier le titre ou l'ISBN d'une envie ne doit jamais
|
||||||
|
/// matérialiser en base la couverture déduite qu'elle ne fait qu'afficher à la lecture.
|
||||||
|
/// </remarks>
|
||||||
|
public bool CoverUrlModifiee { get; set; }
|
||||||
|
|
||||||
public string? Note { get; set; }
|
public string? Note { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user