Transforme la bibliographie en écran de travail D1 à D5
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
ddc60898da
commit
359f4f59c6
@@ -86,6 +86,10 @@ else if (_bibliographie is { } biblio)
|
||||
<p class="compteur">
|
||||
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
|
||||
— @_possedees déjà chez vous
|
||||
@if (_masquees > 0)
|
||||
{
|
||||
<text> — @_masquees masquée@(_masquees > 1 ? "s" : "")</text>
|
||||
}
|
||||
</p>
|
||||
|
||||
@* Deux phrases distinctes plutôt qu'une seule à trous : enchaîner des fragments
|
||||
@@ -124,16 +128,37 @@ else if (_bibliographie is { } biblio)
|
||||
<p class="message-erreur" role="alert">@_message</p>
|
||||
}
|
||||
|
||||
@* Filtre sur une seule question : « qu'est-ce qui me manque ? ». Les livres possédés ne
|
||||
sont jamais masqués par défaut — le grisage est l'information principale de l'écran. *@
|
||||
<div class="filtres">
|
||||
<div class="segments" role="group" aria-label="Filtrer la bibliographie">
|
||||
<button type="button" class="segment @(_seulementManquants ? null : "segment-actif")"
|
||||
@onclick="() => _seulementManquants = false">Tout</button>
|
||||
<button type="button" class="segment @(_seulementManquants ? "segment-actif" : null)"
|
||||
@onclick="() => _seulementManquants = true">Ce qui me manque</button>
|
||||
</div>
|
||||
<div class="actions-formulaire">
|
||||
<button type="button" class="bouton bouton-discret"
|
||||
@onclick="() => _filtresOuverts = !_filtresOuverts"
|
||||
aria-expanded="@_filtresOuverts">
|
||||
Filtrer (@NombreFiltres)
|
||||
</button>
|
||||
</div>
|
||||
@if (_filtresOuverts)
|
||||
{
|
||||
<div class="filtres">
|
||||
<div class="segments" role="group" aria-label="Possession">
|
||||
<button type="button" class="segment @(_modePossession == 0 ? "segment-actif" : null)"
|
||||
@onclick="() => _modePossession = 0">Tout</button>
|
||||
<button type="button" class="segment @(_modePossession == 1 ? "segment-actif" : null)"
|
||||
@onclick="() => _modePossession = 1">Ceux que j'ai déjà</button>
|
||||
<button type="button" class="segment @(_modePossession == 2 ? "segment-actif" : null)"
|
||||
@onclick="() => _modePossession = 2">Ce qui me manque</button>
|
||||
</div>
|
||||
@if (_masquees > 0)
|
||||
{
|
||||
<div class="segments">
|
||||
<button type="button" class="segment @(_afficherMasquees ? "segment-actif" : null)"
|
||||
@onclick="() => _afficherMasquees = true">
|
||||
Afficher les masquées (@_masquees)
|
||||
</button>
|
||||
<button type="button" class="segment @(!_afficherMasquees ? "segment-actif" : null)"
|
||||
@onclick="() => _afficherMasquees = false">Masquer les masquées</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ul class="liste-bibliographie">
|
||||
@foreach (var oeuvre in Affichees(biblio))
|
||||
@@ -204,16 +229,51 @@ else if (_bibliographie is { } biblio)
|
||||
|
||||
@if (oeuvre.ADecouvrir)
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
|
||||
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
|
||||
Je le veux
|
||||
<div class="actions-formulaire">
|
||||
<label class="selection-oeuvre">
|
||||
<input type="checkbox"
|
||||
checked="@_selection.Contains(oeuvre.Titre)"
|
||||
@onchange="e => Selectionner(oeuvre.Titre, e.Value is true)" />
|
||||
Sélectionner
|
||||
</label>
|
||||
<button type="button" class="bouton bouton-compact bouton-principal"
|
||||
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
|
||||
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
|
||||
Je le veux
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!oeuvre.Masquee)
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
disabled="@_enCours" @onclick="() => MasquerAsync(oeuvre)">
|
||||
Masquer
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
disabled="@_enCours" @onclick="() => DemasquerAsync(oeuvre)">
|
||||
Réafficher
|
||||
</button>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@if (_selection.Count > 0)
|
||||
{
|
||||
<div class="actions-flottantes">
|
||||
<button type="button" class="bouton bouton-principal"
|
||||
disabled="@(_enCours || !Reseau.EnLigne)"
|
||||
title="@(Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne)"
|
||||
@onclick="AjouterSelectionAsync">
|
||||
Ajouter la sélection aux envies (@_selection.Count)
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Le rapprochement est fait par titre, et il rate des choses (voir CleOeuvre). Le dire
|
||||
franchement vaut mieux que laisser l'utilisateur découvrir seul qu'un livre qu'il
|
||||
possède n'est pas grisé. *@
|
||||
@@ -239,7 +299,9 @@ else if (_bibliographie is { } biblio)
|
||||
public int AuteurId { get; set; }
|
||||
|
||||
private BibliographieDto? _bibliographie;
|
||||
private bool _seulementManquants;
|
||||
private int _modePossession;
|
||||
private bool _filtresOuverts;
|
||||
private bool _afficherMasquees;
|
||||
private bool _enCours;
|
||||
private bool _chargement;
|
||||
|
||||
@@ -250,13 +312,22 @@ else if (_bibliographie is { } biblio)
|
||||
private string? _message;
|
||||
|
||||
private int _possedees;
|
||||
private int _masquees;
|
||||
private IReadOnlyList<LivreDto> _livresCatalogue = [];
|
||||
private readonly HashSet<string> _oeuvresOuvertes = [];
|
||||
private readonly HashSet<string> _selection = [];
|
||||
|
||||
protected override async Task OnParametersSetAsync() => await ChargerAsync();
|
||||
|
||||
private IEnumerable<OeuvreBibliographie> Affichees(BibliographieDto biblio) =>
|
||||
_seulementManquants ? biblio.Oeuvres.Where(o => o.ADecouvrir) : biblio.Oeuvres;
|
||||
biblio.Oeuvres.Where(o =>
|
||||
(_afficherMasquees || !o.Masquee)
|
||||
&& (_modePossession == 0
|
||||
|| (_modePossession == 1 && o.Possede)
|
||||
|| (_modePossession == 2 && o.ADecouvrir)));
|
||||
|
||||
private int NombreFiltres =>
|
||||
(_modePossession == 0 ? 0 : 1) + (_masquees > 0 && !_afficherMasquees ? 1 : 0);
|
||||
|
||||
private async Task ChargerAsync()
|
||||
{
|
||||
@@ -278,6 +349,7 @@ else if (_bibliographie is { } biblio)
|
||||
}
|
||||
|
||||
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
|
||||
_masquees = _bibliographie.Oeuvres.Count(o => o.Masquee);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -300,6 +372,70 @@ else if (_bibliographie is { } biblio)
|
||||
}
|
||||
}
|
||||
|
||||
private void Selectionner(string titre, bool selectionnee)
|
||||
{
|
||||
if (selectionnee)
|
||||
{
|
||||
_selection.Add(titre);
|
||||
}
|
||||
else
|
||||
{
|
||||
_selection.Remove(titre);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AjouterSelectionAsync()
|
||||
{
|
||||
if (_bibliographie is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var saisies = _bibliographie.Oeuvres
|
||||
.Where(o => _selection.Contains(o.Titre) && o.ADecouvrir)
|
||||
.Select(o => new EnregistrementSouhait
|
||||
{
|
||||
Titre = o.Titre,
|
||||
Auteur = _bibliographie.Auteur.Nom,
|
||||
Editeur = o.Editeur,
|
||||
Annee = o.Annee,
|
||||
Isbn = o.Isbn,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_enCours = true;
|
||||
_message = null;
|
||||
var resultats = await Api.AjouterSouhaitsAsync(saisies);
|
||||
_enCours = false;
|
||||
var erreurs = resultats.Where(r => !r.EstOk).Select(r => r.Erreur).Where(e => e is not null).ToList();
|
||||
_message = erreurs.Count == 0 ? null : string.Join(" ", erreurs);
|
||||
_selection.Clear();
|
||||
await ChargerAsync();
|
||||
}
|
||||
|
||||
private async Task MasquerAsync(OeuvreBibliographie oeuvre)
|
||||
{
|
||||
await ModifierMasquageAsync(oeuvre, false);
|
||||
}
|
||||
|
||||
private async Task DemasquerAsync(OeuvreBibliographie oeuvre)
|
||||
{
|
||||
await ModifierMasquageAsync(oeuvre, true);
|
||||
}
|
||||
|
||||
private async Task ModifierMasquageAsync(OeuvreBibliographie oeuvre, bool demasquer)
|
||||
{
|
||||
_enCours = true;
|
||||
_message = demasquer
|
||||
? await Api.DemasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre)
|
||||
: await Api.MasquerOeuvreBibliographieAsync(AuteurId, oeuvre.Titre);
|
||||
_enCours = false;
|
||||
if (_message is null)
|
||||
{
|
||||
await ChargerAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void BasculerOeuvre(string titre)
|
||||
{
|
||||
if (!_oeuvresOuvertes.Add(titre))
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace MaBibli.Client.Services;
|
||||
public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, EtatReseau reseau)
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||
private readonly Dictionary<int, BibliographieDto> _bibliographies = [];
|
||||
|
||||
/// <summary>
|
||||
/// Recharge tous les instantanés depuis le réseau.
|
||||
@@ -615,8 +616,29 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
}
|
||||
|
||||
public async Task<ResultatApi<SouhaitDto>> AjouterSouhaitAsync(
|
||||
EnregistrementSouhait saisie, CancellationToken ct = default) =>
|
||||
await EcrireAsync<SouhaitDto>(() => http.PostAsJsonAsync("api/souhaits", saisie, Json, ct), ct);
|
||||
EnregistrementSouhait saisie, CancellationToken ct = default)
|
||||
{
|
||||
var resultat = await EcrireAsync<SouhaitDto>(
|
||||
() => http.PostAsJsonAsync("api/souhaits", saisie, Json, ct), ct);
|
||||
if (resultat.EstOk)
|
||||
{
|
||||
_bibliographies.Clear();
|
||||
}
|
||||
|
||||
return resultat;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ResultatApi<SouhaitDto>>> AjouterSouhaitsAsync(
|
||||
IReadOnlyList<EnregistrementSouhait> saisies, CancellationToken ct = default)
|
||||
{
|
||||
var resultats = new List<ResultatApi<SouhaitDto>>(saisies.Count);
|
||||
foreach (var saisie in saisies)
|
||||
{
|
||||
resultats.Add(await AjouterSouhaitAsync(saisie, ct));
|
||||
}
|
||||
|
||||
return resultats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retire une envie. <c>null</c> si c'est fait, sinon le motif à afficher.
|
||||
@@ -735,10 +757,48 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
|
||||
public async Task<BibliographieDto?> ObtenirBibliographieAsync(
|
||||
int auteurId, CancellationToken ct = default)
|
||||
{
|
||||
if (_bibliographies.TryGetValue(auteurId, out var cachee)
|
||||
&& cachee.Etat == EtatSourceBibliographie.Ok)
|
||||
{
|
||||
return cachee;
|
||||
}
|
||||
|
||||
var reponse = await http.GetAsync($"api/auteurs/{auteurId}/bibliographie", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
var bibliographie = reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<BibliographieDto>(reponse, ct);
|
||||
if (bibliographie?.Etat == EtatSourceBibliographie.Ok)
|
||||
{
|
||||
_bibliographies[auteurId] = bibliographie;
|
||||
}
|
||||
|
||||
return bibliographie;
|
||||
}
|
||||
|
||||
public async Task<string?> MasquerOeuvreBibliographieAsync(
|
||||
int auteurId, string titre, CancellationToken ct = default) =>
|
||||
await ModifierBibliographieAsync(
|
||||
() => http.PostAsJsonAsync(
|
||||
$"api/auteurs/{auteurId}/bibliographie/masque",
|
||||
new MasquageBibliographie { Titre = titre }, Json, ct), ct);
|
||||
|
||||
public async Task<string?> DemasquerOeuvreBibliographieAsync(
|
||||
int auteurId, string titre, CancellationToken ct = default) =>
|
||||
await ModifierBibliographieAsync(
|
||||
() => http.DeleteAsync(
|
||||
$"api/auteurs/{auteurId}/bibliographie/masque?titre={Uri.EscapeDataString(titre)}"), ct);
|
||||
|
||||
private async Task<string?> ModifierBibliographieAsync(
|
||||
Func<Task<HttpResponseMessage>> appel, CancellationToken ct)
|
||||
{
|
||||
var (reponse, motif) = await EnvoyerAsync(appel);
|
||||
if (reponse is null)
|
||||
{
|
||||
return motif;
|
||||
}
|
||||
|
||||
_bibliographies.Clear();
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user