feat(ui/arch): Optimize Graph Dynamics, Immersive Reader, and Core Stability (#19)
This PR introduces a major optimization of graph dynamics, immersive reading experience, and architectural stabilization. ### 🚀 Key Improvements - **Knowledge Graph (Fix #16)**: - Implemented smooth D3.js transitions using the General Update Pattern. - Added "Neon Flash" entry animations and dynamic node dimming for better focus. - **Immersive Reader (Fix #12)**: - Standardized centered layout (`max-width: 800px`) with **Merriweather** typography. - Optimized line-height and letter-spacing for premium readability. - **Technical Code Blocks (Fix #20)**: - High-contrast dark containers for code snippets. - **JetBrains Mono** integration and neon-accented scrollbars. - **Architectural Stabilization**: - Enforced a strict **'no async void'** policy in UI services using `Func<Task>`. - Resolved WASM runtime DI errors by implementing dummy service proxies for server-side dependencies. - Replaced generic 'Not Found' message with a branded Nexus preloader. Fixes #7, Fixes #12, Fixes #16, Fixes #20. Reviewed-on: #19 Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #19.
This commit is contained in:
@@ -6,7 +6,7 @@ public sealed class FocusModeService : IFocusModeService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
public bool IsFocusModeActive { get; private set; }
|
||||
public event Action? OnFocusModeChanged;
|
||||
public event Func<Task>? OnFocusModeChanged;
|
||||
|
||||
public FocusModeService(IJSRuntime jsRuntime)
|
||||
{
|
||||
@@ -21,7 +21,7 @@ public sealed class FocusModeService : IFocusModeService
|
||||
if (value == "true" && !IsFocusModeActive)
|
||||
{
|
||||
IsFocusModeActive = true;
|
||||
OnFocusModeChanged?.Invoke();
|
||||
if (OnFocusModeChanged != null) await OnFocusModeChanged();
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -33,7 +33,7 @@ public sealed class FocusModeService : IFocusModeService
|
||||
public async Task ToggleAsync()
|
||||
{
|
||||
IsFocusModeActive = !IsFocusModeActive;
|
||||
OnFocusModeChanged?.Invoke();
|
||||
if (OnFocusModeChanged != null) await OnFocusModeChanged();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace NexusReader.UI.Shared.Services;
|
||||
public interface IFocusModeService
|
||||
{
|
||||
bool IsFocusModeActive { get; }
|
||||
event Action? OnFocusModeChanged;
|
||||
event Func<Task>? OnFocusModeChanged;
|
||||
Task InitializeAsync();
|
||||
Task ToggleAsync();
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ public interface IKnowledgeGraphService
|
||||
string? ActiveNodeId { get; }
|
||||
bool IsLoading { get; }
|
||||
|
||||
event Action? OnGraphUpdated;
|
||||
event Action<string>? OnActiveNodeChanged;
|
||||
event Action<bool>? OnLoadingChanged;
|
||||
event Func<Task>? OnGraphUpdated;
|
||||
event Func<string, Task>? OnActiveNodeChanged;
|
||||
event Func<bool, Task>? OnLoadingChanged;
|
||||
|
||||
void UpdateGraph(GraphDataDto newData);
|
||||
void SetActiveNode(string nodeId);
|
||||
void SetLoading(bool isLoading);
|
||||
void Clear();
|
||||
Task UpdateGraph(GraphDataDto newData);
|
||||
Task SetActiveNode(string nodeId);
|
||||
Task SetLoading(bool isLoading);
|
||||
Task Clear();
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ public interface IQuizStateService
|
||||
bool IsHydrating { get; }
|
||||
bool HasNewQuiz { get; }
|
||||
|
||||
event Action<string>? OnQuizRequested;
|
||||
event Action? OnQuizUpdated;
|
||||
event Func<string, Task>? OnQuizRequested;
|
||||
event Func<Task>? OnQuizUpdated;
|
||||
|
||||
void RequestQuiz(string blockId);
|
||||
void SetQuiz(string? blockId, QuizDto quiz);
|
||||
void SetHydrating(bool hydrating);
|
||||
void MarkQuizAsSeen();
|
||||
Task RequestQuiz(string blockId);
|
||||
Task SetQuiz(string? blockId, QuizDto? quiz);
|
||||
Task SetHydrating(bool hydrating);
|
||||
Task MarkQuizAsSeen();
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public interface IReaderInteractionService
|
||||
{
|
||||
event Action<string>? OnNodeSelected;
|
||||
event Action<string>? OnScrollToBlockRequested;
|
||||
event Action<string>? OnHighlightBlockRequested;
|
||||
event Action<string, string, SelectionCoordinates>? OnTextSelected;
|
||||
event Func<string, Task>? OnNodeSelected;
|
||||
event Func<string, Task>? OnScrollToBlockRequested;
|
||||
event Func<string, Task>? OnHighlightBlockRequested;
|
||||
event Func<string, string, SelectionCoordinates, Task>? OnTextSelected;
|
||||
|
||||
void NotifyNodeSelected(string nodeId);
|
||||
void RequestScrollToBlock(string blockId);
|
||||
void RequestHighlightBlock(string blockId);
|
||||
void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
|
||||
Task NotifyNodeSelected(string nodeId);
|
||||
Task RequestScrollToBlock(string blockId);
|
||||
Task RequestHighlightBlock(string blockId);
|
||||
Task NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
|
||||
}
|
||||
|
||||
public record SelectionCoordinates(double Top, double Left, double Width);
|
||||
|
||||
@@ -6,6 +6,6 @@ public interface ISyncService
|
||||
{
|
||||
Task<Result> InitializeAsync();
|
||||
Task<Result> UpdateProgressAsync(string pageId);
|
||||
event Action<string, DateTime> OnProgressReceived;
|
||||
event Func<string, DateTime, Task> OnProgressReceived;
|
||||
Task DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ namespace NexusReader.UI.Shared.Services;
|
||||
public interface IThemeService
|
||||
{
|
||||
bool IsLightMode { get; }
|
||||
event Action? OnThemeChanged;
|
||||
void ToggleTheme();
|
||||
event Func<Task>? OnThemeChanged;
|
||||
Task ToggleTheme();
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
_interactionService.OnNodeSelected += HandleNodeSelected;
|
||||
}
|
||||
|
||||
private void HandleNodeSelected(string nodeId)
|
||||
private async Task HandleNodeSelected(string nodeId)
|
||||
{
|
||||
_interactionService.RequestScrollToBlock(nodeId);
|
||||
_interactionService.RequestHighlightBlock(nodeId);
|
||||
await _interactionService.RequestScrollToBlock(nodeId);
|
||||
await _interactionService.RequestHighlightBlock(nodeId);
|
||||
}
|
||||
|
||||
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
|
||||
@@ -48,8 +48,8 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
|
||||
LogGeneratingGraph(tenantId);
|
||||
|
||||
_graphService.Clear();
|
||||
_graphService.SetLoading(true);
|
||||
await _graphService.Clear();
|
||||
await _graphService.SetLoading(true);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -59,7 +59,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
var packet = result.Value;
|
||||
if (packet.Graph != null)
|
||||
{
|
||||
_graphService.UpdateGraph(packet.Graph);
|
||||
await _graphService.UpdateGraph(packet.Graph);
|
||||
OnGraphUpdated?.Invoke(packet.Graph);
|
||||
await _platformService.VibrateSuccessAsync();
|
||||
}
|
||||
@@ -71,10 +71,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBlockReached(string blockId, string content)
|
||||
public async Task OnBlockReachedAsync(string blockId, string content)
|
||||
{
|
||||
// Only update active node for "TU JESTEŚ" logic, do NOT trigger highlight here
|
||||
_graphService.SetActiveNode(blockId);
|
||||
await _graphService.SetActiveNode(blockId);
|
||||
}
|
||||
|
||||
public async Task<KnowledgePacket?> RequestSummaryAndQuizAsync(string content, string tenantId = "global")
|
||||
@@ -109,9 +109,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
_graphService.Clear();
|
||||
await _graphService.Clear();
|
||||
_quizService.SetQuiz(null, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,36 +8,36 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService
|
||||
public string? ActiveNodeId { get; private set; }
|
||||
public bool IsLoading { get; private set; }
|
||||
|
||||
public event Action? OnGraphUpdated;
|
||||
public event Action<string>? OnActiveNodeChanged;
|
||||
public event Action<bool>? OnLoadingChanged;
|
||||
public event Func<Task>? OnGraphUpdated;
|
||||
public event Func<string, Task>? OnActiveNodeChanged;
|
||||
public event Func<bool, Task>? OnLoadingChanged;
|
||||
|
||||
public void UpdateGraph(GraphDataDto newData)
|
||||
public async Task UpdateGraph(GraphDataDto newData)
|
||||
{
|
||||
CurrentGraphData = newData;
|
||||
IsLoading = false;
|
||||
OnLoadingChanged?.Invoke(false);
|
||||
OnGraphUpdated?.Invoke();
|
||||
if (OnLoadingChanged != null) await OnLoadingChanged(false);
|
||||
if (OnGraphUpdated != null) await OnGraphUpdated();
|
||||
}
|
||||
|
||||
public void SetActiveNode(string nodeId)
|
||||
public async Task SetActiveNode(string nodeId)
|
||||
{
|
||||
if (ActiveNodeId == nodeId) return;
|
||||
ActiveNodeId = nodeId;
|
||||
OnActiveNodeChanged?.Invoke(nodeId);
|
||||
if (OnActiveNodeChanged != null) await OnActiveNodeChanged(nodeId);
|
||||
}
|
||||
|
||||
public void SetLoading(bool isLoading)
|
||||
public async Task SetLoading(bool isLoading)
|
||||
{
|
||||
IsLoading = isLoading;
|
||||
OnLoadingChanged?.Invoke(isLoading);
|
||||
if (OnLoadingChanged != null) await OnLoadingChanged(isLoading);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
public async Task Clear()
|
||||
{
|
||||
CurrentGraphData = null;
|
||||
ActiveNodeId = null;
|
||||
IsLoading = false;
|
||||
OnGraphUpdated?.Invoke();
|
||||
if (OnGraphUpdated != null) await OnGraphUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,34 +9,34 @@ public sealed class QuizStateService : IQuizStateService
|
||||
public bool IsHydrating { get; private set; }
|
||||
public bool HasNewQuiz { get; private set; }
|
||||
|
||||
public event Action<string>? OnQuizRequested;
|
||||
public event Action? OnQuizUpdated;
|
||||
public event Func<string, Task>? OnQuizRequested;
|
||||
public event Func<Task>? OnQuizUpdated;
|
||||
|
||||
public void RequestQuiz(string blockId)
|
||||
public async Task RequestQuiz(string blockId)
|
||||
{
|
||||
CurrentQuizBlockId = blockId;
|
||||
OnQuizRequested?.Invoke(blockId);
|
||||
if (OnQuizRequested != null) await OnQuizRequested(blockId);
|
||||
}
|
||||
|
||||
public void SetQuiz(string? blockId, QuizDto quiz)
|
||||
public async Task SetQuiz(string? blockId, QuizDto? quiz)
|
||||
{
|
||||
CurrentQuizBlockId = blockId;
|
||||
CurrentQuiz = quiz;
|
||||
IsHydrating = false;
|
||||
HasNewQuiz = true;
|
||||
OnQuizUpdated?.Invoke();
|
||||
HasNewQuiz = quiz != null;
|
||||
if (OnQuizUpdated != null) await OnQuizUpdated();
|
||||
}
|
||||
|
||||
public void SetHydrating(bool hydrating)
|
||||
public async Task SetHydrating(bool hydrating)
|
||||
{
|
||||
IsHydrating = hydrating;
|
||||
OnQuizUpdated?.Invoke();
|
||||
if (OnQuizUpdated != null) await OnQuizUpdated();
|
||||
}
|
||||
|
||||
public void MarkQuizAsSeen()
|
||||
public async Task MarkQuizAsSeen()
|
||||
{
|
||||
if (!HasNewQuiz) return;
|
||||
HasNewQuiz = false;
|
||||
OnQuizUpdated?.Invoke();
|
||||
if (OnQuizUpdated != null) await OnQuizUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@ namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public sealed class ReaderInteractionService : IReaderInteractionService
|
||||
{
|
||||
public event Action<string>? OnNodeSelected;
|
||||
public event Action<string>? OnScrollToBlockRequested;
|
||||
public event Action<string>? OnHighlightBlockRequested;
|
||||
public event Action<string, string, SelectionCoordinates>? OnTextSelected;
|
||||
public event Func<string, Task>? OnNodeSelected;
|
||||
public event Func<string, Task>? OnScrollToBlockRequested;
|
||||
public event Func<string, Task>? OnHighlightBlockRequested;
|
||||
public event Func<string, string, SelectionCoordinates, Task>? OnTextSelected;
|
||||
|
||||
public void NotifyNodeSelected(string nodeId)
|
||||
public async Task NotifyNodeSelected(string nodeId)
|
||||
{
|
||||
OnNodeSelected?.Invoke(nodeId);
|
||||
if (OnNodeSelected != null) await OnNodeSelected(nodeId);
|
||||
}
|
||||
|
||||
public void RequestScrollToBlock(string blockId)
|
||||
public async Task RequestScrollToBlock(string blockId)
|
||||
{
|
||||
OnScrollToBlockRequested?.Invoke(blockId);
|
||||
if (OnScrollToBlockRequested != null) await OnScrollToBlockRequested(blockId);
|
||||
}
|
||||
|
||||
public void RequestHighlightBlock(string blockId)
|
||||
public async Task RequestHighlightBlock(string blockId)
|
||||
{
|
||||
OnHighlightBlockRequested?.Invoke(blockId);
|
||||
if (OnHighlightBlockRequested != null) await OnHighlightBlockRequested(blockId);
|
||||
}
|
||||
|
||||
public void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords)
|
||||
public async Task NotifyTextSelected(string text, string blockId, SelectionCoordinates coords)
|
||||
{
|
||||
OnTextSelected?.Invoke(text, blockId, coords);
|
||||
if (OnTextSelected != null) await OnTextSelected(text, blockId, coords);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class SyncService : ISyncService, IAsyncDisposable
|
||||
private bool _isInitialized;
|
||||
private CancellationTokenSource? _debounceCts;
|
||||
|
||||
public event Action<string, DateTime>? OnProgressReceived;
|
||||
public event Func<string, DateTime, Task>? OnProgressReceived;
|
||||
|
||||
public SyncService(
|
||||
HttpClient httpClient,
|
||||
@@ -44,9 +44,9 @@ public class SyncService : ISyncService, IAsyncDisposable
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_hubConnection.On<string, DateTime>("ProgressUpdated", (pageId, timestamp) =>
|
||||
_hubConnection.On<string, DateTime>("ProgressUpdated", async (pageId, timestamp) =>
|
||||
{
|
||||
OnProgressReceived?.Invoke(pageId, timestamp);
|
||||
if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);
|
||||
});
|
||||
|
||||
try
|
||||
|
||||
@@ -3,11 +3,11 @@ namespace NexusReader.UI.Shared.Services;
|
||||
public sealed class ThemeService : IThemeService
|
||||
{
|
||||
public bool IsLightMode { get; private set; } = false;
|
||||
public event Action? OnThemeChanged;
|
||||
public event Func<Task>? OnThemeChanged;
|
||||
|
||||
public void ToggleTheme()
|
||||
public async Task ToggleTheme()
|
||||
{
|
||||
IsLightMode = !IsLightMode;
|
||||
OnThemeChanged?.Invoke();
|
||||
if (OnThemeChanged != null) await OnThemeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user