feat: implement cross-device reading progress synchronization using SignalR and remove legacy quiz generation services.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using FluentResults;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public class SyncService : ISyncService, IAsyncDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly INativeStorageService _storageService;
|
||||
private readonly IPlatformService _platformService;
|
||||
private HubConnection? _hubConnection;
|
||||
private bool _isInitialized;
|
||||
private CancellationTokenSource? _debounceCts;
|
||||
|
||||
public event Action<string, DateTime>? OnProgressReceived;
|
||||
|
||||
public SyncService(
|
||||
HttpClient httpClient,
|
||||
INativeStorageService storageService,
|
||||
IPlatformService platformService)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_storageService = storageService;
|
||||
_platformService = platformService;
|
||||
}
|
||||
|
||||
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", (pageId, timestamp) =>
|
||||
{
|
||||
OnProgressReceived?.Invoke(pageId, timestamp);
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
if (pageId == _lastSentPageId) return Result.Ok();
|
||||
|
||||
// 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, token);
|
||||
_lastSentPageId = pageId;
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { /* Ignored, user kept scrolling */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SyncService] Error sending progress: {ex.Message}");
|
||||
}
|
||||
}, token);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_debounceCts?.Cancel();
|
||||
if (_hubConnection != null)
|
||||
{
|
||||
await _hubConnection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
await DisposeAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user