Add image management functionality: introduce Image entity and related API endpoints, update database schema to support image storage, and enhance UI with a new gallery feature for image selection and upload. Update translations for gallery-related terms.

This commit is contained in:
Leonid Pershin
2026-07-25 11:27:34 +03:00
parent 72451f89a8
commit 149cd153b9
34 changed files with 1732 additions and 11 deletions
@@ -0,0 +1,8 @@
using LiteCqrs;
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Images.DeleteImage;
/// <summary>Удаляет изображение из реестра и его файл. Ссылки на него у потребителей могут повиснуть
/// (отдача вернёт 404, UI покажет заглушку).</summary>
public sealed record DeleteImageCommand(Guid Id) : ICommand<Result>;
@@ -0,0 +1,24 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Images.DeleteImage;
public sealed class DeleteImageCommandHandler(IAppDbContext dbContext, IImageStore storage)
: ICommandHandler<DeleteImageCommand, Result>
{
public async Task<Result> Handle(DeleteImageCommand command, CancellationToken cancellationToken)
{
var image = await dbContext.Images.FirstOrDefaultAsync(
i => i.Id == command.Id,
cancellationToken
);
if (image is null)
return Result.Failure(ImageErrors.NotFound);
storage.Delete(image.Id, image.FileExtension);
dbContext.Images.Remove(image);
return Result.Success();
}
}
@@ -0,0 +1,7 @@
using LiteCqrs;
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Images.GetImageFile;
/// <summary>Абсолютный путь к файлу изображения (для отдачи). Ошибка — если записи/файла нет.</summary>
public sealed record GetImageFileQuery(Guid Id) : IQuery<Result<string>>;
@@ -0,0 +1,24 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Images.GetImageFile;
public sealed class GetImageFileQueryHandler(IAppDbContext dbContext, IImageStore storage)
: IQueryHandler<GetImageFileQuery, Result<string>>
{
public async Task<Result<string>> Handle(
GetImageFileQuery query,
CancellationToken cancellationToken
)
{
var image = await dbContext.Images.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == query.Id, cancellationToken);
if (image is null)
return Result.Failure<string>(ImageErrors.NotFound);
var path = storage.ResolvePath(image.Id, image.FileExtension);
return path is null ? Result.Failure<string>(ImageErrors.NotFound) : Result.Success(path);
}
}
@@ -0,0 +1,11 @@
using TeleWave.Domain.Images;
namespace TeleWave.Application.Images;
/// <summary>Запись галереи для показа. Файл отдаётся по /api/images/{Id} (URL строит фронт).</summary>
public sealed record ImageDto(
Guid Id,
ImageCategory Category,
string? OriginalFileName,
DateTimeOffset CreatedAt
);
@@ -0,0 +1,16 @@
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Images;
public static class ImageErrors
{
public static readonly Error NotFound = Error.NotFound(
"Images.NotFound",
"Изображение не найдено."
);
public static readonly Error InvalidFile = Error.Validation(
"Images.InvalidFile",
"Недопустимый файл изображения (формат или размер)."
);
}
@@ -0,0 +1,9 @@
using LiteCqrs;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Images;
namespace TeleWave.Application.Images.ListImages;
/// <summary>Список изображений одной категории (вкладки), новые сверху.</summary>
public sealed record ListImagesQuery(ImageCategory Category)
: IQuery<Result<IReadOnlyList<ImageDto>>>;
@@ -0,0 +1,24 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Images.ListImages;
public sealed class ListImagesQueryHandler(IAppDbContext dbContext)
: IQueryHandler<ListImagesQuery, Result<IReadOnlyList<ImageDto>>>
{
public async Task<Result<IReadOnlyList<ImageDto>>> Handle(
ListImagesQuery query,
CancellationToken cancellationToken
)
{
var images = await dbContext.Images.AsNoTracking()
.Where(i => i.Category == query.Category)
.OrderByDescending(i => i.CreatedAt)
.Select(i => new ImageDto(i.Id, i.Category, i.OriginalFileName, i.CreatedAt))
.ToListAsync(cancellationToken);
return Result.Success<IReadOnlyList<ImageDto>>(images);
}
}
@@ -0,0 +1,12 @@
using LiteCqrs;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Images;
namespace TeleWave.Application.Images.UploadImage;
/// <summary>Регистрирует запись изображения (файл кладёт вызывающий эндпоинт по возвращённому Id).</summary>
public sealed record UploadImageCommand(
ImageCategory Category,
string Extension,
string? OriginalFileName
) : ICommand<Result<Guid>>;
@@ -0,0 +1,17 @@
using LiteCqrs;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Images;
namespace TeleWave.Application.Images.UploadImage;
public sealed class UploadImageCommandHandler(IAppDbContext dbContext)
: ICommandHandler<UploadImageCommand, Result<Guid>>
{
public Task<Result<Guid>> Handle(UploadImageCommand command, CancellationToken cancellationToken)
{
var image = Image.Create(command.Category, command.Extension, command.OriginalFileName);
dbContext.Images.Add(image);
return Task.FromResult(Result.Success(image.Id));
}
}