From a34d9293303a2d8ea69518702c33be4ce8119e6b Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 25 Jul 2026 12:19:07 +0300 Subject: [PATCH] Add rename show functionality: implement API endpoint for renaming shows, update Show model to support name changes, and enhance ShowMetadataCard UI for name input. Update translations for new UI elements to improve user experience. --- .../TeleWave.Api/Endpoints/ShowEndpoints.cs | 15 +++++++ .../Library/RenameShow/RenameShowCommand.cs | 7 ++++ .../RenameShow/RenameShowCommandHandler.cs | 23 +++++++++++ .../RenameShow/RenameShowCommandValidator.cs | 11 +++++ backend/src/TeleWave.Domain/Library/Show.cs | 3 ++ .../features/admin/shows/ShowMetadataCard.tsx | 40 ++++++++++--------- frontend/src/features/admin/shows/api.ts | 4 ++ frontend/src/shared/lib/i18n.ts | 4 +- 8 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommand.cs create mode 100644 backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandHandler.cs create mode 100644 backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandValidator.cs diff --git a/backend/src/TeleWave.Api/Endpoints/ShowEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/ShowEndpoints.cs index 33e467c..5926849 100644 --- a/backend/src/TeleWave.Api/Endpoints/ShowEndpoints.cs +++ b/backend/src/TeleWave.Api/Endpoints/ShowEndpoints.cs @@ -7,6 +7,7 @@ using TeleWave.Application.Library.DeleteShow; using TeleWave.Application.Library.GetShow; using TeleWave.Application.Library.ListShows; using TeleWave.Application.Library.RemoveEpisode; +using TeleWave.Application.Library.RenameShow; using TeleWave.Application.Library.SetShowOriginalName; using TeleWave.Infrastructure.Identity; @@ -23,6 +24,7 @@ public static class ShowEndpoints admin.MapPost("", CreateShow).Produces(StatusCodes.Status201Created); admin.MapGet("", ListShows).Produces>(); admin.MapGet("/{id:guid}", GetShow).Produces(); + admin.MapPut("/{id:guid}/name", Rename).Produces(StatusCodes.Status204NoContent); admin .MapPut("/{id:guid}/original-name", SetOriginalName) .Produces(StatusCodes.Status204NoContent); @@ -65,6 +67,17 @@ public static class ShowEndpoints return result.ToHttpResult(); } + private static async Task Rename( + Guid id, + RenameShowBody body, + ISender sender, + CancellationToken cancellationToken + ) + { + var result = await sender.Send(new RenameShowCommand(id, body.Name), cancellationToken); + return result.ToHttpResult(); + } + private static async Task SetOriginalName( Guid id, SetShowOriginalNameBody body, @@ -116,4 +129,6 @@ public static class ShowEndpoints public sealed record AddEpisodeBody(Guid MediaAssetId); +public sealed record RenameShowBody(string Name); + public sealed record SetShowOriginalNameBody(string? OriginalName); diff --git a/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommand.cs b/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommand.cs new file mode 100644 index 0000000..fe6a35b --- /dev/null +++ b/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommand.cs @@ -0,0 +1,7 @@ +using LiteCqrs; +using TeleWave.Application.Common.Models; + +namespace TeleWave.Application.Library.RenameShow; + +/// Изменить отображаемое название шоу. +public sealed record RenameShowCommand(Guid Id, string Name) : ICommand; diff --git a/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandHandler.cs b/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandHandler.cs new file mode 100644 index 0000000..5bc5347 --- /dev/null +++ b/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandHandler.cs @@ -0,0 +1,23 @@ +using LiteCqrs; +using Microsoft.EntityFrameworkCore; +using TeleWave.Application.Common.Interfaces; +using TeleWave.Application.Common.Models; + +namespace TeleWave.Application.Library.RenameShow; + +public sealed class RenameShowCommandHandler(IAppDbContext dbContext) + : ICommandHandler +{ + public async Task Handle(RenameShowCommand command, CancellationToken cancellationToken) + { + var show = await dbContext.Shows.FirstOrDefaultAsync( + s => s.Id == command.Id, + cancellationToken + ); + if (show is null) + return Result.Failure(ShowErrors.NotFound); + + show.SetName(command.Name.Trim()); + return Result.Success(); + } +} diff --git a/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandValidator.cs b/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandValidator.cs new file mode 100644 index 0000000..4cfd14b --- /dev/null +++ b/backend/src/TeleWave.Application/Library/RenameShow/RenameShowCommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace TeleWave.Application.Library.RenameShow; + +public sealed class RenameShowCommandValidator : AbstractValidator +{ + public RenameShowCommandValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(256); + } +} diff --git a/backend/src/TeleWave.Domain/Library/Show.cs b/backend/src/TeleWave.Domain/Library/Show.cs index ea31ec1..bf72148 100644 --- a/backend/src/TeleWave.Domain/Library/Show.cs +++ b/backend/src/TeleWave.Domain/Library/Show.cs @@ -60,6 +60,9 @@ public class Show Description = description; } + /// Изменить отображаемое название (на экранах). Метаданные ищутся по . + public void SetName(string name) => Name = name; + /// Задать/снять оригинальное название (пустая строка трактуется как отсутствие). public void SetOriginalName(string? originalName) => OriginalName = Normalize(originalName); diff --git a/frontend/src/features/admin/shows/ShowMetadataCard.tsx b/frontend/src/features/admin/shows/ShowMetadataCard.tsx index 962ca89..627db66 100644 --- a/frontend/src/features/admin/shows/ShowMetadataCard.tsx +++ b/frontend/src/features/admin/shows/ShowMetadataCard.tsx @@ -16,6 +16,7 @@ import { clearMetadata, getMetadataProviders, refreshEpisodesMetadata, + renameShow, searchMetadata, setShowOriginalName, setShowPoster, @@ -26,8 +27,8 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged const { t } = useTranslation() const [galleryOpen, setGalleryOpen] = useState(false) const [provider, setProvider] = useState('') + const [name, setName] = useState(show.name) const [originalName, setOriginalName] = useState(show.originalName ?? '') - const [query, setQuery] = useState(show.originalName || show.name) const [results, setResults] = useState([]) const [searched, setSearched] = useState(false) const [description, setDescription] = useState(show.description ?? '') @@ -52,9 +53,11 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged }) const effectiveProvider = provider || providers?.[0] || '' + // Метаданные ищем по оригинальному названию (иначе — по обычному). + const searchTerm = originalName.trim() || name.trim() const search = useMutation({ - mutationFn: () => searchMetadata(effectiveProvider, query.trim()), + mutationFn: () => searchMetadata(effectiveProvider, searchTerm), onSuccess: (data) => { setResults(data) setSearched(true) @@ -76,10 +79,11 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged const save = useMutation({ mutationFn: async () => { const yearNum = year.trim() ? Number(year) : null - const nameChanged = originalName.trim() !== (show.originalName ?? '') const infoChanged = (description.trim() || null) !== (show.description ?? null) || yearNum !== (show.year ?? null) - if (nameChanged) await setShowOriginalName(show.id, originalName.trim() || null) + if (name.trim() && name.trim() !== show.name) await renameShow(show.id, name.trim()) + if (originalName.trim() !== (show.originalName ?? '')) + await setShowOriginalName(show.id, originalName.trim() || null) if (infoChanged) await updateMetadata(show.id, { description: description.trim() || null, year: yearNum }) }, @@ -140,31 +144,29 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged /> - {/* Поиск + ручная правка */} + {/* Название + поиск + ручная правка */}
+
+ + setName(e.target.value)} /> +
+
setOriginalName(e.target.value)} + onChange={(e) => { + setOriginalName(e.target.value) + setSearched(false) + }} />

{t('admin.metadata.originalNameHint')}

- {providers && providers.length > 0 && ( + {providers && providers.length > 0 && (searched || results.length > 0) && (
- - { - setQuery(e.target.value) - setSearched(false) - }} - placeholder={t('admin.metadata.searchPlaceholder')} - /> - {searched && results.length === 0 && (

{t('admin.metadata.nothingFound')}

)} @@ -241,14 +243,14 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged )} - {linked && ( diff --git a/frontend/src/features/admin/shows/api.ts b/frontend/src/features/admin/shows/api.ts index 4fc761e..ca5a089 100644 --- a/frontend/src/features/admin/shows/api.ts +++ b/frontend/src/features/admin/shows/api.ts @@ -24,6 +24,10 @@ export function createShow(body: { return apiRequest('/admin/shows', { method: 'POST', body }) } +export function renameShow(id: string, name: string) { + return apiRequest(`/admin/shows/${id}/name`, { method: 'PUT', body: { name } }) +} + export function setShowOriginalName(id: string, originalName: string | null) { return apiRequest(`/admin/shows/${id}/original-name`, { method: 'PUT', diff --git a/frontend/src/shared/lib/i18n.ts b/frontend/src/shared/lib/i18n.ts index 735e6b5..6b3a424 100644 --- a/frontend/src/shared/lib/i18n.ts +++ b/frontend/src/shared/lib/i18n.ts @@ -285,7 +285,7 @@ const resources = { title: 'Метаданные', pickFromGallery: 'Выбрать из галереи', pickPoster: 'Из галереи', - searchLabel: 'Поиск метаданных', + name: 'Название', originalName: 'Оригинальное название (eng)', originalNamePlaceholder: 'Например: Family Guy', originalNameHint: 'По нему ищутся метаданные; на экранах показывается обычное название.', @@ -591,7 +591,7 @@ const resources = { title: 'Metadata', pickFromGallery: 'Pick from gallery', pickPoster: 'From gallery', - searchLabel: 'Metadata search', + name: 'Name', originalName: 'Original name (eng)', originalNamePlaceholder: 'e.g. Family Guy', originalNameHint: 'Metadata is looked up by this; screens still show the regular name.',