Merge branch 'main' into feature-whiteboards-recent-and-history
# Conflicts: # front/src/App.vue
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
|
using AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Common.Message.AddRectangle;
|
||||||
|
|
||||||
|
public record AddRectangleMessage(CreateRectangleCommand Command) : IMessage;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using AipsCore.Application.Abstract;
|
||||||
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Common.Message.AddRectangle;
|
||||||
|
|
||||||
|
public class AddRectangleMessageHandler : IMessageHandler<AddRectangleMessage>
|
||||||
|
{
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
|
public AddRectangleMessageHandler(IDispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(AddRectangleMessage message, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _dispatcher.Execute(message.Command, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using AipsCore.Domain.Models.Shape.ValueObjects;
|
|||||||
namespace AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
namespace AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
||||||
|
|
||||||
public record CreateRectangleCommand(
|
public record CreateRectangleCommand(
|
||||||
|
string Id,
|
||||||
string WhiteboardId,
|
string WhiteboardId,
|
||||||
string AuthorId,
|
string AuthorId,
|
||||||
int PositionX,
|
int PositionX,
|
||||||
@@ -11,4 +12,4 @@ public record CreateRectangleCommand(
|
|||||||
string Color,
|
string Color,
|
||||||
int EndPositionX,
|
int EndPositionX,
|
||||||
int EndPositionY,
|
int EndPositionY,
|
||||||
int BorderThickness) : ICommand<ShapeId>;
|
int BorderThickness) : ICommand;
|
||||||
@@ -13,7 +13,7 @@ using AipsCore.Domain.Models.Whiteboard.ValueObjects;
|
|||||||
|
|
||||||
namespace AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
namespace AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
||||||
|
|
||||||
public class CreateRectangleCommandHandler : ICommandHandler<CreateRectangleCommand, ShapeId>
|
public class CreateRectangleCommandHandler : ICommandHandler<CreateRectangleCommand>
|
||||||
{
|
{
|
||||||
private readonly IShapeRepository _shapeRepository;
|
private readonly IShapeRepository _shapeRepository;
|
||||||
private readonly IWhiteboardRepository _whiteboardRepository;
|
private readonly IWhiteboardRepository _whiteboardRepository;
|
||||||
@@ -28,11 +28,12 @@ public class CreateRectangleCommandHandler : ICommandHandler<CreateRectangleComm
|
|||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ShapeId> Handle(CreateRectangleCommand command, CancellationToken cancellationToken = default)
|
public async Task Handle(CreateRectangleCommand command, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Validate(command);
|
Validate(command);
|
||||||
|
|
||||||
var rectangle = Rectangle.Create(
|
var rectangle = Rectangle.Create(
|
||||||
|
command.Id,
|
||||||
command.WhiteboardId,
|
command.WhiteboardId,
|
||||||
command.AuthorId,
|
command.AuthorId,
|
||||||
command.PositionX, command.PositionY,
|
command.PositionX, command.PositionY,
|
||||||
@@ -43,8 +44,6 @@ public class CreateRectangleCommandHandler : ICommandHandler<CreateRectangleComm
|
|||||||
|
|
||||||
await _shapeRepository.SaveAsync(rectangle, cancellationToken);
|
await _shapeRepository.SaveAsync(rectangle, cancellationToken);
|
||||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return rectangle.Id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Validate(CreateRectangleCommand command)
|
private void Validate(CreateRectangleCommand command)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using AipsCore.Application.Abstract.Query;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT;
|
||||||
|
|
||||||
|
public record GetWhiteboardInfoRTQuery(Guid WhiteboardId) : IQuery<Infrastructure.Persistence.Whiteboard.Whiteboard>;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using AipsCore.Application.Abstract.Query;
|
||||||
|
using AipsCore.Domain.Common.Validation;
|
||||||
|
using AipsCore.Domain.Models.Whiteboard.Validation;
|
||||||
|
using AipsCore.Domain.Models.Whiteboard.ValueObjects;
|
||||||
|
using AipsCore.Infrastructure.Persistence.Db;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT;
|
||||||
|
|
||||||
|
public class GetWhiteboardInfoRTQueryHandler
|
||||||
|
: IQueryHandler<GetWhiteboardInfoRTQuery, Infrastructure.Persistence.Whiteboard.Whiteboard>
|
||||||
|
{
|
||||||
|
private readonly AipsDbContext _context;
|
||||||
|
|
||||||
|
public GetWhiteboardInfoRTQueryHandler(AipsDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Infrastructure.Persistence.Whiteboard.Whiteboard> Handle(GetWhiteboardInfoRTQuery query, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var whiteboard = await GetQuery(query.WhiteboardId).FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (whiteboard is null)
|
||||||
|
{
|
||||||
|
throw new ValidationException(WhiteboardErrors.NotFound(new WhiteboardId(query.WhiteboardId.ToString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return whiteboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IQueryable<Infrastructure.Persistence.Whiteboard.Whiteboard> GetQuery(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
return _context.Whiteboards
|
||||||
|
.Where(w => w.Id == whiteboardId)
|
||||||
|
.Include(w => w.Memberships)
|
||||||
|
.Include(w => w.Owner)
|
||||||
|
.Include(w => w.Shapes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ public class RabbitMqSubscriber : IMessageSubscriber
|
|||||||
|
|
||||||
await channel.QueueDeclareAsync(
|
await channel.QueueDeclareAsync(
|
||||||
queue: GetQueueName<T>(),
|
queue: GetQueueName<T>(),
|
||||||
|
autoDelete: false,
|
||||||
durable: true);
|
durable: true);
|
||||||
|
|
||||||
await channel.QueueBindAsync(
|
await channel.QueueBindAsync(
|
||||||
|
|||||||
@@ -6,4 +6,16 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AipsCore\AipsCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Hubs\Dtos\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace AipsRT.Hubs;
|
namespace AipsRT.Hubs;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class TestHub : Hub
|
public class TestHub : Hub
|
||||||
{
|
{
|
||||||
|
public override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"LOOOOOOOOOG: [{Context.UserIdentifier}] User identifier connected");
|
||||||
|
Console.WriteLine($"LOOOOOG222: [{Context.User?.Identity?.Name}] User identity name connected");
|
||||||
|
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendText(string text)
|
public async Task SendText(string text)
|
||||||
{
|
{
|
||||||
await Clients.All.SendAsync("ReceiveText", text);
|
await Clients.All.SendAsync("ReceiveText", text);
|
||||||
|
|||||||
57
dotnet/AipsRT/Hubs/WhiteboardHub.cs
Normal file
57
dotnet/AipsRT/Hubs/WhiteboardHub.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
|
using AipsRT.Model.Whiteboard;
|
||||||
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
using AipsRT.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace AipsRT.Hubs;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public class WhiteboardHub : Hub
|
||||||
|
{
|
||||||
|
private readonly WhiteboardManager _whiteboardManager;
|
||||||
|
private readonly IMessagingService _messagingService;
|
||||||
|
|
||||||
|
public WhiteboardHub(WhiteboardManager whiteboardManager, IMessagingService messagingService)
|
||||||
|
{
|
||||||
|
_whiteboardManager = whiteboardManager;
|
||||||
|
_messagingService = messagingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task JoinWhiteboard(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
if (!_whiteboardManager.WhiteboardExists(whiteboardId))
|
||||||
|
await _whiteboardManager.AddWhiteboard(whiteboardId);
|
||||||
|
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, whiteboardId.ToString());
|
||||||
|
|
||||||
|
var state = _whiteboardManager.GetWhiteboard(whiteboardId)!;
|
||||||
|
|
||||||
|
_whiteboardManager.AddUserToWhiteboard(Guid.Parse(Context.UserIdentifier!), whiteboardId);
|
||||||
|
|
||||||
|
await Clients.Caller.SendAsync("InitWhiteboard", state);
|
||||||
|
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
|
||||||
|
.SendAsync("Joined", Context.UserIdentifier!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LeaveWhiteboard(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
await Clients.GroupExcept(whiteboardId.ToString(), Context.ConnectionId)
|
||||||
|
.SendAsync("Leaved", Context.UserIdentifier!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddRectangle(Rectangle rectangle)
|
||||||
|
{
|
||||||
|
var whiteboard = _whiteboardManager.GetWhiteboardForUser(Guid.Parse(Context.UserIdentifier!))!;
|
||||||
|
|
||||||
|
rectangle.OwnerId = Guid.Parse(Context.UserIdentifier!);
|
||||||
|
|
||||||
|
await _messagingService.CreatedRectangle(whiteboard.WhiteboardId, rectangle);
|
||||||
|
|
||||||
|
whiteboard.AddRectangle(rectangle);
|
||||||
|
|
||||||
|
await Clients.GroupExcept(whiteboard.WhiteboardId.ToString(), Context.ConnectionId)
|
||||||
|
.SendAsync("AddedRectangle", rectangle);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
dotnet/AipsRT/Model/Whiteboard/GetWhiteboardService.cs
Normal file
52
dotnet/AipsRT/Model/Whiteboard/GetWhiteboardService.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using AipsCore.Application.Abstract;
|
||||||
|
using AipsCore.Application.Models.Whiteboard.Query.GetWhiteboardInfoRT;
|
||||||
|
using AipsCore.Domain.Models.Shape.Enums;
|
||||||
|
using AipsRT.Model.Whiteboard.Shapes.Map;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard;
|
||||||
|
|
||||||
|
public class GetWhiteboardService
|
||||||
|
{
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
|
public GetWhiteboardService(IDispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Whiteboard> GetWhiteboard(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
var query = new GetWhiteboardInfoRTQuery(whiteboardId);
|
||||||
|
return Map(await _dispatcher.Execute(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Whiteboard Map(AipsCore.Infrastructure.Persistence.Whiteboard.Whiteboard entity)
|
||||||
|
{
|
||||||
|
var whiteboard = new Whiteboard()
|
||||||
|
{
|
||||||
|
WhiteboardId = entity.Id,
|
||||||
|
OwnerId = entity.OwnerId,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var shape in entity.Shapes)
|
||||||
|
{
|
||||||
|
switch (shape.Type)
|
||||||
|
{
|
||||||
|
case ShapeType.Rectangle:
|
||||||
|
whiteboard.AddRectangle(shape.ToRectangle());
|
||||||
|
break;
|
||||||
|
case ShapeType.Arrow:
|
||||||
|
whiteboard.AddArrow(shape.ToArrow());
|
||||||
|
break;
|
||||||
|
case ShapeType.Line:
|
||||||
|
whiteboard.AddLine(shape.ToLine());
|
||||||
|
break;
|
||||||
|
case ShapeType.Text:
|
||||||
|
whiteboard.AddTextShape(shape.ToTextShape());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return whiteboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
dotnet/AipsRT/Model/Whiteboard/Shapes/Arrow.cs
Normal file
10
dotnet/AipsRT/Model/Whiteboard/Shapes/Arrow.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Structs;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
public class Arrow : Shape
|
||||||
|
{
|
||||||
|
public Position EndPosition { get; set; }
|
||||||
|
|
||||||
|
public int Thickness { get; set; }
|
||||||
|
}
|
||||||
10
dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs
Normal file
10
dotnet/AipsRT/Model/Whiteboard/Shapes/Line.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Structs;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
public class Line : Shape
|
||||||
|
{
|
||||||
|
public Position EndPosition { get; set; }
|
||||||
|
|
||||||
|
public int Thickness { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Structs;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard.Shapes.Map;
|
||||||
|
|
||||||
|
public static class ShapeMappingExtensions
|
||||||
|
{
|
||||||
|
extension(AipsCore.Infrastructure.Persistence.Shape.Shape shape)
|
||||||
|
{
|
||||||
|
public Rectangle ToRectangle()
|
||||||
|
{
|
||||||
|
return new Rectangle()
|
||||||
|
{
|
||||||
|
Id = shape.Id,
|
||||||
|
Position = new Position(shape.PositionX, shape.PositionY),
|
||||||
|
Color = shape.Color,
|
||||||
|
EndPosition = new Position(shape.EndPositionX!.Value, shape.EndPositionY!.Value),
|
||||||
|
BorderThickness = shape.Thickness!.Value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Arrow ToArrow()
|
||||||
|
{
|
||||||
|
return new Arrow()
|
||||||
|
{
|
||||||
|
Id = shape.Id,
|
||||||
|
Position = new Position(shape.PositionX, shape.PositionY),
|
||||||
|
Color = shape.Color,
|
||||||
|
EndPosition = new Position(shape.EndPositionX!.Value, shape.EndPositionY!.Value),
|
||||||
|
Thickness = shape.Thickness!.Value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Line ToLine()
|
||||||
|
{
|
||||||
|
return new Line()
|
||||||
|
{
|
||||||
|
Id = shape.Id,
|
||||||
|
Position = new Position(shape.PositionX, shape.PositionY),
|
||||||
|
Color = shape.Color,
|
||||||
|
EndPosition = new Position(shape.EndPositionX!.Value, shape.EndPositionY!.Value),
|
||||||
|
Thickness = shape.Thickness!.Value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextShape ToTextShape()
|
||||||
|
{
|
||||||
|
return new TextShape()
|
||||||
|
{
|
||||||
|
Id = shape.Id,
|
||||||
|
Position = new Position(shape.PositionX, shape.PositionY),
|
||||||
|
Color = shape.Color,
|
||||||
|
TextValue = shape.TextValue!,
|
||||||
|
TextSize = shape.TextSize!.Value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
dotnet/AipsRT/Model/Whiteboard/Shapes/Rectangle.cs
Normal file
10
dotnet/AipsRT/Model/Whiteboard/Shapes/Rectangle.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Structs;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
public class Rectangle : Shape
|
||||||
|
{
|
||||||
|
public Position EndPosition { get; set; }
|
||||||
|
|
||||||
|
public int BorderThickness { get; set; }
|
||||||
|
}
|
||||||
14
dotnet/AipsRT/Model/Whiteboard/Shapes/Shape.cs
Normal file
14
dotnet/AipsRT/Model/Whiteboard/Shapes/Shape.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Structs;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
public abstract class Shape
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Guid OwnerId { get; set; }
|
||||||
|
|
||||||
|
public Position Position { get; set; }
|
||||||
|
|
||||||
|
public string Color { get; set; }
|
||||||
|
}
|
||||||
10
dotnet/AipsRT/Model/Whiteboard/Shapes/TextShape.cs
Normal file
10
dotnet/AipsRT/Model/Whiteboard/Shapes/TextShape.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
public class TextShape : Shape
|
||||||
|
{
|
||||||
|
public string TextValue { get; set; }
|
||||||
|
|
||||||
|
public int TextSize { get; set; }
|
||||||
|
}
|
||||||
13
dotnet/AipsRT/Model/Whiteboard/Structs/Position.cs
Normal file
13
dotnet/AipsRT/Model/Whiteboard/Structs/Position.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace AipsRT.Model.Whiteboard.Structs;
|
||||||
|
|
||||||
|
public struct Position
|
||||||
|
{
|
||||||
|
public int X { get; set; }
|
||||||
|
public int Y { get; set; }
|
||||||
|
|
||||||
|
public Position(int x, int y)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs
Normal file
41
dotnet/AipsRT/Model/Whiteboard/Whiteboard.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard;
|
||||||
|
|
||||||
|
public class Whiteboard
|
||||||
|
{
|
||||||
|
public Guid WhiteboardId { get; set; }
|
||||||
|
|
||||||
|
public Guid OwnerId { get; set; }
|
||||||
|
|
||||||
|
public List<Shape> Shapes { get; } = [];
|
||||||
|
|
||||||
|
public List<Rectangle> Rectangles { get; } = [];
|
||||||
|
public List<Arrow> Arrows { get; } = [];
|
||||||
|
public List<Line> Lines { get; } = [];
|
||||||
|
public List<TextShape> TextShapes { get; } = [];
|
||||||
|
|
||||||
|
public void AddRectangle(Rectangle shape)
|
||||||
|
{
|
||||||
|
Shapes.Add(shape);
|
||||||
|
Rectangles.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddArrow(Arrow shape)
|
||||||
|
{
|
||||||
|
Shapes.Add(shape);
|
||||||
|
Arrows.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddLine(Line shape)
|
||||||
|
{
|
||||||
|
Shapes.Add(shape);
|
||||||
|
Lines.Add(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTextShape(TextShape shape)
|
||||||
|
{
|
||||||
|
Shapes.Add(shape);
|
||||||
|
TextShapes.Add(shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
dotnet/AipsRT/Model/Whiteboard/WhiteboardManager.cs
Normal file
59
dotnet/AipsRT/Model/Whiteboard/WhiteboardManager.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using AipsCore.Application.Abstract;
|
||||||
|
|
||||||
|
namespace AipsRT.Model.Whiteboard;
|
||||||
|
|
||||||
|
public class WhiteboardManager
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ConcurrentDictionary<Guid, Whiteboard> _whiteboards = new();
|
||||||
|
private readonly ConcurrentDictionary<Guid, Guid> _userInWhiteboards = new();
|
||||||
|
|
||||||
|
public WhiteboardManager(IServiceScopeFactory scopeFactory)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddWhiteboard(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
var getWhiteboardService = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<GetWhiteboardService>();
|
||||||
|
var whiteboard = await getWhiteboardService.GetWhiteboard(whiteboardId);
|
||||||
|
|
||||||
|
_whiteboards[whiteboardId] = whiteboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WhiteboardExists(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
return _whiteboards.ContainsKey(whiteboardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveWhiteboard(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
_whiteboards.TryRemove(whiteboardId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Whiteboard? GetWhiteboard(Guid whiteboardId)
|
||||||
|
{
|
||||||
|
return _whiteboards.GetValueOrDefault(whiteboardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUserToWhiteboard(Guid userId, Guid whiteboardId)
|
||||||
|
{
|
||||||
|
_userInWhiteboards[userId] = whiteboardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid GetUserWhiteboard(Guid userId)
|
||||||
|
{
|
||||||
|
return _userInWhiteboards[userId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveUserFromWhiteboard(Guid userId, Guid whiteboardId)
|
||||||
|
{
|
||||||
|
_userInWhiteboards.TryRemove(whiteboardId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Whiteboard? GetWhiteboardForUser(Guid userId)
|
||||||
|
{
|
||||||
|
return GetWhiteboard(GetUserWhiteboard(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,25 @@
|
|||||||
|
using AipsCore.Infrastructure.DI;
|
||||||
using AipsRT.Hubs;
|
using AipsRT.Hubs;
|
||||||
|
using AipsRT.Model.Whiteboard;
|
||||||
|
using AipsRT.Services;
|
||||||
|
using AipsRT.Services.Interfaces;
|
||||||
|
using DotNetEnv;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
Env.Load("../../.env");
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Configuration.AddEnvironmentVariables();
|
||||||
|
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
|
builder.Services.AddAips(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddScoped<GetWhiteboardService>();
|
||||||
|
builder.Services.AddSingleton<WhiteboardManager>();
|
||||||
|
builder.Services.AddSingleton<IMessagingService, MessagingService>();
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("frontend",
|
options.AddPolicy("frontend",
|
||||||
@@ -26,6 +41,11 @@ app.MapGet("/test", (IHubContext<TestHub> hubContext) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.UseCors("frontend");
|
app.UseCors("frontend");
|
||||||
app.MapHub<TestHub>("/testhub");
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapHub<TestHub>("/hubs/test");
|
||||||
|
app.MapHub<WhiteboardHub>("/hubs/whiteboard");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
8
dotnet/AipsRT/Services/Interfaces/IMessagingService.cs
Normal file
8
dotnet/AipsRT/Services/Interfaces/IMessagingService.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
|
||||||
|
namespace AipsRT.Services.Interfaces;
|
||||||
|
|
||||||
|
public interface IMessagingService
|
||||||
|
{
|
||||||
|
Task CreatedRectangle(Guid whiteboardId, Rectangle rectangle);
|
||||||
|
}
|
||||||
36
dotnet/AipsRT/Services/MessagingService.cs
Normal file
36
dotnet/AipsRT/Services/MessagingService.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
|
using AipsCore.Application.Common.Message.AddRectangle;
|
||||||
|
using AipsCore.Application.Models.Shape.Command.CreateRectangle;
|
||||||
|
using AipsRT.Model.Whiteboard.Shapes;
|
||||||
|
using AipsRT.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace AipsRT.Services;
|
||||||
|
|
||||||
|
public class MessagingService : IMessagingService
|
||||||
|
{
|
||||||
|
private readonly IMessagePublisher _messagePublisher;
|
||||||
|
|
||||||
|
public MessagingService(IMessagePublisher messagePublisher)
|
||||||
|
{
|
||||||
|
_messagePublisher = messagePublisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreatedRectangle(Guid whiteboardId, Rectangle rectangle)
|
||||||
|
{
|
||||||
|
var command = new CreateRectangleCommand(
|
||||||
|
rectangle.Id.ToString(),
|
||||||
|
whiteboardId.ToString(),
|
||||||
|
rectangle.OwnerId.ToString(),
|
||||||
|
rectangle.Position.X,
|
||||||
|
rectangle.Position.Y,
|
||||||
|
rectangle.Color,
|
||||||
|
rectangle.EndPosition.X,
|
||||||
|
rectangle.EndPosition.Y,
|
||||||
|
rectangle.BorderThickness
|
||||||
|
);
|
||||||
|
|
||||||
|
var message = new AddRectangleMessage(command);
|
||||||
|
|
||||||
|
await _messagePublisher.PublishAsync(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Reflection;
|
|||||||
using AipsCore.Application.Abstract;
|
using AipsCore.Application.Abstract;
|
||||||
using AipsCore.Application.Abstract.MessageBroking;
|
using AipsCore.Application.Abstract.MessageBroking;
|
||||||
using AipsCore.Application.Common.Message.TestMessage;
|
using AipsCore.Application.Common.Message.TestMessage;
|
||||||
|
using AipsCore.Domain.Common.Validation;
|
||||||
using AipsWorker.Utilities;
|
using AipsWorker.Utilities;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
@@ -48,7 +49,26 @@ public class WorkerService : BackgroundService
|
|||||||
|
|
||||||
private async Task HandleMessage<T>(T message, CancellationToken ct) where T : IMessage
|
private async Task HandleMessage<T>(T message, CancellationToken ct) where T : IMessage
|
||||||
{
|
{
|
||||||
await _dispatcher.Execute(message, ct);
|
try
|
||||||
|
{
|
||||||
|
await _dispatcher.Execute(message, ct);
|
||||||
|
|
||||||
|
Console.WriteLine($"OK: {message.GetType().Name}");
|
||||||
|
}
|
||||||
|
catch (ValidationException validationException)
|
||||||
|
{
|
||||||
|
Console.WriteLine("===Validation Exception: ");
|
||||||
|
foreach (var error in validationException.ValidationErrors)
|
||||||
|
{
|
||||||
|
Console.WriteLine(" * Code: " + error.Code);
|
||||||
|
Console.WriteLine(" * Message: " + error.Message);
|
||||||
|
Console.WriteLine("===================");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Unhandled Exception: " + ex.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodInfo GetMessageHandleMethod(Type messageType)
|
private MethodInfo GetMessageHandleMethod(Type messageType)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
|
import { computed } from 'vue'
|
||||||
import AppTopBar from './components/AppTopBar.vue'
|
import AppTopBar from './components/AppTopBar.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
@@ -11,12 +12,20 @@ onMounted(() => {
|
|||||||
auth.initialize()
|
auth.initialize()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const hideTopBar = computed(() => route.meta.hideTopBar === true)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppTopBar />
|
<template v-if="hideTopBar">
|
||||||
<main class="container py-4">
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</main>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<AppTopBar />
|
||||||
|
<main class="container py-4">
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
100
front/src/components/whiteboard/WhiteboardCanvas.vue
Normal file
100
front/src/components/whiteboard/WhiteboardCanvas.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useWhiteboardStore } from '@/stores/whiteboard.ts'
|
||||||
|
import type { Rectangle } from '@/types/whiteboard.ts'
|
||||||
|
import SvgRectangle from '@/components/whiteboard/shapes/SvgRectangle.vue'
|
||||||
|
import SvgDraftRect from '@/components/whiteboard/shapes/SvgDraftRect.vue'
|
||||||
|
|
||||||
|
const store = useWhiteboardStore()
|
||||||
|
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const dragStart = ref({ x: 0, y: 0 })
|
||||||
|
const dragEnd = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
const draftRect = computed(() => {
|
||||||
|
if (!isDragging.value) return null
|
||||||
|
const x = Math.min(dragStart.value.x, dragEnd.value.x)
|
||||||
|
const y = Math.min(dragStart.value.y, dragEnd.value.y)
|
||||||
|
const width = Math.abs(dragEnd.value.x - dragStart.value.x)
|
||||||
|
const height = Math.abs(dragEnd.value.y - dragStart.value.y)
|
||||||
|
return { x, y, width, height }
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMouseDown(e: MouseEvent) {
|
||||||
|
if (store.selectedTool !== 'rectangle') return
|
||||||
|
const svg = (e.currentTarget as SVGSVGElement)
|
||||||
|
const rect = svg.getBoundingClientRect()
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
dragStart.value = { x, y }
|
||||||
|
dragEnd.value = { x, y }
|
||||||
|
isDragging.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
const svg = (e.currentTarget as SVGSVGElement)
|
||||||
|
const rect = svg.getBoundingClientRect()
|
||||||
|
dragEnd.value = {
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
isDragging.value = false
|
||||||
|
|
||||||
|
const x1 = Math.min(dragStart.value.x, dragEnd.value.x)
|
||||||
|
const y1 = Math.min(dragStart.value.y, dragEnd.value.y)
|
||||||
|
const x2 = Math.max(dragStart.value.x, dragEnd.value.x)
|
||||||
|
const y2 = Math.max(dragStart.value.y, dragEnd.value.y)
|
||||||
|
|
||||||
|
if (x2 - x1 < 5 && y2 - y1 < 5) return
|
||||||
|
|
||||||
|
const rectangle: Rectangle = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
ownerId: '00000000-0000-0000-0000-000000000000',
|
||||||
|
position: { x: Math.round(x1), y: Math.round(y1) },
|
||||||
|
endPosition: { x: Math.round(x2), y: Math.round(y2) },
|
||||||
|
color: '#4f9dff',
|
||||||
|
borderThickness: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addRectangle(rectangle)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="whiteboard-canvas"
|
||||||
|
@mousedown="onMouseDown"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseup="onMouseUp"
|
||||||
|
@mouseleave="onMouseUp"
|
||||||
|
>
|
||||||
|
<SvgRectangle
|
||||||
|
v-for="rect in store.whiteboard?.rectangles"
|
||||||
|
:key="rect.id"
|
||||||
|
:rectangle="rect"
|
||||||
|
/>
|
||||||
|
<SvgDraftRect
|
||||||
|
v-if="draftRect"
|
||||||
|
:x="draftRect.x"
|
||||||
|
:y="draftRect.y"
|
||||||
|
:width="draftRect.width"
|
||||||
|
:height="draftRect.height"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.whiteboard-canvas {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #1a1a2e;
|
||||||
|
cursor: crosshair;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
front/src/components/whiteboard/WhiteboardToolbar.vue
Normal file
54
front/src/components/whiteboard/WhiteboardToolbar.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useWhiteboardStore } from '@/stores/whiteboard.ts'
|
||||||
|
import type { ShapeTool } from '@/types/whiteboard.ts'
|
||||||
|
|
||||||
|
const store = useWhiteboardStore()
|
||||||
|
const emit = defineEmits<{ leave: [] }>()
|
||||||
|
|
||||||
|
const tools: { name: ShapeTool; label: string; icon: string; enabled: boolean }[] = [
|
||||||
|
{ name: 'rectangle', label: 'Rectangle', icon: '▭', enabled: true },
|
||||||
|
{ name: 'arrow', label: 'Arrow', icon: '→', enabled: false },
|
||||||
|
{ name: 'line', label: 'Line', icon: '╱', enabled: false },
|
||||||
|
{ name: 'text', label: 'Text', icon: 'T', enabled: false },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="toolbar d-flex flex-column align-items-center py-2 gap-2">
|
||||||
|
<button
|
||||||
|
v-for="tool in tools"
|
||||||
|
:key="tool.name"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="[
|
||||||
|
store.selectedTool === tool.name ? 'btn-primary' : 'btn-outline-secondary',
|
||||||
|
{ disabled: !tool.enabled },
|
||||||
|
]"
|
||||||
|
:disabled="!tool.enabled"
|
||||||
|
:title="tool.enabled ? tool.label : `${tool.label} (coming soon)`"
|
||||||
|
style="width: 40px; height: 40px; font-size: 1.1rem"
|
||||||
|
@click="tool.enabled && store.selectTool(tool.name)"
|
||||||
|
>
|
||||||
|
{{ tool.icon }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mt-auto mb-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
title="Leave whiteboard"
|
||||||
|
style="width: 40px; height: 40px"
|
||||||
|
@click="emit('leave')"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
width: 56px;
|
||||||
|
background-color: #0d0d1a;
|
||||||
|
border-right: 1px solid #2a2a3e;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
front/src/components/whiteboard/shapes/SvgDraftRect.vue
Normal file
22
front/src/components/whiteboard/shapes/SvgDraftRect.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<rect
|
||||||
|
:x="props.x"
|
||||||
|
:y="props.y"
|
||||||
|
:width="props.width"
|
||||||
|
:height="props.height"
|
||||||
|
stroke="#4f9dff"
|
||||||
|
:stroke-width="2"
|
||||||
|
stroke-dasharray="6 3"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.7"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
17
front/src/components/whiteboard/shapes/SvgRectangle.vue
Normal file
17
front/src/components/whiteboard/shapes/SvgRectangle.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Rectangle } from '@/types/whiteboard.ts'
|
||||||
|
|
||||||
|
const props = defineProps<{ rectangle: Rectangle }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<rect
|
||||||
|
:x="Math.min(props.rectangle.position.x, props.rectangle.endPosition.x)"
|
||||||
|
:y="Math.min(props.rectangle.position.y, props.rectangle.endPosition.y)"
|
||||||
|
:width="Math.abs(props.rectangle.endPosition.x - props.rectangle.position.x)"
|
||||||
|
:height="Math.abs(props.rectangle.endPosition.y - props.rectangle.position.y)"
|
||||||
|
:stroke="props.rectangle.color"
|
||||||
|
:stroke-width="props.rectangle.borderThickness"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -35,6 +35,12 @@ const router = createRouter({
|
|||||||
component: () => import('../views/SignupView.vue'),
|
component: () => import('../views/SignupView.vue'),
|
||||||
meta: { guestOnly: true },
|
meta: { guestOnly: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/whiteboard/:id',
|
||||||
|
name: 'whiteboard',
|
||||||
|
component: () => import('../views/WhiteboardView.vue'),
|
||||||
|
meta: { requiresAuth: true, hideTopBar: true },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
HubConnectionBuilder,
|
HubConnectionBuilder,
|
||||||
HubConnectionState,
|
HubConnectionState,
|
||||||
} from "@microsoft/signalr";
|
} from "@microsoft/signalr";
|
||||||
|
import {useAuthStore} from "@/stores/auth.ts";
|
||||||
|
|
||||||
export class SignalRService {
|
export class SignalRService {
|
||||||
private connection: HubConnection;
|
private connection: HubConnection;
|
||||||
@@ -10,8 +11,12 @@ export class SignalRService {
|
|||||||
constructor(
|
constructor(
|
||||||
hubUrl: string,
|
hubUrl: string,
|
||||||
) {
|
) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
this.connection = new HubConnectionBuilder()
|
this.connection = new HubConnectionBuilder()
|
||||||
.withUrl(hubUrl)
|
.withUrl(hubUrl, {
|
||||||
|
accessTokenFactory: () => authStore.accessToken!
|
||||||
|
})
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {SignalRService} from "@/services/signalr.ts";
|
|||||||
|
|
||||||
|
|
||||||
const client = new SignalRService(
|
const client = new SignalRService(
|
||||||
`http://localhost:5039/testhub`,
|
`/hubs/test`,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const testHubService = {
|
export const testHubService = {
|
||||||
|
|||||||
49
front/src/services/whiteboardHubService.ts
Normal file
49
front/src/services/whiteboardHubService.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { SignalRService } from '@/services/signalr.ts'
|
||||||
|
import type { Rectangle, Whiteboard } from '@/types/whiteboard.ts'
|
||||||
|
|
||||||
|
const client = new SignalRService(`/hubs/whiteboard`)
|
||||||
|
|
||||||
|
export const whiteboardHubService = {
|
||||||
|
async connect() {
|
||||||
|
await client.start()
|
||||||
|
},
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
await client.stop()
|
||||||
|
},
|
||||||
|
|
||||||
|
async joinWhiteboard(id: string) {
|
||||||
|
await client.invoke('JoinWhiteboard', id)
|
||||||
|
},
|
||||||
|
|
||||||
|
async leaveWhiteboard(id: string) {
|
||||||
|
await client.invoke('LeaveWhiteboard', id)
|
||||||
|
},
|
||||||
|
|
||||||
|
async addRectangle(rectangle: Rectangle) {
|
||||||
|
await client.invoke('AddRectangle', rectangle)
|
||||||
|
},
|
||||||
|
|
||||||
|
onInitWhiteboard(callback: (whiteboard: Whiteboard) => void) {
|
||||||
|
client.on<Whiteboard>('InitWhiteboard', callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddedRectangle(callback: (rectangle: Rectangle) => void) {
|
||||||
|
client.on<Rectangle>('AddedRectangle', callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
onJoined(callback: (userId: string) => void) {
|
||||||
|
client.on<string>('Joined', callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
onLeaved(callback: (userId: string) => void) {
|
||||||
|
client.on<string>('Leaved', callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
offAll() {
|
||||||
|
client.off('InitWhiteboard')
|
||||||
|
client.off('AddedRectangle')
|
||||||
|
client.off('Joined')
|
||||||
|
client.off('Leaved')
|
||||||
|
},
|
||||||
|
}
|
||||||
88
front/src/stores/whiteboard.ts
Normal file
88
front/src/stores/whiteboard.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { Rectangle, ShapeTool, Whiteboard } from '@/types/whiteboard.ts'
|
||||||
|
import { whiteboardHubService } from '@/services/whiteboardHubService.ts'
|
||||||
|
|
||||||
|
export const useWhiteboardStore = defineStore('whiteboard', () => {
|
||||||
|
const whiteboard = ref<Whiteboard | null>(null)
|
||||||
|
const selectedTool = ref<ShapeTool>('rectangle')
|
||||||
|
const isConnected = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function joinWhiteboard(id: string) {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
await whiteboardHubService.connect()
|
||||||
|
isConnected.value = true
|
||||||
|
|
||||||
|
whiteboardHubService.onInitWhiteboard((wb) => {
|
||||||
|
whiteboard.value = wb
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
whiteboardHubService.onAddedRectangle((rectangle) => {
|
||||||
|
whiteboard.value?.rectangles.push(rectangle)
|
||||||
|
})
|
||||||
|
|
||||||
|
whiteboardHubService.onJoined((userId) => {
|
||||||
|
console.log('User joined:', userId)
|
||||||
|
})
|
||||||
|
|
||||||
|
whiteboardHubService.onLeaved((userId) => {
|
||||||
|
console.log('User left:', userId)
|
||||||
|
})
|
||||||
|
|
||||||
|
await whiteboardHubService.joinWhiteboard(id)
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? 'Failed to join whiteboard'
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveWhiteboard() {
|
||||||
|
if (!whiteboard.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await whiteboardHubService.leaveWhiteboard(whiteboard.value.whiteboardId)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Leave request failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
whiteboardHubService.offAll()
|
||||||
|
await whiteboardHubService.disconnect()
|
||||||
|
|
||||||
|
whiteboard.value = null
|
||||||
|
isConnected.value = false
|
||||||
|
selectedTool.value = 'rectangle'
|
||||||
|
error.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRectangle(rectangle: Rectangle) {
|
||||||
|
whiteboard.value?.rectangles.push(rectangle)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await whiteboardHubService.addRectangle(rectangle)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to send rectangle', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTool(tool: ShapeTool) {
|
||||||
|
selectedTool.value = tool
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
whiteboard,
|
||||||
|
selectedTool,
|
||||||
|
isConnected,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
joinWhiteboard,
|
||||||
|
leaveWhiteboard,
|
||||||
|
addRectangle,
|
||||||
|
selectTool,
|
||||||
|
}
|
||||||
|
})
|
||||||
42
front/src/types/whiteboard.ts
Normal file
42
front/src/types/whiteboard.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export interface Position {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shape {
|
||||||
|
id: string
|
||||||
|
ownerId: string
|
||||||
|
position: Position
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rectangle extends Shape {
|
||||||
|
endPosition: Position
|
||||||
|
borderThickness: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Arrow extends Shape {
|
||||||
|
endPosition: Position
|
||||||
|
thickness: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Line extends Shape {
|
||||||
|
endPosition: Position
|
||||||
|
thickness: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextShape extends Shape {
|
||||||
|
textValue: string
|
||||||
|
textSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Whiteboard {
|
||||||
|
whiteboardId: string
|
||||||
|
ownerId: string
|
||||||
|
rectangles: Rectangle[]
|
||||||
|
arrows: Arrow[]
|
||||||
|
lines: Line[]
|
||||||
|
textShapes: TextShape[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShapeTool = 'rectangle' | 'arrow' | 'line' | 'text'
|
||||||
@@ -1,13 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
import {testHubService} from "@/services/testHubService.ts";
|
import {testHubService} from "@/services/testHubService.ts";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const displayText = ref("");
|
const displayText = ref("");
|
||||||
|
const whiteboardId = ref("");
|
||||||
|
|
||||||
function addText(textToAdd: string): void {
|
function addText(textToAdd: string): void {
|
||||||
displayText.value = displayText.value + textToAdd;
|
displayText.value = displayText.value + textToAdd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function joinWhiteboard() {
|
||||||
|
const id = whiteboardId.value.trim();
|
||||||
|
if (!id) return;
|
||||||
|
router.push(`/whiteboard/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await testHubService.connect();
|
await testHubService.connect();
|
||||||
|
|
||||||
@@ -19,4 +28,19 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>{{ displayText }}</h1>
|
<h1>{{ displayText }}</h1>
|
||||||
|
|
||||||
|
<div class="mt-4" style="max-width: 500px">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
v-model="whiteboardId"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Whiteboard ID (GUID)"
|
||||||
|
@keyup.enter="joinWhiteboard"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary" @click="joinWhiteboard">
|
||||||
|
Join Whiteboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
45
front/src/views/WhiteboardView.vue
Normal file
45
front/src/views/WhiteboardView.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useWhiteboardStore } from '@/stores/whiteboard.ts'
|
||||||
|
import WhiteboardToolbar from '@/components/whiteboard/WhiteboardToolbar.vue'
|
||||||
|
import WhiteboardCanvas from '@/components/whiteboard/WhiteboardCanvas.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const store = useWhiteboardStore()
|
||||||
|
|
||||||
|
const whiteboardId = route.params.id as string
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.joinWhiteboard(whiteboardId)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
store.leaveWhiteboard()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleLeave() {
|
||||||
|
await store.leaveWhiteboard()
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="store.isLoading" class="d-flex justify-content-center align-items-center vh-100">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="store.error" class="d-flex justify-content-center align-items-center vh-100">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{{ store.error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="d-flex vh-100">
|
||||||
|
<WhiteboardToolbar @leave="handleLeave" />
|
||||||
|
<WhiteboardCanvas />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -21,6 +21,11 @@ export default defineConfig({
|
|||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:5266',
|
target: 'http://localhost:5266',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/hubs/': {
|
||||||
|
target: 'http://localhost:5039',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user