aa80c2ba3e
This PR fully implements the Global Chapter-Level Quiz Generation system in the NexusReader application. ### Key Accomplishments: 1. **SubmitQuizResultCommand**: Added MediatR command and handler to persist completed quiz results to the SQLite database securely, using our clean architecture result-pattern. 2. **Dynamic Dashboard Integration**: Re-engineered the user dashboard to fetch, calculate, and display real-time statistics (average score, total books read, total concept nodes mapped, and list of resolved quizzes with their dates and scores) directly from active database queries, eliminating static mockups. 3. **Haptic & Visual Feedback**: Enhanced the quiz flow with interactive CSS transitions, glowing hover feedback, and clear result visualization upon completion. 4. **Robust Verification**: Implemented comprehensive unit tests for `SubmitQuizResultCommandHandler` covering all success and failure/edge cases. Executed full `dotnet test` with 100% success rate. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #53 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
130 lines
4.2 KiB
C#
130 lines
4.2 KiB
C#
using FluentResults;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using Microsoft.Extensions.Logging;
|
|
using NexusReader.Application.Abstractions.Services;
|
|
|
|
namespace NexusReader.UI.Shared.Services;
|
|
|
|
public class SyncService : ISyncService, IAsyncDisposable
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly INativeStorageService _storageService;
|
|
private readonly IPlatformService _platformService;
|
|
private readonly ILogger<SyncService> _logger;
|
|
private HubConnection? _hubConnection;
|
|
private bool _isInitialized;
|
|
private CancellationTokenSource? _debounceCts;
|
|
|
|
public event Func<string, DateTime, Task>? OnProgressReceived;
|
|
public event Func<string, double, Task>? OnIngestionProgressReceived;
|
|
|
|
public SyncService(
|
|
HttpClient httpClient,
|
|
INativeStorageService storageService,
|
|
IPlatformService platformService,
|
|
ILogger<SyncService> logger)
|
|
{
|
|
_httpClient = httpClient;
|
|
_storageService = storageService;
|
|
_platformService = platformService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<Result> InitializeAsync()
|
|
{
|
|
if (_isInitialized) return Result.Ok();
|
|
|
|
var tokenResult = await _storageService.GetSecureString("nexus_auth_token");
|
|
if (tokenResult.IsFailed) return Result.Fail("Not authenticated");
|
|
|
|
var baseUrl = _httpClient.BaseAddress?.ToString() ?? "http://localhost:5000/";
|
|
var hubUrl = new Uri(new Uri(baseUrl), "synchub").ToString();
|
|
|
|
_hubConnection = new HubConnectionBuilder()
|
|
.WithUrl(hubUrl, options =>
|
|
{
|
|
options.AccessTokenProvider = () => Task.FromResult<string?>(tokenResult.Value);
|
|
})
|
|
.WithAutomaticReconnect()
|
|
.Build();
|
|
|
|
_hubConnection.On<string, DateTime>("ProgressUpdated", async (pageId, timestamp) =>
|
|
{
|
|
// Note: In the future we might want to receive ebookId and progress here too
|
|
if (pageId == _lastSentPageId)
|
|
{
|
|
_logger.LogDebug("[Sync] Ignoring self progress update for page {PageId}.", pageId);
|
|
return;
|
|
}
|
|
_lastSentPageId = pageId; // Prevent echoing back duplicate progress updates
|
|
if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);
|
|
});
|
|
|
|
_hubConnection.On<string, double>("IngestionProgress", async (message, progress) =>
|
|
{
|
|
if (OnIngestionProgressReceived != null) await OnIngestionProgressReceived(message, progress);
|
|
});
|
|
|
|
try
|
|
{
|
|
await _hubConnection.StartAsync();
|
|
_isInitialized = true;
|
|
return Result.Ok();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Result.Fail(ex.Message);
|
|
}
|
|
}
|
|
|
|
private string? _lastSentPageId;
|
|
|
|
public async Task<Result> UpdateProgressAsync(string pageId, Guid ebookId, double progress, string? chapterTitle, int chapterIndex)
|
|
{
|
|
if (pageId == _lastSentPageId) return Result.Ok();
|
|
|
|
_lastSentPageId = pageId;
|
|
|
|
// Proper trailing-edge debounce
|
|
_debounceCts?.Cancel();
|
|
_debounceCts = new CancellationTokenSource();
|
|
var token = _debounceCts.Token;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(2000, token);
|
|
|
|
if (!_isInitialized) await InitializeAsync();
|
|
|
|
if (_hubConnection?.State == HubConnectionState.Connected)
|
|
{
|
|
await _hubConnection.SendAsync("UpdateProgress", pageId, ebookId, progress, chapterTitle, chapterIndex);
|
|
}
|
|
}
|
|
catch (TaskCanceledException) { /* Ignored, user kept scrolling */ }
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[SyncService] Error sending reading progress for page {PageId}.", pageId);
|
|
}
|
|
}, token);
|
|
|
|
return Result.Ok();
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
_debounceCts?.Cancel();
|
|
if (_hubConnection != null)
|
|
{
|
|
await _hubConnection.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
async ValueTask IAsyncDisposable.DisposeAsync()
|
|
{
|
|
await DisposeAsync();
|
|
}
|
|
}
|