Traiter les retours d'usage du 2026-08-18 (2ᵉ série)
Six lots, issus d'IDEES.md. Les décisions actées sont reportées dans
CLAUDE.md, et les entrées traitées retirées d'IDEES.md.
1. Douchette USB, ISSN et enchaînement du scan
- Champ ISBN focalisé à l'ouverture : c'est tout ce qu'exige une
douchette USB, qui se présente au système comme un clavier. Remède
au scan caméra, qui rate sur la webcam d'un PC (optique, pas
décodage).
- Un EAN en 977 est un périodique : il porte un ISSN, donc un titre de
revue. Intercepté AVANT la validation ISBN (c'est un EAN-13
parfaitement valide), l'ISSN est déduit du code puis nommé via
bib.issn. Auparavant la cascade s'exécutait en entier pour finir sur
« aucun résultat ».
- L'enchaînement après scan existait déjà mais était invisible : une
étape « Recherche » affiche désormais le code interrogé.
2. ISBN affichés avec des tirets
Tables extraites du RangeMessage.xml officiel — plusieurs tranches ne
sont pas celles qu'on suppose. Le francophone est découpé en entier,
ailleurs on s'arrête au groupe et à la clé : aucune coupure fausse.
Corrige au passage l'export CSV, où un ISBN nu était lu comme un
nombre par Excel.
3. Hors-ligne : la liste d'envies
ListerSouhaitsAsync était le seul point de lecture hors du dispositif
hors-ligne, d'où le « 404 Not Found » brut à l'écran. Cinquième
instantané, écritures refusées, plus aucun message HTTP. Même défaut
corrigé sur la bibliographie.
4. Navigation par onglets
Catalogue / Auteurs / Prêts / Envies dans MainLayout ; les barres
d'actions ne portent plus que des actions. Filtres repliés derrière un
bouton compteur, ligne « format » masquée quand le fonds n'a qu'un
format. Une seule entrée d'ajout, désactivée hors-ligne — pas masquée.
5. Liste d'envies : ordre, recherche, couvertures
Migration RangDesEnvies. Le remplissage reconduit l'ordre affiché
jusqu'ici : sans lui, les listes existantes se seraient réordonnées
toutes seules. Réordonnancement par flèches et glisser-déposer (le
drag & drop HTML5 ne marche pas au doigt). Ajout dans son propre
écran, avec recherche par titre (bib.title) et couvertures enfin
alimentées.
6. Bibliographie : une source muette n'est pas une liste vide
L'écran affichait « BnF injoignable » PUIS « la BnF ne connaît aucun
livre de cet auteur » — la seconde phrase étant fausse. Les deux cas
s'excluent désormais, et un bouton Réessayer est offert.
Vérifié en exécution : le diagnostic d'IDEES.md était faux sur un
point. Robert A. Harper a bien 7 œuvres à la BnF (85 notices
annoncées) ; c'était le même délai dépassé observé deux fois, pris
pour deux causes distinctes.
380 tests, dont un qui applique réellement la migration (EnsureCreated
n'en joue aucune) et un qui verrouille les messages atteignant
l'utilisateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,21 @@
|
||||
|
||||
<PageTitle>MaBibli — catalogue</PageTitle>
|
||||
|
||||
<h1 class="titre-page">Catalogue</h1>
|
||||
@*
|
||||
Les filtres tiennent derrière un seul bouton, près du titre : deux rangées de segments
|
||||
occupaient en permanence le haut de l'écran d'un téléphone pour un réglage qu'on change
|
||||
rarement. Le compteur sur le bouton est ce qui rend le repli sans danger — un filtre actif
|
||||
et replié serait invisible, et l'on conclurait que des livres ont disparu.
|
||||
*@
|
||||
<div class="entete-page">
|
||||
<h1 class="titre-page">Catalogue</h1>
|
||||
|
||||
<button type="button" class="bouton bouton-discret bouton-compact"
|
||||
aria-expanded="@_filtresOuverts" aria-controls="filtres-catalogue"
|
||||
@onclick="() => _filtresOuverts = !_filtresOuverts">
|
||||
Filtrer@(FiltresActifs > 0 ? $" ({FiltresActifs})" : "")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (_auteur is not null)
|
||||
{
|
||||
@@ -21,29 +35,44 @@
|
||||
value="@_recherche" @oninput="SurRecherche" aria-label="Rechercher un livre" />
|
||||
</div>
|
||||
|
||||
<div class="filtres">
|
||||
<div class="segments" role="group" aria-label="Filtrer par format">
|
||||
<button type="button" class="segment @(_format is null ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerFormat(null)">Tous</button>
|
||||
@foreach (var format in Enum.GetValues<Format>())
|
||||
@if (_filtresOuverts)
|
||||
{
|
||||
<div class="filtres" id="filtres-catalogue">
|
||||
@* Un fonds tout physique n'a rien à filtrer par format : la ligne n'apprendrait rien
|
||||
et prendrait de la place. Cohérent avec « le format ne s'affiche que pour les
|
||||
ebooks ». Elle réapparaît d'elle-même au premier ebook ajouté. *@
|
||||
@if (PlusieursFormats)
|
||||
{
|
||||
<button type="button" class="segment @(_format == format ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerFormat(format)">@Libelles.Format(format)</button>
|
||||
<div class="segments" role="group" aria-label="Filtrer par format">
|
||||
<button type="button" class="segment @(_format is null ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerFormat(null)">Tous</button>
|
||||
@foreach (var format in Enum.GetValues<Format>())
|
||||
{
|
||||
<button type="button" class="segment @(_format == format ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerFormat(format)">@Libelles.Format(format)</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Ce filtre porte sur MON statut, pas sur celui du foyer : un livre sans statut pour moi
|
||||
ne remonte sous aucune valeur, il reste visible sous « Tous ». *@
|
||||
<div class="segments" role="group" aria-label="Filtrer par mon statut de lecture">
|
||||
<button type="button" class="segment @(_statut is null ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerStatut(null)">Tous</button>
|
||||
@foreach (var statut in Enum.GetValues<Statut>())
|
||||
{
|
||||
<button type="button" class="segment @(_statut == statut ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerStatut(statut)">@Libelles.Statut(statut)</button>
|
||||
}
|
||||
@* ⚠️ Ce filtre porte sur MON statut, pas sur celui du foyer : un livre sans statut
|
||||
pour moi ne remonte sous aucune valeur, il reste visible sous « Tous ». L'écran
|
||||
l'assume au lieu de laisser croire à une vue commune. *@
|
||||
<div class="segments" role="group" aria-label="Filtrer par mon statut de lecture">
|
||||
<button type="button" class="segment @(_statut is null ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerStatut(null)">Tous</button>
|
||||
@foreach (var statut in Enum.GetValues<Statut>())
|
||||
{
|
||||
<button type="button" class="segment @(_statut == statut ? "segment-actif" : null)"
|
||||
@onclick="() => FiltrerStatut(statut)">@Libelles.Statut(statut)</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p class="champ-aide">
|
||||
Le statut de lecture est <strong>le vôtre</strong> : filtrer dessus montre votre
|
||||
lecture, pas celle du foyer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_erreur is not null)
|
||||
{
|
||||
@@ -122,25 +151,27 @@ else if (_livres is not null)
|
||||
</ul>
|
||||
}
|
||||
|
||||
@* Hors-ligne, les deux entrées d'ajout deviennent des boutons éteints plutôt que des liens qui
|
||||
mèneraient à un formulaire invalidable. La consultation, elle, reste entière : recherche,
|
||||
filtres et tri portent sur toute la bibliothèque, depuis l'instantané local. *@
|
||||
@*
|
||||
Une seule entrée d'ajout, et plus aucun lien de navigation : les quatre écrans sont dans
|
||||
les onglets, en bas.
|
||||
|
||||
« Saisie manuelle » est retirée d'ici — elle reste accessible un écran plus loin, depuis
|
||||
« Ajouter », qui la propose dès qu'un ISBN ne donne rien.
|
||||
|
||||
⚠️ Hors-ligne le bouton est DÉSACTIVÉ, pas masqué : un bouton disparu est indiscernable
|
||||
d'une fonction supprimée, et rien n'apprendrait à l'utilisateur que c'est le réseau. Le
|
||||
désencombrement demandé passe par le regroupement (deux boutons en un), pas par l'effacement.
|
||||
*@
|
||||
<nav class="actions-flottantes">
|
||||
@if (Reseau.EnLigne)
|
||||
{
|
||||
<a class="bouton bouton-principal" href="ajout/isbn">Ajouter par ISBN</a>
|
||||
<a class="bouton bouton-discret" href="ajout/manuel">Saisie manuelle</a>
|
||||
<a class="bouton bouton-principal" href="ajout/isbn">Ajouter un livre</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="bouton bouton-principal" disabled
|
||||
title="@EtatReseau.MotifHorsLigne">Ajouter par ISBN</button>
|
||||
<button type="button" class="bouton bouton-discret" disabled
|
||||
title="@EtatReseau.MotifHorsLigne">Saisie manuelle</button>
|
||||
title="@EtatReseau.MotifHorsLigne">Ajouter un livre</button>
|
||||
}
|
||||
<a class="bouton bouton-discret" href="prets">Prêts</a>
|
||||
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
|
||||
<a class="bouton bouton-discret" href="souhaits">Envies</a>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
@@ -155,8 +186,33 @@ else if (_livres is not null)
|
||||
private Format? _format;
|
||||
private Statut? _statut;
|
||||
private bool _chargement;
|
||||
private bool _filtresOuverts;
|
||||
private string? _erreur;
|
||||
|
||||
/// <summary>
|
||||
/// Formats réellement présents dans le fonds, ou <c>null</c> tant qu'on ne le sait pas.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ Ne se calcule que sur un chargement <b>sans aucun critère</b>. Le déduire d'une liste
|
||||
/// filtrée ferait disparaître la ligne « Tous / Physique / Numérique » dès qu'une recherche
|
||||
/// ne remonte qu'un seul format — et l'utilisateur ne pourrait plus l'élargir. Dans le doute
|
||||
/// (arrivée directe sur un auteur, par exemple) la ligne reste affichée : montrer un filtre
|
||||
/// inutile est bénin, en escamoter un qui sert ne l'est pas.
|
||||
/// </remarks>
|
||||
private IReadOnlySet<Format>? _formatsDuFonds;
|
||||
|
||||
private bool PlusieursFormats => _formatsDuFonds is null || _formatsDuFonds.Count > 1;
|
||||
|
||||
/// <summary>
|
||||
/// Nombre de filtres repliés actuellement actifs, affiché sur le bouton « Filtrer ».
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// La recherche et la restriction à un auteur n'y comptent pas : toutes deux se voient
|
||||
/// déjà à l'écran, l'une dans son champ, l'autre dans sa phrase. Ce compteur ne dit que ce
|
||||
/// qui serait invisible une fois le panneau refermé.
|
||||
/// </remarks>
|
||||
private int FiltresActifs => (_format is null ? 0 : 1) + (_statut is null ? 0 : 1);
|
||||
|
||||
// Une frappe au clavier annule la recherche précédente encore en vol.
|
||||
private CancellationTokenSource _cts = new();
|
||||
|
||||
@@ -205,6 +261,13 @@ else if (_livres is not null)
|
||||
return ChargerAsync();
|
||||
}
|
||||
|
||||
/// <summary>Vrai quand la liste demandée est le fonds entier, sans restriction d'aucune sorte.</summary>
|
||||
private static bool EstSansCritere(CritereLivres criteres) =>
|
||||
string.IsNullOrWhiteSpace(criteres.Recherche)
|
||||
&& criteres.Format is null
|
||||
&& criteres.Statut is null
|
||||
&& criteres.AuteurId is null;
|
||||
|
||||
private async Task ChargerAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
@@ -226,14 +289,21 @@ else if (_livres is not null)
|
||||
};
|
||||
|
||||
_livres = await Api.ListerAsync(criteres, jeton);
|
||||
|
||||
if (EstSansCritere(criteres))
|
||||
{
|
||||
_formatsDuFonds = _livres.Select(l => l.Format).ToHashSet();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return; // Une recherche plus récente a pris le relais.
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
_erreur = $"Impossible de charger le catalogue : {ex.Message}";
|
||||
_erreur = Reseau.EnLigne
|
||||
? "Le catalogue n'a pas pu être chargé. Réessayez dans un instant."
|
||||
: "Aucune donnée n'a encore pu être enregistrée sur cet appareil.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user