+ style="@(IsUploadActive ? "display:flex;" : "display:none;")"
+ @ondragenter="OnDragEnter"
+ @ondragleave="OnDragLeave">
@@ -150,6 +150,7 @@
private string? ErrorMessage { get; set; }
private byte[]? _epubBytes;
private bool _disposed;
+ private bool IsUploadActive => !IsParsing && !IsVerifying && !IsIngesting && !IsIndexing;
// Allow up to 50 MB
private const long MaxFileSize = 50 * 1024 * 1024;
@@ -170,6 +171,8 @@
if (!_disposed)
{
+ // Dispatch the state change to the Blazor synchronization context
+ // because this event is triggered asynchronously from a SignalR / WebSocket background thread.
await InvokeAsync(StateHasChanged);
}
@@ -184,6 +187,8 @@
if (IngestedBookId != Guid.Empty)
{
var bookId = IngestedBookId;
+ // Dispatch UI updates and navigation back to the Blazor thread
+ // to avoid thread affinity issues and potential UI lockups in MAUI/Web applications.
await InvokeAsync(async () => {
if (_disposed) return;
await CloseModal();
diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor
index e45c1ee..98aa195 100644
--- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor
+++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor
@@ -159,15 +159,24 @@
}
}
+ private async ValueTask EnsureViewportModuleAsync()
+ {
+ if (_viewportModule == null)
+ {
+ _viewportModule = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/viewport.js");
+ }
+ return _viewportModule;
+ }
+
private async Task InitViewportDetectionAsync()
{
try
{
- _viewportModule = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/viewport.js");
+ var module = await EnsureViewportModuleAsync();
_selfReference = DotNetObjectReference.Create(this);
- var isMobileViewport = await _viewportModule.InvokeAsync("isMobileViewport");
+ var isMobileViewport = await module.InvokeAsync("isMobileViewport");
await OnViewportChanged(isMobileViewport);
- await _viewportModule.InvokeVoidAsync("registerViewportObserver", _selfReference);
+ await module.InvokeVoidAsync("registerViewportObserver", _selfReference);
}
catch (Exception ex)
{
@@ -308,11 +317,6 @@
StatusMessage = "Wczytywanie treści...";
StateHasChanged();
- if (string.IsNullOrEmpty(NavigationService.PendingScrollBlockId))
- {
- await ScrollToTopAsync();
- }
-
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
@@ -353,16 +357,25 @@
_isLoadingChapter = false;
StateHasChanged();
- if (result.IsSuccess && !string.IsNullOrEmpty(NavigationService.PendingScrollBlockId))
+ if (result.IsSuccess)
{
- var targetBlockId = NavigationService.PendingScrollBlockId;
- NavigationService.PendingScrollBlockId = null; // Clear it to prevent multiple scrolls
- _currentActiveBlockId = targetBlockId;
+ if (!string.IsNullOrEmpty(NavigationService.PendingScrollBlockId))
+ {
+ var targetBlockId = NavigationService.PendingScrollBlockId;
+ NavigationService.PendingScrollBlockId = null; // Clear it to prevent multiple scrolls
+ _currentActiveBlockId = targetBlockId;
- // Give the browser slightly more than one frame to render the loaded blocks
- await Task.Delay(150);
- await ScrollToNodeAsync(targetBlockId);
- await InteractionService.RequestHighlightBlock(targetBlockId);
+ // Give the browser slightly more than one frame to render the loaded blocks
+ await Task.Delay(150);
+ await ScrollToNodeAsync(targetBlockId);
+ await InteractionService.RequestHighlightBlock(targetBlockId);
+ }
+ else
+ {
+ // Reset scroll to top now that the new content DOM is rendered
+ await Task.Delay(50); // Give the browser a frame to render the new chapter content
+ await ScrollToTopAsync();
+ }
}
}
@@ -370,7 +383,7 @@
{
try
{
- var module = _viewportModule ?? await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/viewport.js");
+ var module = await EnsureViewportModuleAsync();
await module.InvokeVoidAsync("scrollIntoView", id);
}
catch (Exception ex)
@@ -383,8 +396,8 @@
{
try
{
- var module = _viewportModule ?? await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/viewport.js");
- await module.InvokeVoidAsync("scrollToTop");
+ var module = await EnsureViewportModuleAsync();
+ await module.InvokeVoidAsync("scrollToTop", ".reader-canvas");
}
catch (Exception ex)
{
diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
index dd34ba1..cdf2008 100644
--- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
+++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging;
namespace NexusReader.UI.Shared.Services;
-public sealed partial class KnowledgeCoordinator : IDisposable
+public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
{
private readonly IKnowledgeService _knowledgeService;
private readonly IKnowledgeGraphService _graphService;
@@ -78,15 +78,39 @@ public sealed partial class KnowledgeCoordinator : IDisposable
}
}
+ private void CancelAndDisposeCts(ref CancellationTokenSource? cts)
+ {
+ var localCts = cts;
+ cts = null;
+ if (localCts != null)
+ {
+ try
+ {
+ localCts.Cancel();
+ }
+ catch (ObjectDisposedException) { }
+ finally
+ {
+ localCts.Dispose();
+ }
+ }
+ }
+
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null)
{
- _graphCts?.Cancel();
- _graphCts?.Dispose();
+ if (string.IsNullOrWhiteSpace(fullContent))
+ {
+ CancelAndDisposeCts(ref _graphCts);
+ await _graphService.Clear();
+ await _graphService.SetLoading(false);
+ CurrentFullPageContent = string.Empty;
+ return;
+ }
+
+ CancelAndDisposeCts(ref _graphCts);
_graphCts = new CancellationTokenSource();
var token = _graphCts.Token;
- if (string.IsNullOrWhiteSpace(fullContent)) return;
-
CurrentFullPageContent = fullContent;
LogGeneratingGraph(tenantId);
@@ -135,8 +159,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
public async Task> RequestSummaryAndQuizAsync(string content, string tenantId = "global")
{
- _quizCts?.Cancel();
- _quizCts?.Dispose();
+ CancelAndDisposeCts(ref _quizCts);
_quizCts = new CancellationTokenSource();
var token = _quizCts.Token;
@@ -184,13 +207,8 @@ public sealed partial class KnowledgeCoordinator : IDisposable
public async Task ClearAsync()
{
- _graphCts?.Cancel();
- _graphCts?.Dispose();
- _graphCts = null;
-
- _quizCts?.Cancel();
- _quizCts?.Dispose();
- _quizCts = null;
+ CancelAndDisposeCts(ref _graphCts);
+ CancelAndDisposeCts(ref _quizCts);
CurrentFullPageContent = string.Empty;
await _graphService.Clear();
@@ -201,11 +219,26 @@ public sealed partial class KnowledgeCoordinator : IDisposable
{
_interactionService.OnNodeSelected -= HandleNodeSelected;
- _graphCts?.Cancel();
- _graphCts?.Dispose();
+ CancelAndDisposeCts(ref _graphCts);
+ CancelAndDisposeCts(ref _quizCts);
+ }
- _quizCts?.Cancel();
- _quizCts?.Dispose();
+ public async ValueTask DisposeAsync()
+ {
+ _interactionService.OnNodeSelected -= HandleNodeSelected;
+
+ CancelAndDisposeCts(ref _graphCts);
+ CancelAndDisposeCts(ref _quizCts);
+
+ try
+ {
+ await _graphService.Clear();
+ await _quizService.SetQuiz(null, null);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error clearing services during KnowledgeCoordinator disposal.");
+ }
}
[LoggerMessage(Level = LogLevel.Information, Message = "[KnowledgeCoordinator] Generating full page graph for tenant: {TenantId}")]
diff --git a/src/NexusReader.UI.Shared/wwwroot/js/viewport.js b/src/NexusReader.UI.Shared/wwwroot/js/viewport.js
index d4cb965..e456f4c 100644
--- a/src/NexusReader.UI.Shared/wwwroot/js/viewport.js
+++ b/src/NexusReader.UI.Shared/wwwroot/js/viewport.js
@@ -39,8 +39,10 @@ export function scrollIntoView(id) {
return false;
}
-export function scrollToTop() {
- const el = document.querySelector('.reader-canvas');
+// NOTE: Assumes the selector matches the active scroll container (default '.reader-canvas').
+// Scoping is flexible to avoid issues if SSR pre-render or animated layouts render multiple wrappers.
+export function scrollToTop(selector = '.reader-canvas') {
+ const el = document.querySelector(selector);
if (el) {
el.scrollTop = 0;
return true;
@@ -48,3 +50,4 @@ export function scrollToTop() {
return false;
}
+