Implement image management enhancements: add functionality to relocate legacy images during startup, update image handling in show metadata, and refactor related API endpoints to utilize the new image storage system. Adjust database schema to support image references and update UI components for improved image selection and display.
This commit is contained in:
@@ -9,6 +9,8 @@ import { Input } from '@/shared/ui/input'
|
||||
import { Label } from '@/shared/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { imageUrl } from '@/features/admin/images/api'
|
||||
import { ImageGallery } from '@/features/admin/images/ImageGallery'
|
||||
import {
|
||||
applyMetadata,
|
||||
clearMetadata,
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
refreshEpisodesMetadata,
|
||||
searchMetadata,
|
||||
setShowOriginalName,
|
||||
showPosterUrl,
|
||||
setShowPoster,
|
||||
updateMetadata,
|
||||
uploadPoster,
|
||||
} from './api'
|
||||
@@ -24,7 +26,7 @@ import {
|
||||
export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const posterInput = useRef<HTMLInputElement>(null)
|
||||
const [bust, setBust] = useState(0)
|
||||
const [galleryOpen, setGalleryOpen] = useState(false)
|
||||
const [provider, setProvider] = useState('')
|
||||
const [originalName, setOriginalName] = useState(show.originalName ?? '')
|
||||
const [query, setQuery] = useState(show.originalName || show.name)
|
||||
@@ -40,10 +42,16 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
|
||||
const onError = (error: unknown) =>
|
||||
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
|
||||
const changed = () => {
|
||||
setBust(Date.now())
|
||||
onChanged()
|
||||
}
|
||||
const changed = () => onChanged()
|
||||
|
||||
const setPoster = useMutation({
|
||||
mutationFn: (imageId: string) => setShowPoster(show.id, imageId),
|
||||
onSuccess: () => {
|
||||
toast.success(t('settings.saved'))
|
||||
changed()
|
||||
},
|
||||
onError,
|
||||
})
|
||||
|
||||
const effectiveProvider = provider || providers?.[0] || ''
|
||||
|
||||
@@ -121,9 +129,9 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
{/* Постер */}
|
||||
<div className="flex w-40 shrink-0 flex-col gap-2">
|
||||
<div className="flex aspect-[2/3] items-center justify-center overflow-hidden rounded-md border border-border bg-muted/30">
|
||||
{show.hasPoster ? (
|
||||
{show.posterImageId ? (
|
||||
<img
|
||||
src={showPosterUrl(show.id, String(bust))}
|
||||
src={imageUrl(show.posterImageId)}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@@ -150,6 +158,15 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
>
|
||||
{t('admin.metadata.uploadPoster')}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
||||
{t('admin.metadata.pickFromGallery')}
|
||||
</Button>
|
||||
<ImageGallery
|
||||
open={galleryOpen}
|
||||
onOpenChange={setGalleryOpen}
|
||||
category="ShowPoster"
|
||||
onSelect={(img) => setPoster.mutate(img.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Поиск + ручная правка */}
|
||||
@@ -282,7 +299,7 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
: t('admin.metadata.refreshEpisodes')}
|
||||
</Button>
|
||||
)}
|
||||
{(show.metadataProvider || show.hasPoster) && (
|
||||
{(show.metadataProvider || show.posterImageId) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
@@ -72,9 +72,12 @@ export function clearMetadata(showId: string) {
|
||||
return apiRequest<void>(`/admin/metadata/shows/${showId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
/** Ссылка на локальный постер шоу (публичный эндпоинт; cache-buster — по флагу наличия). */
|
||||
export function showPosterUrl(showId: string, bust?: string) {
|
||||
return `/api/metadata/shows/${showId}/poster${bust ? `?v=${encodeURIComponent(bust)}` : ''}`
|
||||
/** Привязать/снять постер шоу по ссылке на изображение из реестра (null — отвязать). */
|
||||
export function setShowPoster(showId: string, imageId: string | null) {
|
||||
return apiRequest<void>(`/admin/metadata/shows/${showId}/poster-image`, {
|
||||
method: 'PUT',
|
||||
body: { imageId },
|
||||
})
|
||||
}
|
||||
|
||||
/** Ссылка на локальный кадр серии. */
|
||||
|
||||
@@ -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, listChannels, showPosterUrl, watchChannel } from './api'
|
||||
import { episodeStillUrl, getEpg, imageUrl, listChannels, watchChannel } from './api'
|
||||
|
||||
function formatTime(iso: string) {
|
||||
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
@@ -89,9 +89,9 @@ export function AirPage() {
|
||||
selected === channel.slug && 'border-primary text-primary',
|
||||
)}
|
||||
>
|
||||
{channel.currentShowHasPoster && channel.currentShowId ? (
|
||||
{channel.currentShowPosterImageId ? (
|
||||
<img
|
||||
src={showPosterUrl(channel.currentShowId)}
|
||||
src={imageUrl(channel.currentShowPosterImageId)}
|
||||
alt=""
|
||||
className="h-10 w-7 shrink-0 rounded object-cover"
|
||||
/>
|
||||
@@ -144,9 +144,9 @@ export function AirPage() {
|
||||
alt=""
|
||||
className="hidden h-24 w-40 shrink-0 rounded object-cover sm:block"
|
||||
/>
|
||||
) : currentEntry?.showHasPoster && current.showId ? (
|
||||
) : currentEntry?.showPosterImageId ? (
|
||||
<img
|
||||
src={showPosterUrl(current.showId)}
|
||||
src={imageUrl(currentEntry.showPosterImageId)}
|
||||
alt=""
|
||||
className="hidden h-24 w-16 shrink-0 rounded object-cover sm:block"
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { apiRequest } from '@/shared/api/client'
|
||||
import type { PublicChannelDto, PublicEpgEntryDto } from '@/shared/api/types'
|
||||
|
||||
/** Ссылки на локальные постер шоу / кадр серии (публичные эндпоинты метаданных). */
|
||||
export function showPosterUrl(showId: string) {
|
||||
return `/api/metadata/shows/${showId}/poster`
|
||||
/** Ссылка на изображение общего реестра (постеры) — по id из публичных DTO. */
|
||||
export function imageUrl(imageId: string) {
|
||||
return `/api/images/${imageId}`
|
||||
}
|
||||
/** Кадр серии (пока по-старому — публичный эндпоинт метаданных). */
|
||||
export function episodeStillUrl(episodeId: string) {
|
||||
return `/api/metadata/episodes/${episodeId}/still`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user