Files
Nexus.Reader/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor
T

226 lines
7.5 KiB
Plaintext

@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
<div class="reader-canvas @(ThemeService.IsLightMode ? "theme-light" : "theme-dark")">
@if (ViewModel == null)
{
<div class="loading-state">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@StatusMessage</NexusTypography>
</div>
}
else
{
<div @ref="_containerRef" class="reader-flow-container">
@foreach (var block in ViewModel.Blocks)
{
<div id="@block.Id" class="block-wrapper @(_highlightedBlockId == block.Id ? "highlighted" : "")">
@if (block is TextSegmentBlock textSegment)
{
<NexusTypography Variant="NexusTypography.TypographyVariant.Ebook">@((MarkupString)textSegment.Content)</NexusTypography>
}
</div>
}
</div>
}
<SelectionAiPanel
SelectedText="@_selectedText"
BlockId="@_selectedBlockId"
Coordinates="@_selectionCoords"
FullPageContent="@GetFullPageContent()" />
</div>
@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<IJSObjectReference>("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<IJSObjectReference>("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<TextSegmentBlock>()
.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;
}
}