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:
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Domain.Auth;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Images;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Settings;
|
||||
@@ -16,6 +17,7 @@ public interface IAppDbContext
|
||||
DbSet<ScheduleEntry> ScheduleEntries { get; }
|
||||
DbSet<BumperAsset> BumperAssets { get; }
|
||||
DbSet<AppSetting> AppSettings { get; }
|
||||
DbSet<Image> Images { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace TeleWave.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Файловое хранилище картинок общего реестра: файлы лежат под images/{imageId}{ext} в корне
|
||||
/// хранилища и отдаются как есть (не режутся). Один файл на запись реестра.
|
||||
/// </summary>
|
||||
public interface IImageStore
|
||||
{
|
||||
Task SaveAsync(
|
||||
Guid imageId,
|
||||
string extension,
|
||||
Stream content,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Сохранить готовые байты (например, скачанный из метадаты постер).</summary>
|
||||
Task SaveAsync(
|
||||
Guid imageId,
|
||||
string extension,
|
||||
byte[] content,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
void Delete(Guid imageId, string extension);
|
||||
|
||||
/// <summary>Абсолютный путь к файлу изображения или null, если файла нет.</summary>
|
||||
string? ResolvePath(Guid imageId, string extension);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user