- 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é.
555 lines
21 KiB
Plaintext
555 lines
21 KiB
Plaintext
@page "/ajout"
|
|
@page "/ajout/isbn"
|
|
@inject ServiceLivresApi Api
|
|
@inject NavigationManager Navigation
|
|
@inject EtatReseau Reseau
|
|
@implements IDisposable
|
|
|
|
<PageTitle>MaBibli — ajouter un ouvrage</PageTitle>
|
|
|
|
@*
|
|
Entrée UNIQUE d'ajout, atteinte depuis le catalogue comme depuis les revues.
|
|
|
|
Le geste réel est « j'ai un truc avec un code-barres », pas « je vais cataloguer un livre » :
|
|
obliger à savoir d'avance ce qu'on tient était un détour, d'autant que le décodage sait
|
|
déjà distinguer un 978/979 (livre) d'un 977 (publication en série). Le modèle, lui, ne
|
|
bouge pas : une revue reste une Revue, jamais un Livre — voir CLAUDE.md.
|
|
*@
|
|
<h1 class="titre-page">Ajouter un ouvrage</h1>
|
|
|
|
@if (_etape == Etape.Saisie)
|
|
{
|
|
<p class="message-discret">
|
|
Scannez le code-barres, ou saisissez le code imprimé sur l'ouvrage — livre ou
|
|
magazine, l'application reconnaît lequel. Une douchette USB fonctionne telle quelle :
|
|
le champ est déjà actif.
|
|
</p>
|
|
|
|
@*
|
|
Le scan comme le lookup interrogent la BnF puis OpenLibrary : ils EXIGENT le réseau,
|
|
et rien ne peut être ajouté au catalogue hors-ligne de toute façon. Le dire ici évite
|
|
une caméra ouverte pour rien, puis un délai d'attente incompréhensible.
|
|
*@
|
|
@if (!Reseau.EnLigne)
|
|
{
|
|
<p class="message-avertissement" role="status">
|
|
Hors ligne : la recherche par ISBN interroge la BnF et OpenLibrary, et l'ajout au
|
|
catalogue passe par le serveur. Les deux redeviendront possibles au retour du réseau.
|
|
</p>
|
|
}
|
|
|
|
@*
|
|
Le champ est focalisé à l'ouverture et valide sur Entrée : c'est tout ce qu'exige une
|
|
douchette USB, qui se présente au système comme un CLAVIER — elle « tape » les chiffres
|
|
puis Entrée. Aucune permission, aucun HTTPS, aucun décodage : c'est le contournement le
|
|
plus rentable du scan caméra, qui rate souvent sur la webcam d'un PC.
|
|
La recherche n'est délibérément PAS déclenchée à chaque frappe : une douchette tape trop
|
|
vite, et chaque caractère partirait en requête.
|
|
*@
|
|
<div class="barre-recherche">
|
|
<input class="champ-saisie" type="text" inputmode="numeric" placeholder="978…"
|
|
@ref="_champIsbn"
|
|
@bind="_isbn" @bind:event="oninput" @onkeydown="SurTouche"
|
|
aria-label="ISBN" autocomplete="off" />
|
|
</div>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(_chargement || !Reseau.EnLigne || string.IsNullOrWhiteSpace(_isbn))"
|
|
title="@MotifBlocage"
|
|
@onclick="ChercherAsync">
|
|
@(_chargement ? "Recherche…" : "Chercher")
|
|
</button>
|
|
@* La saisie manuelle reste le recours quand le code-barres est abîmé, absent,
|
|
ou que la caméra est indisponible : elle ne disparaît jamais derrière le scan. *@
|
|
<button type="button" class="bouton bouton-discret"
|
|
disabled="@(!Reseau.EnLigne)" title="@MotifBlocage"
|
|
@onclick="OuvrirScanner">
|
|
Scanner
|
|
</button>
|
|
@* Deux saisies manuelles, parce qu'il faut bien trancher soi-même quand aucun code
|
|
n'est là pour le faire : c'est le seul endroit où l'entrée unique doit poser la
|
|
question. *@
|
|
<a class="bouton bouton-discret" href="ajout/manuel">Saisir un livre</a>
|
|
<a class="bouton bouton-discret" href="revues/ajout">Saisir une revue</a>
|
|
|
|
@* Une pile de livres et une douchette : on collecte d'abord, on traite ensuite.
|
|
Reste ici, et non dans le menu : c'est une façon d'ajouter, pas une destination. *@
|
|
<a class="bouton bouton-discret" href="ajout/rafale">Cataloguer en rafale</a>
|
|
</div>
|
|
}
|
|
|
|
@*
|
|
Le scan enchaîne la recherche tout seul (décision actée). Encore faut-il que ça SE VOIE :
|
|
sans cet écran, on retombait sur la saisie avec un bouton « Chercher » intact, et l'on
|
|
croyait que le scan n'avait rien déclenché.
|
|
*@
|
|
@if (_etape == Etape.Recherche)
|
|
{
|
|
@* L1 : l'étape existait déjà pour que l'enchaînement du scan SE VOIE ; la rondelle dit en
|
|
plus que ça travaille encore, ce qu'un texte figé ne distingue pas d'un écran bloqué. *@
|
|
<Patience Classe="attente-recherche"
|
|
Message="@($"Recherche de {FormatageIsbn.Afficher(_isbn)} à la BnF, puis chez OpenLibrary…")" />
|
|
}
|
|
|
|
@if (_etape == Etape.Scan)
|
|
{
|
|
<ScannerCodeBarres OnCodeDetecte="SurCodeDetecteAsync" OnAnnuler="Recommencer" />
|
|
}
|
|
|
|
@if (_erreur is not null)
|
|
{
|
|
<p class="message-erreur" role="alert">@_erreur</p>
|
|
}
|
|
|
|
@if (_messagePeriodique is not null)
|
|
{
|
|
<p class="message-avertissement" role="status">@_messagePeriodique</p>
|
|
|
|
@* Une revue a désormais sa place : sa fiche, où l'on ajoute le numéro qu'on vient de
|
|
scanner. Avant, ce code retombait sur la saisie manuelle d'un LIVRE — ce qu'un magazine
|
|
n'est pas. *@
|
|
@if (_periodique is not null)
|
|
{
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-principal"
|
|
disabled="@(_enregistrement || !Reseau.EnLigne)" title="@MotifBlocage"
|
|
@onclick="OuvrirLaRevueAsync">
|
|
@(_enregistrement ? "Ouverture…" : "Ouvrir la fiche de cette revue")
|
|
</button>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
@foreach (var avertissement in _avertissements)
|
|
{
|
|
<p class="message-avertissement">@avertissement</p>
|
|
}
|
|
|
|
@if (_etape == Etape.Choix)
|
|
{
|
|
@* Décision actée dans CLAUDE.md : plusieurs notices pour un même ISBN (rééditions
|
|
successives partageant l'ISBN), on ne choisit JAMAIS à la place de l'utilisateur.
|
|
Les candidats partagent titre et auteur : ce sont l'éditeur et l'année qui départagent,
|
|
donc ils sont mis en avant. *@
|
|
<p class="message-discret">
|
|
@_candidats.Count notices correspondent à cet ISBN. Elles se distinguent par l'éditeur et l'année.
|
|
</p>
|
|
|
|
<ul class="liste-candidats">
|
|
@foreach (var (candidat, index) in _candidats.Select((c, i) => (c, i)))
|
|
{
|
|
<li class="carte-candidat" @key="index">
|
|
<Couverture Url="@candidat.CoverUrl" Titre="@candidat.Titre" Classe="couverture-petite" />
|
|
|
|
<div class="carte-corps">
|
|
<p class="carte-titre">@candidat.Titre</p>
|
|
@if (!string.IsNullOrWhiteSpace(candidat.Auteur))
|
|
{
|
|
<p class="carte-auteur">@candidat.Auteur</p>
|
|
}
|
|
|
|
<p class="candidat-distinction">
|
|
<span class="candidat-editeur">@(candidat.Editeur ?? "éditeur inconnu")</span>
|
|
<span class="candidat-annee">@(candidat.Annee ?? "année inconnue")</span>
|
|
</p>
|
|
|
|
<p class="carte-details">
|
|
<span class="etiquette">@candidat.Source</span>
|
|
@if (!string.IsNullOrWhiteSpace(candidat.IsbnInterroge))
|
|
{
|
|
<span>trouvé via <span class="code-isbn">@FormatageIsbn.Afficher(candidat.IsbnInterroge)</span></span>
|
|
}
|
|
</p>
|
|
|
|
<button type="button" class="bouton bouton-principal"
|
|
@onclick="() => Choisir(candidat)">
|
|
Choisir cette édition
|
|
</button>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
|
|
<div class="actions-formulaire">
|
|
<button type="button" class="bouton bouton-discret" @onclick="Recommencer">Aucune : saisir à la main</button>
|
|
</div>
|
|
}
|
|
|
|
@if (_etape == Etape.Formulaire)
|
|
{
|
|
@* Le doublon prend toute la place tant qu'il n'est pas tranché : la fiche revient intacte
|
|
si l'on renonce. C'est ici que le cas se présente le plus — scanner deux fois le même
|
|
livre est précisément ce qui crée les fiches en double. *@
|
|
@if (_doublons is { } doublons)
|
|
{
|
|
<AvertissementDoublon Doublons="doublons"
|
|
EnCours="_enregistrement"
|
|
OnConfirmer="ConfirmerAsync"
|
|
OnRenoncer="() => _doublons = null" />
|
|
}
|
|
else
|
|
{
|
|
<p class="message-discret">
|
|
Vérifiez et complétez la fiche : tout reste modifiable.
|
|
</p>
|
|
|
|
<FormulaireLivre Saisie="_saisie"
|
|
LibelleValidation="Ajouter au catalogue"
|
|
Erreur="@_erreurFormulaire"
|
|
EnCours="_enregistrement"
|
|
MessageBlocage="@MotifBlocage"
|
|
OnValider="EnregistrerAsync"
|
|
OnAnnuler="Recommencer" />
|
|
}
|
|
}
|
|
|
|
@code {
|
|
private enum Etape { Saisie, Scan, Recherche, Choix, Formulaire }
|
|
|
|
private Etape _etape = Etape.Saisie;
|
|
private string _isbn = string.Empty;
|
|
private ElementReference _champIsbn;
|
|
|
|
// Le focus ne se redonne qu'une fois par entrée dans l'étape de saisie : le reprendre à
|
|
// chaque rendu arracherait le curseur à l'utilisateur en pleine frappe.
|
|
private bool _focusAFaire = true;
|
|
|
|
private string? _messagePeriodique;
|
|
private PeriodiqueDetecte? _periodique;
|
|
|
|
/// <summary>Add-on EAN-2 du dernier scan, quand il y en avait un. Voir <see cref="CodeScanne"/>.</summary>
|
|
private string? _parutionLue;
|
|
private bool _chargement;
|
|
private bool _enregistrement;
|
|
private string? _erreur;
|
|
private string? _erreurFormulaire;
|
|
private IReadOnlyList<CandidatLivre> _candidats = [];
|
|
private IReadOnlyList<string> _avertissements = [];
|
|
private EnregistrementLivre _saisie = new();
|
|
private DoublonsLivre? _doublons;
|
|
|
|
/// <summary>
|
|
/// Code transmis par un autre écran — un résidu de rafale à trancher.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ La recherche s'enchaîne d'elle-même, comme après un scan : arriver ici depuis un
|
|
/// compte rendu de rafale signifie qu'on a déjà décidé de reprendre ce code. Demander un
|
|
/// clic de plus sur « Chercher » serait le détour que cet écran documente déjà comme un
|
|
/// défaut (voir l'étape « Recherche »).
|
|
/// </remarks>
|
|
[SupplyParameterFromQuery(Name = "code")]
|
|
public string? CodePropose { get; set; }
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
Reseau.Change += SurChangementReseau;
|
|
|
|
if (!string.IsNullOrWhiteSpace(CodePropose))
|
|
{
|
|
_isbn = CodePropose.Trim();
|
|
}
|
|
}
|
|
|
|
private bool _repriseFaite;
|
|
|
|
/// <summary>
|
|
/// Donne le focus au champ ISBN dès que l'étape de saisie s'affiche.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// C'est ce qui rend une <b>douchette USB</b> utilisable sans rien d'autre : elle tape dans
|
|
/// le champ actif, quel qu'il soit. Sans focus automatique, l'utilisateur devrait cliquer
|
|
/// dans le champ avant chaque livre — et le gain sur le scan caméra disparaîtrait.
|
|
/// </remarks>
|
|
protected override async Task OnAfterRenderAsync(bool premierRendu)
|
|
{
|
|
// ⚠️ AVANT le garde de focus ci-dessous, qui rend la main dès que le focus est posé :
|
|
// greffée après, la reprise ne serait jouée qu'une fois sur deux, selon l'étape.
|
|
//
|
|
// ⚠️ Et surtout PAS conditionnée à `premierRendu` : au premier rendu, l'état réseau
|
|
// n'est pas encore établi (il s'initialise de façon asynchrone), donc `EnLigne` est
|
|
// faux et la reprise ne partait jamais. Constaté à l'écran — le champ se préremplissait
|
|
// bien, la recherche ne s'enchaînait pas. C'est `_repriseFaite` qui garantit l'unicité,
|
|
// pas le premier rendu ; l'événement réseau provoque le rendu où la condition passe.
|
|
if (!_repriseFaite && !string.IsNullOrWhiteSpace(CodePropose) && Reseau.EnLigne)
|
|
{
|
|
_repriseFaite = true;
|
|
await ChercherAsync();
|
|
|
|
// ⚠️ `StateHasChanged` est INDISPENSABLE ici, et son absence ne se voit pas :
|
|
// `OnAfterRenderAsync` n'est pas un gestionnaire d'événement, donc Blazor ne
|
|
// redessine rien de lui-même. La recherche partait bien — sonde à l'appui — et
|
|
// remplissait `_candidats`, mais l'écran restait sur l'étape de saisie.
|
|
// Même famille que « un composant qui écrit dans un objet prêté doit prévenir son
|
|
// parent » : le symptôme trompe, on cherche un appel qui n'a pas eu lieu là où il
|
|
// n'y a qu'un rendu manquant.
|
|
StateHasChanged();
|
|
}
|
|
|
|
if (_etape != Etape.Saisie || !_focusAFaire)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_focusAFaire = false;
|
|
|
|
try
|
|
{
|
|
await _champIsbn.FocusAsync();
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// L'élément a disparu entre le rendu et l'appel (navigation rapide) : sans intérêt.
|
|
}
|
|
}
|
|
|
|
private void SurChangementReseau() => _ = InvokeAsync(StateHasChanged);
|
|
|
|
public void Dispose() => Reseau.Change -= SurChangementReseau;
|
|
|
|
/// <summary>Raison du blocage des actions, ou <c>null</c> quand tout est possible.</summary>
|
|
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
|
|
|
|
private void OuvrirScanner()
|
|
{
|
|
_erreur = null;
|
|
_messagePeriodique = null;
|
|
_periodique = null;
|
|
_parutionLue = null;
|
|
_avertissements = [];
|
|
_etape = Etape.Scan;
|
|
}
|
|
|
|
// Le scan enchaîne directement sur le flux de lookup existant : l'utilisateur
|
|
// ne retape jamais ce qui vient d'être scanné, et n'a aucun bouton à confirmer.
|
|
// Le code lu reste dans le champ : si le décodage était mauvais, il se corrige et se relance.
|
|
private async Task SurCodeDetecteAsync(CodeScanne lecture)
|
|
{
|
|
_isbn = lecture.Code;
|
|
|
|
// Gardé de côté pour la seule chose qui sache quoi en faire : la fiche d'une revue.
|
|
// Sur un livre, l'add-on EAN-5 (le prix) n'est même pas remonté, et l'EAN-2 n'a pas
|
|
// de sens.
|
|
_parutionLue = lecture.Parution;
|
|
|
|
await ChercherAsync();
|
|
}
|
|
|
|
private async Task SurTouche(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_isbn))
|
|
{
|
|
// Saisi à la main ou à la douchette : aucun add-on, et surtout pas celui d'un
|
|
// scan précédent, qui se retrouverait proposé pour un autre magazine.
|
|
_parutionLue = null;
|
|
await ChercherAsync();
|
|
}
|
|
}
|
|
|
|
private async Task ChercherAsync()
|
|
{
|
|
_chargement = true;
|
|
_erreur = null;
|
|
_messagePeriodique = null;
|
|
_periodique = null;
|
|
_avertissements = [];
|
|
_etape = Etape.Recherche;
|
|
|
|
try
|
|
{
|
|
var resultat = await Api.ChercherIsbnAsync(_isbn.Trim());
|
|
|
|
if (resultat is null)
|
|
{
|
|
_erreur = $"« {_isbn} » n'est pas un ISBN valide.";
|
|
RevenirALaSaisie();
|
|
return;
|
|
}
|
|
|
|
_avertissements = resultat.Avertissements;
|
|
_candidats = resultat.Candidats;
|
|
|
|
// Un code de périodique (977) n'a JAMAIS de candidat : ce n'est pas un échec de
|
|
// recherche, c'est un code qui ne décrit pas un livre. Le dire avant tout le reste.
|
|
if (resultat.Periodique is { } revue)
|
|
{
|
|
DecrireLeMagazine(revue);
|
|
return;
|
|
}
|
|
|
|
if (_candidats.Count == 0)
|
|
{
|
|
// Aucune notice : on ne bloque pas, le formulaire manuel reste la porte de sortie.
|
|
_erreur = "Aucune notice trouvée pour cet ISBN. Complétez la fiche à la main.";
|
|
_saisie = new EnregistrementLivre { Isbn = resultat.IsbnDemande };
|
|
_etape = Etape.Formulaire;
|
|
return;
|
|
}
|
|
|
|
if (_candidats.Count == 1)
|
|
{
|
|
Choisir(_candidats[0]);
|
|
return;
|
|
}
|
|
|
|
_etape = Etape.Choix;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
_erreur = Reseau.EnLigne
|
|
? "La recherche a échoué. Réessayez, ou saisissez la fiche à la main."
|
|
: "La recherche par ISBN interroge la BnF et OpenLibrary : indisponible hors ligne.";
|
|
RevenirALaSaisie();
|
|
}
|
|
finally
|
|
{
|
|
_chargement = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dit ce qu'est le code scanné, et propose la fiche de revue correspondante.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ <b>Ne remplit surtout pas le formulaire d'un livre</b>, ce qu'il faisait tant que les
|
|
/// revues n'avaient pas de modèle : un magazine n'est pas un livre, et ranger un code
|
|
/// <c>977</c> dans <c>Livre.Isbn</c> ferait échouer tout lookup ultérieur sur la fiche.
|
|
/// <para>
|
|
/// Le <b>numéro</b> se saisit ensuite à la main : les deux chiffres de parution du
|
|
/// code-barres ne sont pas un numéro fiable, et l'add-on EAN-2 qui le porterait vraiment
|
|
/// n'a pas pu être vérifié sur un magazine réel.
|
|
/// </para>
|
|
/// </remarks>
|
|
private void DecrireLeMagazine(PeriodiqueDetecte revue)
|
|
{
|
|
var nom = revue.Titre is null ? "un magazine" : $"le magazine « {revue.Titre} »";
|
|
|
|
_periodique = revue;
|
|
_messagePeriodique =
|
|
$"Ce code-barres désigne {nom} (ISSN {revue.Issn}), pas un livre : "
|
|
+ "il commence par 977, réservé aux publications en série. "
|
|
+ "Ouvrez sa fiche pour y ajouter le numéro que vous venez de scanner.";
|
|
|
|
_erreur = null;
|
|
RevenirALaSaisie();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ouvre la fiche de la revue scannée, en la créant si elle n'existe pas encore.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Le serveur fait « créer ou retrouver » : scanner le numéro suivant du même magazine
|
|
/// retombe sur la même fiche, ce qui est exactement le geste attendu.
|
|
/// <para>
|
|
/// Sans titre connu de la BnF, on n'invente rien : l'ISSN sert de nom provisoire, que
|
|
/// l'utilisateur corrigera sur la fiche.
|
|
/// </para>
|
|
/// </remarks>
|
|
private async Task OuvrirLaRevueAsync()
|
|
{
|
|
if (_periodique is not { } revue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_enregistrement = true;
|
|
_erreur = null;
|
|
|
|
var resultat = await Api.CreerRevueAsync(new EnregistrementRevue
|
|
{
|
|
Titre = revue.Titre ?? $"Revue ISSN {revue.Issn}",
|
|
Issn = revue.Issn,
|
|
Editeur = revue.Editeur,
|
|
});
|
|
|
|
_enregistrement = false;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreur = resultat.Erreur;
|
|
return;
|
|
}
|
|
|
|
// Le numéro lu sur l'add-on EAN-2 suit la navigation : la fiche le proposera dans un
|
|
// champ modifiable. On ne l'enregistre pas soi-même — voir CodeScanne.
|
|
var numero = string.IsNullOrWhiteSpace(_parutionLue)
|
|
? string.Empty
|
|
: $"?numero={Uri.EscapeDataString(_parutionLue)}";
|
|
|
|
Navigation.NavigateTo($"revues/{resultat.Valeur!.Id}{numero}");
|
|
}
|
|
|
|
/// <summary>Ramène l'écran à la saisie, champ ISBN de nouveau actif pour la douchette.</summary>
|
|
private void RevenirALaSaisie()
|
|
{
|
|
_etape = Etape.Saisie;
|
|
_focusAFaire = true;
|
|
}
|
|
|
|
private void Choisir(CandidatLivre candidat)
|
|
{
|
|
_saisie = new EnregistrementLivre
|
|
{
|
|
Isbn = _isbn.Trim(),
|
|
Titre = candidat.Titre,
|
|
Auteur = candidat.Auteur,
|
|
Editeur = candidat.Editeur,
|
|
|
|
// Proposé, jamais imposé : la notice donne une phrase (« 1 vol. (349 p.) »), le
|
|
// champ reste modifiable et se vide d'un geste si elle a mal été lue.
|
|
NombrePages = candidat.NombrePages,
|
|
CoverUrl = candidat.CoverUrl,
|
|
UrlNotice = candidat.UrlNotice,
|
|
Format = Format.Physique,
|
|
Statut = Statut.ALire,
|
|
};
|
|
|
|
_erreur = null;
|
|
_etape = Etape.Formulaire;
|
|
}
|
|
|
|
private void Recommencer()
|
|
{
|
|
_erreur = null;
|
|
_messagePeriodique = null;
|
|
_periodique = null;
|
|
_erreurFormulaire = null;
|
|
_doublons = null;
|
|
_candidats = [];
|
|
RevenirALaSaisie();
|
|
}
|
|
|
|
private Task EnregistrerAsync() => AjouterAsync(false);
|
|
|
|
/// <summary>Ajout maintenu après avoir vu ce que le catalogue contenait déjà.</summary>
|
|
private Task ConfirmerAsync() => AjouterAsync(true);
|
|
|
|
private async Task AjouterAsync(bool confirmerDoublon)
|
|
{
|
|
_enregistrement = true;
|
|
_erreurFormulaire = null;
|
|
|
|
var resultat = await Api.CreerAsync(_saisie, confirmerDoublon);
|
|
|
|
_enregistrement = false;
|
|
|
|
// Rien n'a été écrit : le catalogue contient déjà quelque chose de semblable, et c'est
|
|
// à l'utilisateur de dire s'il s'agit du même livre. Voir AvertissementDoublon.
|
|
if (resultat.Doublons is { } doublons)
|
|
{
|
|
_doublons = doublons;
|
|
return;
|
|
}
|
|
|
|
_doublons = null;
|
|
|
|
if (!resultat.EstOk)
|
|
{
|
|
_erreurFormulaire = resultat.Erreur;
|
|
return;
|
|
}
|
|
|
|
Navigation.NavigateTo("/");
|
|
}
|
|
}
|