feat: implement mobile reader header and navigation components with updated icon support

This commit is contained in:
2026-05-30 20:23:25 +02:00
parent a90507ad8a
commit 57b988e16f
14 changed files with 1599 additions and 162 deletions
@@ -15,9 +15,31 @@
@inject AuthenticationStateProvider AuthStateProvider
@inject IQuizStateService QuizService
@inject IPlatformService PlatformService
@inject NavigationManager Navigation
@inject ILogger<ReaderCanvas> Logger
<div class="reader-canvas @(ThemeService.IsLightMode ? "theme-light" : "theme-dark")">
@if (_isMobile && ViewModel != null)
{
<header class="nexus-mobile-reader-header">
<button class="nexus-mobile-escape-btn" @onclick="HandleEscape" aria-label="Powrót do pulpitu">
<NexusIcon Name="chevron-left" Size="18" />
<span>Pulpit</span>
</button>
<div class="nexus-mobile-chapter-navigation">
<button class="nexus-chapter-nav-btn prev" @onclick="NavigationService.GoToPreviousChapter" disabled="@(NavigationService.CurrentChapterIndex == 0)" aria-label="Poprzedni rozdział">
<NexusIcon Name="chevron-left" Size="14" />
</button>
<div class="nexus-mobile-chapter-title">
@ViewModel.ChapterTitle
</div>
<button class="nexus-chapter-nav-btn next" @onclick="NavigationService.GoToNextChapter" disabled="@(NavigationService.CurrentChapterIndex >= NavigationService.TotalChapters - 1)" aria-label="Następny rozdział">
<NexusIcon Name="chevron-right" Size="14" />
</button>
</div>
</header>
}
@if (ViewModel == null)
{
<div class="loading-state full-page">
@@ -56,16 +78,7 @@
Coordinates="@_selectionCoords"
FullPageContent="@GetFullPageContent()" />
@if (_isMobile)
{
<button class="nexus-mobile-assistant-fab @(QuizService.HasNewQuiz ? "has-new-quiz" : "")" @onclick="HandleAssistantFabClick" aria-label="Asystent AI">
<NexusIcon Name="robot" Size="24" Class="neon-glow" />
@if (QuizService.HasNewQuiz)
{
<span class="fab-badge"></span>
}
</button>
}
</div>
@code {
@@ -194,12 +207,15 @@
}
}
private IJSObjectReference? _scrollListenerReference;
private async Task InitializeObserverAsync()
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/readerObserver.js");
await module.InvokeVoidAsync("initObserver", DotNetObjectReference.Create(this), ".reader-flow-container", ".block-wrapper");
_scrollListenerReference = await module.InvokeAsync<IJSObjectReference>("initScrollListener", DotNetObjectReference.Create(this), ".reader-flow-container");
}
catch (Exception ex)
{
@@ -207,10 +223,17 @@
}
}
[JSInvokable]
public async Task HandleScrollPercentChanged(int percent)
{
await InteractionService.NotifyScrollPercentChanged(percent);
}
[JSInvokable]
public async Task HandleBlockReached(string blockId, string content)
{
_currentActiveBlockId = blockId;
await InteractionService.NotifyBlockReached(blockId);
await Coordinator.OnBlockReachedAsync(blockId, content);
if (ViewModel != null)
@@ -310,6 +333,13 @@
ViewModel = result.Value;
await NavigationService.UpdateMetadataAsync(ViewModel.CurrentChapterIndex, ViewModel.TotalChapters, ViewModel.ChapterTitle);
// Populate checkpoints!
var checkpoints = ViewModel.Blocks
.Where(b => !string.IsNullOrEmpty(b.Id) && b.Id.Contains("seg"))
.Select(b => b.Id)
.ToList();
InteractionService.CurrentCheckpoints = checkpoints;
if (_isInteractive)
{
await Coordinator.ProcessFullPageAsync(GetFullPageContent(), ebookId: ViewModel.EbookId);
@@ -352,6 +382,14 @@
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
private void HandleEscape()
{
if (ViewModel != null)
{
Navigation.NavigateTo("/");
}
}
private async Task HandleAssistantFabClick()
{
await InteractionService.RequestAssistant();
@@ -368,5 +406,14 @@
InteractionService.OnTextSelected -= HandleTextSelected;
SyncService.OnProgressReceived -= HandleSyncProgressReceived;
_selfReference?.Dispose();
try
{
if (_scrollListenerReference != null)
{
_ = _scrollListenerReference.DisposeAsync();
}
}
catch { }
}
}