feat: implement cross-device reading progress synchronization using SignalR and remove legacy quiz generation services.
This commit is contained in:
@@ -65,7 +65,6 @@ public static class DependencyInjection
|
||||
}));
|
||||
|
||||
services.AddScoped<IKnowledgeService, KnowledgeService>();
|
||||
services.AddTransient<IAiGenerateQuizService, FakeAiGenerateQuizService>();
|
||||
services.AddTransient<IEpubService, EpubService>();
|
||||
|
||||
services.AddAuthorizationCore(options =>
|
||||
@@ -75,6 +74,11 @@ public static class DependencyInjection
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, ProUserHandler>();
|
||||
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NexusReader.Application.Commands.Sync;
|
||||
using NexusReader.Domain.Entities;
|
||||
using NexusReader.Infrastructure.Persistence;
|
||||
using NexusReader.Infrastructure.RealTime;
|
||||
|
||||
namespace NexusReader.Infrastructure.Handlers;
|
||||
|
||||
public class UpdateReadingProgressCommandHandler : IRequestHandler<UpdateReadingProgressCommand, Result>
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IHubContext<SyncHub> _hubContext;
|
||||
|
||||
public UpdateReadingProgressCommandHandler(
|
||||
AppDbContext context,
|
||||
IHubContext<SyncHub> hubContext)
|
||||
{
|
||||
_context = context;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
public async Task<Result> Handle(UpdateReadingProgressCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return Result.Fail("User not found.");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
user.LastReadPageId = request.PageId;
|
||||
user.LastReadAt = now;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Broadcast to other devices
|
||||
await _hubContext.Clients
|
||||
.Group($"User_{request.UserId}")
|
||||
.SendAsync("ProgressUpdated", request.PageId, now, cancellationToken);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NexusReader.Application\NexusReader.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NexusReader.Application\NexusReader.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GeminiDotnet.Extensions.AI" Version="0.23.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="10.0.7" />
|
||||
|
||||
@@ -16,6 +16,12 @@ public class AppDbContext : IdentityDbContext<NexusUser>
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<NexusUser>(entity =>
|
||||
{
|
||||
entity.Property(u => u.LastReadPageId).HasMaxLength(255);
|
||||
entity.Property(u => u.LastReadAt).IsRequired(false);
|
||||
});
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<SemanticKnowledgeCache>(entity =>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NexusReader.Application.Commands.Sync;
|
||||
|
||||
namespace NexusReader.Infrastructure.RealTime;
|
||||
|
||||
[Authorize]
|
||||
public class SyncHub : Hub
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public SyncHub(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task UpdateProgress(string pageId)
|
||||
{
|
||||
var userId = Context.UserIdentifier;
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
await _mediator.Send(new UpdateReadingProgressCommand(pageId, userId));
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var userId = Context.UserIdentifier;
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"User_{userId}");
|
||||
}
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
var userId = Context.UserIdentifier;
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"User_{userId}");
|
||||
}
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using FluentResults;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Application.Queries.Quiz;
|
||||
|
||||
namespace NexusReader.Infrastructure.Services;
|
||||
|
||||
public sealed class FakeAiGenerateQuizService : IAiGenerateQuizService
|
||||
{
|
||||
public async Task<Result<QuizDto>> GenerateQuizAsync(string contextBlockId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 2000ms delay to highlight Skeleton loader visually
|
||||
await Task.Delay(2000, cancellationToken);
|
||||
|
||||
var fakeQuiz = new List<QuizQuestionDto>
|
||||
{
|
||||
new("Co było głównym centrum włoskiego Renesansu?", new List<string> { "Wenecja", "Rzym", "Florencja", "Mediolan" }, 2),
|
||||
new("Kto stanowił wpływowy ród mecenasów sztuki?", new List<string> { "Habsburgowie", "Medyceusze", "Borgiowie", "Sforzowie" }, 1),
|
||||
new("Jaką koncepcją filozoficzną charakteryzował się renesans?", new List<string> { "Teocentryzmem", "Nihilizmem", "Humanizmem", "Egzystencjalizmem" }, 2)
|
||||
};
|
||||
|
||||
return Result.Ok(new QuizDto(fakeQuiz));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user