@using MediatR @using NexusReader.Application.Queries.Reader @using Microsoft.JSInterop @using NexusReader.UI.Shared.Services @implements IDisposable @inject IMediator Mediator @inject IJSRuntime JS @inject IThemeService ThemeService @inject IFocusModeService FocusMode @inject IReaderNavigationService NavigationService @inject KnowledgeCoordinator Coordinator @inject IReaderInteractionService InteractionService @inject ISyncService SyncService
@if (ViewModel == null) {
@StatusMessage
} else {
@foreach (var block in ViewModel.Blocks) {
@if (block is TextSegmentBlock textSegment) { @((MarkupString)textSegment.Content) }
}
}
@code { private ReaderPageViewModel? ViewModel; private string StatusMessage = "Loading chapter..."; private string _selectedText = string.Empty; private string _selectedBlockId = string.Empty; private SelectionCoordinates? _selectionCoords; private string? _highlightedBlockId; private bool _isJsInitialized; private ElementReference _containerRef; protected override void OnInitialized() { Coordinator.Clear(); ThemeService.OnThemeChanged += StateHasChanged; NavigationService.OnNavigationChanged += OnNavigationChanged; InteractionService.OnScrollToBlockRequested += HandleScrollRequested; InteractionService.OnHighlightBlockRequested += HandleHighlightRequested; InteractionService.OnTextSelected += HandleTextSelected; } protected override async Task OnParametersSetAsync() { await LoadChapterAsync(NavigationService.CurrentChapterIndex); } private async Task OnNavigationChanged() { _isJsInitialized = false; _selectedText = string.Empty; _selectionCoords = null; await LoadChapterAsync(NavigationService.CurrentChapterIndex); StateHasChanged(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await SyncService.InitializeAsync(); } if (ViewModel != null && !_isJsInitialized) { _isJsInitialized = true; await InitializeObserverAsync(); await InitializeSelectionListenerAsync(); } } private async Task InitializeSelectionListenerAsync() { try { var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); await module.InvokeVoidAsync("initSelectionListener", DotNetObjectReference.Create(this), _containerRef); } catch { } } private async Task InitializeObserverAsync() { try { var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/readerObserver.js"); await module.InvokeVoidAsync("initObserver", DotNetObjectReference.Create(this), ".reader-flow-container", ".block-wrapper"); } catch { } } [JSInvokable] public void HandleBlockReached(string blockId, string content) { Coordinator.OnBlockReached(blockId, content); // Debounce sync update (simple version: every 5 seconds or on a timer) _ = SyncService.UpdateProgressAsync(blockId); } private void HandleSyncProgressReceived(string blockId, DateTime timestamp) { // For now, let's just scroll to the node if it's in the current view, // or just log it. Usually, we should prompt the user. Console.WriteLine($"[Sync] Received progress from another device: {blockId} at {timestamp}"); // Simple auto-scroll if it's newer than what we have (we don't track our own timestamp yet, // but we can assume incoming syncs are from other active devices) _ = InvokeAsync(async () => { await ScrollToNodeAsync(blockId); StateHasChanged(); }); } [JSInvokable] public void HandleTextSelected(string text, string blockId, SelectionCoordinates coords) { Console.WriteLine($"[ReaderCanvas] Text selected: {text} at {coords.Top},{coords.Left}"); _selectedText = text; _selectedBlockId = blockId; _selectionCoords = coords; StateHasChanged(); } [JSInvokable] public void HandleSelectionCleared() { _selectedText = string.Empty; _selectionCoords = null; StateHasChanged(); } private void HandleScrollRequested(string blockId) { _ = ScrollToNodeAsync(blockId); } private async void HandleHighlightRequested(string blockId) { _highlightedBlockId = blockId; StateHasChanged(); await Task.Delay(3000); // Highlight for 3 seconds if (_highlightedBlockId == blockId) { _highlightedBlockId = null; StateHasChanged(); } } private string GetFullPageContent() { if (ViewModel == null) return string.Empty; return string.Join("\n\n", ViewModel.Blocks .OfType() .Select(b => $"[ID: {b.Id}]\n{b.Content}")); } private async Task LoadChapterAsync(int index) { ViewModel = null; StatusMessage = "Fetching content..."; var result = await Mediator.Send(new GetReaderPageQuery(index)); if (result.IsSuccess) { ViewModel = result.Value; NavigationService.UpdateMetadata(ViewModel.CurrentChapterIndex, ViewModel.TotalChapters, ViewModel.ChapterTitle); // Trigger full page graph generation after loading await Coordinator.ProcessFullPageAsync(GetFullPageContent()); } else { StatusMessage = $"Error: {result.Errors.FirstOrDefault()?.Message ?? "Failed to load"}"; } } private void HandleAiAction(string action) { Console.WriteLine($"Action Triggered from Bubble: {action}"); } public async Task ScrollToNodeAsync(string id) { try { await JS.InvokeVoidAsync("eval", $"document.getElementById('{id}')?.scrollIntoView({{ behavior: 'smooth', block: 'center' }});"); } catch { } } public void Dispose() { ThemeService.OnThemeChanged -= StateHasChanged; NavigationService.OnNavigationChanged -= OnNavigationChanged; InteractionService.OnScrollToBlockRequested -= HandleScrollRequested; InteractionService.OnHighlightBlockRequested -= HandleHighlightRequested; InteractionService.OnTextSelected -= HandleTextSelected; SyncService.OnProgressReceived -= HandleSyncProgressReceived; } }