Refactor ShowMetadataCard and API: remove obsolete poster upload functionality, streamline metadata saving process into a single mutation, and enhance UI for original name input. Update related state management and error handling for improved user experience.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||||
import { useRef, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { HttpError } from '@/shared/api/client'
|
import { HttpError } from '@/shared/api/client'
|
||||||
import type { MetadataCandidate, ShowDto } from '@/shared/api/types'
|
import type { MetadataCandidate, ShowDto } from '@/shared/api/types'
|
||||||
@@ -20,12 +20,10 @@ import {
|
|||||||
setShowOriginalName,
|
setShowOriginalName,
|
||||||
setShowPoster,
|
setShowPoster,
|
||||||
updateMetadata,
|
updateMetadata,
|
||||||
uploadPoster,
|
|
||||||
} from './api'
|
} from './api'
|
||||||
|
|
||||||
export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
|
export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const posterInput = useRef<HTMLInputElement>(null)
|
|
||||||
const [galleryOpen, setGalleryOpen] = useState(false)
|
const [galleryOpen, setGalleryOpen] = useState(false)
|
||||||
const [provider, setProvider] = useState('')
|
const [provider, setProvider] = useState('')
|
||||||
const [originalName, setOriginalName] = useState(show.originalName ?? '')
|
const [originalName, setOriginalName] = useState(show.originalName ?? '')
|
||||||
@@ -63,15 +61,6 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
|||||||
},
|
},
|
||||||
onError,
|
onError,
|
||||||
})
|
})
|
||||||
const saveOriginal = useMutation({
|
|
||||||
mutationFn: () => setShowOriginalName(show.id, originalName.trim() || null),
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success(t('settings.saved'))
|
|
||||||
setQuery(originalName.trim() || show.name)
|
|
||||||
onChanged()
|
|
||||||
},
|
|
||||||
onError,
|
|
||||||
})
|
|
||||||
const apply = useMutation({
|
const apply = useMutation({
|
||||||
mutationFn: (externalId: string) => applyMetadata(show.id, effectiveProvider, externalId),
|
mutationFn: (externalId: string) => applyMetadata(show.id, effectiveProvider, externalId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -82,14 +71,21 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
|||||||
},
|
},
|
||||||
onError,
|
onError,
|
||||||
})
|
})
|
||||||
const saveManual = useMutation({
|
// Одна кнопка «Сохранить»: пишем только изменённое. Оригинальное название идёт своим эндпоинтом
|
||||||
mutationFn: () =>
|
// (не трогает привязку к источнику), описание/год — ручной правкой метаданных.
|
||||||
updateMetadata(show.id, {
|
const save = useMutation({
|
||||||
description: description.trim() || null,
|
mutationFn: async () => {
|
||||||
year: year.trim() ? Number(year) : null,
|
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 (infoChanged)
|
||||||
|
await updateMetadata(show.id, { description: description.trim() || null, year: yearNum })
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t('settings.saved'))
|
toast.success(t('settings.saved'))
|
||||||
|
setQuery(originalName.trim() || show.name)
|
||||||
changed()
|
changed()
|
||||||
},
|
},
|
||||||
onError,
|
onError,
|
||||||
@@ -103,11 +99,6 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
|||||||
},
|
},
|
||||||
onError,
|
onError,
|
||||||
})
|
})
|
||||||
const posterUpload = useMutation({
|
|
||||||
mutationFn: (file: File) => uploadPoster(show.id, file),
|
|
||||||
onSuccess: changed,
|
|
||||||
onError,
|
|
||||||
})
|
|
||||||
const refreshEpisodes = useMutation({
|
const refreshEpisodes = useMutation({
|
||||||
mutationFn: () => refreshEpisodesMetadata(show.id),
|
mutationFn: () => refreshEpisodesMetadata(show.id),
|
||||||
onSuccess: (count) => {
|
onSuccess: (count) => {
|
||||||
@@ -139,25 +130,6 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
|||||||
<span className="text-xs text-muted-foreground">{t('admin.metadata.noPoster')}</span>
|
<span className="text-xs text-muted-foreground">{t('admin.metadata.noPoster')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
ref={posterInput}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0]
|
|
||||||
if (file) posterUpload.mutate(file)
|
|
||||||
e.target.value = ''
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={posterUpload.isPending}
|
|
||||||
onClick={() => posterInput.current?.click()}
|
|
||||||
>
|
|
||||||
{t('admin.metadata.uploadPoster')}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
||||||
{t('admin.metadata.pickFromGallery')}
|
{t('admin.metadata.pickFromGallery')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -173,23 +145,12 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
|||||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label>{t('admin.metadata.originalName')}</Label>
|
<Label>{t('admin.metadata.originalName')}</Label>
|
||||||
<div className="flex items-end gap-2">
|
<Input
|
||||||
<Input
|
value={originalName}
|
||||||
className="min-w-40 flex-1"
|
maxLength={256}
|
||||||
value={originalName}
|
placeholder={t('admin.metadata.originalNamePlaceholder')}
|
||||||
maxLength={256}
|
onChange={(e) => setOriginalName(e.target.value)}
|
||||||
placeholder={t('admin.metadata.originalNamePlaceholder')}
|
/>
|
||||||
onChange={(e) => setOriginalName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={saveOriginal.isPending || originalName === (show.originalName ?? '')}
|
|
||||||
onClick={() => saveOriginal.mutate()}
|
|
||||||
>
|
|
||||||
{t('common.save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">{t('admin.metadata.originalNameHint')}</p>
|
<p className="text-xs text-muted-foreground">{t('admin.metadata.originalNameHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -284,7 +245,7 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button size="sm" disabled={saveManual.isPending} onClick={() => saveManual.mutate()}>
|
<Button size="sm" disabled={save.isPending} onClick={() => save.mutate()}>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
{linked && (
|
{linked && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client'
|
import { apiRequest } from '@/shared/api/client'
|
||||||
import type {
|
import type {
|
||||||
CreatedIdResponse,
|
CreatedIdResponse,
|
||||||
MetadataCandidate,
|
MetadataCandidate,
|
||||||
@@ -84,29 +84,3 @@ export function setShowPoster(showId: string, imageId: string | null) {
|
|||||||
export function refreshEpisodesMetadata(showId: string) {
|
export function refreshEpisodesMetadata(showId: string) {
|
||||||
return apiRequest<number>(`/admin/metadata/shows/${showId}/refresh-episodes`, { method: 'POST' })
|
return apiRequest<number>(`/admin/metadata/shows/${showId}/refresh-episodes`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Загрузка постера вручную (сырое тело, имя в query — как uploadMedia). */
|
|
||||||
export function uploadPoster(showId: string, file: File): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest()
|
|
||||||
const q = new URLSearchParams({ fileName: file.name })
|
|
||||||
xhr.open('PUT', `/api/admin/metadata/shows/${showId}/poster?${q.toString()}`)
|
|
||||||
const token = getAccessToken()
|
|
||||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300) resolve()
|
|
||||||
else {
|
|
||||||
let detail = `HTTP ${xhr.status}`
|
|
||||||
try {
|
|
||||||
const p = JSON.parse(xhr.responseText) as { detail?: string; title?: string }
|
|
||||||
detail = p.detail ?? p.title ?? detail
|
|
||||||
} catch {
|
|
||||||
/* пусто */
|
|
||||||
}
|
|
||||||
reject(new HttpError({ detail }, xhr.status))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.onerror = () => reject(new HttpError({ title: 'Network error' }, 0))
|
|
||||||
xhr.send(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user