Refactor bumper template background management: rename background upload endpoint to reflect new functionality, update related command and handler to use image ID instead of extension, and adjust data models to support image references. Remove obsolete background handling code and enhance UI components for improved image selection and display.
This commit is contained in:
@@ -4,6 +4,8 @@ import Hls from 'hls.js'
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronDown, ChevronLeft, RefreshCw } from 'lucide-react'
|
||||
import { imageUrl } from '@/features/admin/images/api'
|
||||
import { ImageGallery } from '@/features/admin/images/ImageGallery'
|
||||
import { getAccessToken, HttpError } from '@/shared/api/client'
|
||||
import type {
|
||||
AdInsertion,
|
||||
@@ -41,11 +43,11 @@ import {
|
||||
removeChannelAd,
|
||||
removeChannelShow,
|
||||
renderBumperPreview,
|
||||
setBumperTemplateBackground,
|
||||
updateBumperTemplate,
|
||||
updateChannelSettings,
|
||||
updateChannelShow,
|
||||
uploadBumperTemplateAudio,
|
||||
uploadBumperTemplateBackground,
|
||||
} from './api'
|
||||
|
||||
function formatTime(iso: string) {
|
||||
@@ -654,17 +656,11 @@ function BumperTemplateEditor({
|
||||
onSaved={onChanged}
|
||||
onError={onError}
|
||||
/>
|
||||
<BumperFileUpload
|
||||
<BumperBackgroundField
|
||||
channelId={channelId}
|
||||
templateId={template.id}
|
||||
kind="background"
|
||||
label={t('admin.channels.bumperBackground')}
|
||||
hint={t('admin.channels.bumperBackgroundHint')}
|
||||
has={template.hasBackground}
|
||||
accept="image/*"
|
||||
upload={uploadBumperTemplateBackground}
|
||||
clear={clearBumperTemplateBackground}
|
||||
onSaved={onChanged}
|
||||
backgroundImageId={template.backgroundImageId}
|
||||
onChanged={onChanged}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
@@ -758,6 +754,71 @@ function BumperPreviewPlayer({
|
||||
)
|
||||
}
|
||||
|
||||
function BumperBackgroundField({
|
||||
channelId,
|
||||
templateId,
|
||||
backgroundImageId,
|
||||
onChanged,
|
||||
onError,
|
||||
}: {
|
||||
channelId: string
|
||||
templateId: string
|
||||
backgroundImageId: string | null
|
||||
onChanged: () => void
|
||||
onError: (e: unknown) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [galleryOpen, setGalleryOpen] = useState(false)
|
||||
|
||||
const setBg = useMutation({
|
||||
mutationFn: (imageId: string) => setBumperTemplateBackground(channelId, templateId, imageId),
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
const clearBg = useMutation({
|
||||
mutationFn: () => clearBumperTemplateBackground(channelId, templateId),
|
||||
onSuccess: onChanged,
|
||||
onError,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>
|
||||
{t('admin.channels.bumperBackground')}{' '}
|
||||
<span className={backgroundImageId ? 'text-emerald-500' : 'text-muted-foreground'}>
|
||||
{backgroundImageId
|
||||
? `· ${t('admin.channels.bumperFileLoaded')}`
|
||||
: `· ${t('admin.channels.bumperFileDefault')}`}
|
||||
</span>
|
||||
</Label>
|
||||
<span className="text-xs text-muted-foreground">{t('admin.channels.bumperBackgroundHint')}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{backgroundImageId && (
|
||||
<img
|
||||
src={imageUrl(backgroundImageId)}
|
||||
alt=""
|
||||
className="h-10 w-16 shrink-0 rounded border border-border object-cover"
|
||||
/>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
||||
{t('admin.channels.bumperBackgroundPick')}
|
||||
</Button>
|
||||
{backgroundImageId && (
|
||||
<Button size="sm" variant="ghost" disabled={clearBg.isPending} onClick={() => clearBg.mutate()}>
|
||||
{t('admin.channels.bumperReset')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ImageGallery
|
||||
open={galleryOpen}
|
||||
onOpenChange={setGalleryOpen}
|
||||
category="BumperBackground"
|
||||
onSelect={(img) => setBg.mutate(img.id)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BumperFileUpload({
|
||||
channelId,
|
||||
templateId,
|
||||
|
||||
@@ -137,8 +137,12 @@ export function uploadBumperTemplateAudio(id: string, templateId: string, file:
|
||||
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
||||
}
|
||||
|
||||
export function uploadBumperTemplateBackground(id: string, templateId: string, file: File) {
|
||||
return uploadBumperTemplateFile(id, templateId, 'background', file)
|
||||
/** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */
|
||||
export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||
method: 'PUT',
|
||||
body: { imageId },
|
||||
})
|
||||
}
|
||||
|
||||
export function clearBumperTemplateAudio(id: string, templateId: string) {
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
} from '@/features/admin/media/episode-parse'
|
||||
import { formatDuration } from '@/features/admin/media/MediaPanel'
|
||||
import { ShowMetadataCard } from './ShowMetadataCard'
|
||||
import { addEpisode, episodeStillUrl, getShow, removeEpisode } from './api'
|
||||
import { imageUrl } from '@/features/admin/images/api'
|
||||
import { addEpisode, getShow, removeEpisode } from './api'
|
||||
|
||||
type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode }
|
||||
|
||||
@@ -219,9 +220,9 @@ export function ShowDetail({ showId }: { showId: string }) {
|
||||
<td className="px-4 py-2 text-muted-foreground">{index + 1}</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{episode.hasStill && (
|
||||
{episode.stillImageId && (
|
||||
<img
|
||||
src={episodeStillUrl(episode.id)}
|
||||
src={imageUrl(episode.stillImageId)}
|
||||
alt=""
|
||||
className="h-9 w-16 shrink-0 rounded object-cover"
|
||||
/>
|
||||
|
||||
@@ -80,11 +80,6 @@ export function setShowPoster(showId: string, imageId: string | null) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Ссылка на локальный кадр серии. */
|
||||
export function episodeStillUrl(episodeId: string, bust?: string) {
|
||||
return `/api/metadata/episodes/${episodeId}/still${bust ? `?v=${encodeURIComponent(bust)}` : ''}`
|
||||
}
|
||||
|
||||
/** Довыгрузить метаданные серий из привязанного источника. Возвращает число обновлённых. */
|
||||
export function refreshEpisodesMetadata(showId: string) {
|
||||
return apiRequest<number>(`/admin/metadata/shows/${showId}/refresh-episodes`, { method: 'POST' })
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cn } from '@/shared/lib/cn'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { ChannelPlayer } from './ChannelPlayer'
|
||||
import { episodeStillUrl, getEpg, imageUrl, listChannels, watchChannel } from './api'
|
||||
import { getEpg, imageUrl, listChannels, watchChannel } from './api'
|
||||
|
||||
function formatTime(iso: string) {
|
||||
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
@@ -138,9 +138,9 @@ export function AirPage() {
|
||||
<div className="flex flex-col gap-3">
|
||||
{current && (
|
||||
<div className="crt-panel flex gap-3 rounded-md p-3">
|
||||
{currentEntry?.episodeHasStill && currentEntry.episodeId ? (
|
||||
{currentEntry?.episodeStillImageId ? (
|
||||
<img
|
||||
src={episodeStillUrl(currentEntry.episodeId)}
|
||||
src={imageUrl(currentEntry.episodeStillImageId)}
|
||||
alt=""
|
||||
className="hidden h-24 w-40 shrink-0 rounded object-cover sm:block"
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { apiRequest } from '@/shared/api/client'
|
||||
import type { PublicChannelDto, PublicEpgEntryDto } from '@/shared/api/types'
|
||||
|
||||
/** Ссылка на изображение общего реестра (постеры) — по id из публичных DTO. */
|
||||
/** Ссылка на изображение общего реестра (постеры/кадры) — по id из публичных DTO. */
|
||||
export function imageUrl(imageId: string) {
|
||||
return `/api/images/${imageId}`
|
||||
}
|
||||
/** Кадр серии (пока по-старому — публичный эндпоинт метаданных). */
|
||||
export function episodeStillUrl(episodeId: string) {
|
||||
return `/api/metadata/episodes/${episodeId}/still`
|
||||
}
|
||||
|
||||
export function listChannels() {
|
||||
return apiRequest<PublicChannelDto[]>('/channels')
|
||||
|
||||
Reference in New Issue
Block a user