Files
mabibli/MaBibli.Client/Layout/MainLayout.razor
T
LIMONIER MathieuandCopilot 5d45eb492e Traiter le lot A d'IDEES.md : CSS onglets, pastilles, identité visuelle, couverture cassée, cache couvertures
- A1 : forcer la revalidation HTTP (Cache-Control: no-cache) sur les fichiers statiques,
  pour empêcher un navigateur de garder indéfiniment un CSS/JS périmé quand le service
  worker ne s'active pas.
- A2 : le filtre du catalogue (statut, type de document) reprend les mêmes pastilles
  colorées que les cartes (Libelles.ClasseStatut / nouvelle Libelles.ClasseTypeDocument).
- A3 : remplacement du logo par défaut de Blazor par un pictogramme de livre ouvert
  (favicon, icônes PWA, logo du bandeau, logo mabibli_ynh).
- A4 : une couverture cassée désactive aussi son bouton d'agrandissement.
- A5 : cache IndexedDB des couvertures pour la consultation hors-ligne, sans ralentir
  l'affichage en ligne (mise en cache en tâche de fond). L'allègement (redimensionnement,
  WebP) reste hors scope, à mesurer avant de s'y engager.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 13:03:14 +02:00

153 lines
5.3 KiB
Plaintext

@inherits LayoutComponentBase
@implements IDisposable
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@* Mise en page pensée mobile d'abord : un bandeau, une colonne, rien à gauche.
Le PC hérite de la même colonne, simplement centrée et limitée en largeur. *@
<header class="bandeau">
<a class="marque" href="">
<img src="logo-bandeau.svg" alt="" class="marque-logo" />
MaBibli
</a>
@if (!Reseau.EnLigne)
{
@* Une pastille dans le bandeau, visible sur tous les écrans : l'état du réseau change
l'usage de l'application, il ne doit pas se découvrir au premier clic qui échoue. *@
<span class="pastille-hors-ligne" title="@EtatReseau.MotifHorsLigne">Hors ligne</span>
}
@if (_utilisateur?.Identifiant is not null)
{
<span class="utilisateur" title="@(_utilisateur.Simule ? "Utilisateur simulé (développement)" : "Connecté via le portail YunoHost")">
@_utilisateur.Affichage@(_utilisateur.Simule ? " (dev)" : "")
</span>
}
</header>
@if (!Reseau.EnLigne)
{
@*
Dire d'où viennent les données et de quand elles datent. Sans cette phrase, une
bibliothèque affichée hors-ligne est indiscernable d'une bibliothèque à jour — et un
livre ajouté depuis un autre appareil manquerait sans explication.
*@
<p class="bandeau-reseau" role="status">
<strong>Hors ligne.</strong>
@(Reseau.DerniereSynchro is { } synchro
? $" Données enregistrées {Quand(synchro)}. "
: " Aucune donnée n'a encore pu être enregistrée sur cet appareil. ")
Consultation et recherche fonctionnent ; les modifications sont impossibles.
</p>
}
<main class="contenu">
@Body
</main>
@*
Navigation permanente, en bas de l'écran : six onglets, toujours au même endroit.
Elle remplace les listes de liens que chaque page portait dans sa barre d'actions — cinq
boutons y passaient sur deux ou trois lignes sur un téléphone, et l'entrée « Envies » ne
figurait pas partout. Les barres d'actions restantes ne portent plus que des ACTIONS.
⚠️ Le pouce atteint le bas de l'écran, pas le haut : c'est ce qui justifie le bas plutôt
qu'un rang d'onglets sous le bandeau. Les actions de page se placent juste au-dessus
(variable --mb-onglets-hauteur, partagée avec .actions-flottantes).
Les onglets restent actifs hors-ligne : les six écrans se consultent depuis leurs
instantanés. Ce sont les écritures qui se désactivent, pas la navigation.
*@
<nav class="onglets" aria-label="Navigation principale">
<NavLink class="onglet" href="" Match="NavLinkMatch.All">Catalogue</NavLink>
<NavLink class="onglet" href="auteurs">Auteurs</NavLink>
<NavLink class="onglet" href="series">Séries</NavLink>
<NavLink class="onglet" href="revues">Revues</NavLink>
<NavLink class="onglet" href="prets">Prêts</NavLink>
<NavLink class="onglet" href="souhaits">Envies</NavLink>
</nav>
@code {
private UtilisateurCourant? _utilisateur;
private bool _etaitEnLigne = true;
private bool _synchroEnCours;
protected override async Task OnInitializedAsync()
{
Reseau.Change += SurChangementReseau;
Reseau.SynchroChange += SurSynchro;
// É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.
await Reseau.DemarrerAsync();
_etaitEnLigne = Reseau.EnLigne;
_utilisateur = await Api.ObtenirUtilisateurAsync();
// Rafraîchit tout le fonds, pas seulement l'écran ouvert : c'est ce qui rend la
// bibliothèque entière consultable et cherchable après la coupure.
await SynchroniserAsync();
}
/// <summary>
/// Au retour du réseau, on recharge : la bibliothèque a pu changer depuis un autre appareil,
/// et les actions d'écriture redeviennent disponibles dans la foulée.
/// </summary>
private void SurChangementReseau()
{
var revenu = Reseau.EnLigne && !_etaitEnLigne;
_etaitEnLigne = Reseau.EnLigne;
_ = InvokeAsync(async () =>
{
StateHasChanged();
if (revenu)
{
_utilisateur = await Api.ObtenirUtilisateurAsync();
await SynchroniserAsync();
StateHasChanged();
}
});
}
private async Task SynchroniserAsync()
{
if (_synchroEnCours)
{
return;
}
_synchroEnCours = true;
try
{
await Api.SynchroniserAsync();
}
finally
{
_synchroEnCours = false;
}
}
/// <summary>Date de synchronisation en clair : l'heure suffit le jour même.</summary>
private static string Quand(DateTimeOffset instant)
{
var local = instant.ToLocalTime();
return local.Date == DateTimeOffset.Now.Date
? $"aujourd'hui à {local:HH:mm}"
: $"le {local:dd/MM/yyyy} à {local:HH:mm}";
}
/// <summary>La date affichée vient de changer : rien à recharger, juste à redessiner.</summary>
private void SurSynchro() => _ = InvokeAsync(StateHasChanged);
public void Dispose()
{
Reseau.Change -= SurChangementReseau;
Reseau.SynchroChange -= SurSynchro;
}
}