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:
Leonid Pershin
2026-07-25 11:43:17 +03:00
parent 149cd153b9
commit a970eae7d2
30 changed files with 1231 additions and 172 deletions
@@ -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"
+6 -3
View File
@@ -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 },
})
}
/** Ссылка на локальный кадр серии. */