Files
mabibli/MaBibli.Client/Layout/MainLayout.razor
T
mathieuandClaude Opus 5 4e372ab694 Ajouter les sagas et cycles, avec leurs tomes manquants
Première relation entre livres du projet. Une série contient des séries
(un cycle EST une série de séries) et des PLACES dans l'ordre de lecture —
pas des livres : une place sans livre est le trou qu'on vient voir.

L'ordre est stocké, jamais déduit d'une année : une préquelle se lit avant
le livre paru dix ans plus tôt. Les tomes non possédés se saisissent à la
main, aucune source ne donnant l'ordre de lecture d'une saga.

Le titre de la place survit à la suppression du livre (SetNull, pas de
cascade), sans quoi perdre un exemplaire effacerait le tome 3 de la série.

Les séries sont communes au foyer. Seule exception : la mise en envies d'un
tome manquant, qui écrit dans une liste personnelle — c'est le seul point
où les deux portées se rencontrent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:03:08 +02:00

149 lines
5.2 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="">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 : cinq 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 cinq é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="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;
}
}