226 lines
7.5 KiB
Plaintext
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;
|
|
}
|
|
}
|