Add Prettier to the frontend and gate formatting in CI
Форматтера у фронтенда не было: стиль держался вручную и успел разъехаться в 50 файлах. Ставим Prettier с настройками под уже сложившийся стиль (без точек с запятой, одинарные кавычки, ширина 100 — подобрана замером: при 100 расходится меньше файлов, чем при 96 или 110) и прогоняем его по коду. `src/routeTree.gen.ts` исключён — его переписывает плагин роутера. Чтобы форматирование больше не расходилось незаметно, добавлены проверки в CI: `csharpier check` для бэкенда (его отсутствие и позволило накопиться 79 неотформатированным файлам) и `prettier --check` для фронтенда. Версии форматтеров прибиты точно, без кареток: минорка меняет вывод и красит CI на файлах, которых никто не трогал. `.editorconfig` задаёт редакторам те же отступы и LF ещё до форматтера; значения совпадают с настройками csharpier и Prettier намеренно — оба его читают. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0442056367
commit
0606ea3e6e
@@ -240,30 +240,36 @@ export function ShowDetail({ showId }: Readonly<{ showId: string }>) {
|
||||
>
|
||||
{t('admin.shows.deselectAll')}
|
||||
</Button>
|
||||
<Button size="sm" disabled={selected.length === 0 || adding != null} onClick={() => void bulkAdd()}>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={selected.length === 0 || adding != null}
|
||||
onClick={() => void bulkAdd()}
|
||||
>
|
||||
{addButtonLabel(adding, isSingle ? Math.min(1, selected.length) : selected.length, t)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="crt-panel max-h-72 overflow-y-auto rounded-md">
|
||||
{candidates.length === 0 ? (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">{t('admin.shows.noMatches')}</p>
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{t('admin.shows.noMatches')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-border text-sm">
|
||||
{candItems.map(({ asset, parsed }) => {
|
||||
const label = formatSeasonEpisode(parsed)
|
||||
return (
|
||||
<li key={asset.id}>
|
||||
<label className="flex cursor-pointer items-center gap-3 px-4 py-2 hover:bg-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!deselected.has(asset.id)}
|
||||
onChange={() => toggle(asset.id)}
|
||||
/>
|
||||
{label ? <Badge>{label}</Badge> : <Badge variant="muted">—</Badge>}
|
||||
<span className="truncate">{asset.originalFileName}</span>
|
||||
</label>
|
||||
</li>
|
||||
<li key={asset.id}>
|
||||
<label className="flex cursor-pointer items-center gap-3 px-4 py-2 hover:bg-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!deselected.has(asset.id)}
|
||||
onChange={() => toggle(asset.id)}
|
||||
/>
|
||||
{label ? <Badge>{label}</Badge> : <Badge variant="muted">—</Badge>}
|
||||
<span className="truncate">{asset.originalFileName}</span>
|
||||
</label>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
@@ -295,48 +301,48 @@ export function ShowDetail({ showId }: Readonly<{ showId: string }>) {
|
||||
: parseEpisodeName(episode.assetName ?? '')
|
||||
const label = formatSeasonEpisode(parsed)
|
||||
return (
|
||||
<tr key={episode.id} className="border-b border-border last:border-0">
|
||||
<td className="px-4 py-2 text-muted-foreground">{epOffset + index + 1}</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{episode.stillImageId && (
|
||||
<img
|
||||
src={imageUrl(episode.stillImageId)}
|
||||
alt=""
|
||||
className="h-9 w-16 shrink-0 rounded object-cover"
|
||||
/>
|
||||
)}
|
||||
{label && <Badge>{label}</Badge>}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{episode.title ?? episode.assetName ?? '—'}</div>
|
||||
{episode.title && (
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{episode.assetName}
|
||||
</div>
|
||||
<tr key={episode.id} className="border-b border-border last:border-0">
|
||||
<td className="px-4 py-2 text-muted-foreground">{epOffset + index + 1}</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{episode.stillImageId && (
|
||||
<img
|
||||
src={imageUrl(episode.stillImageId)}
|
||||
alt=""
|
||||
className="h-9 w-16 shrink-0 rounded object-cover"
|
||||
/>
|
||||
)}
|
||||
{label && <Badge>{label}</Badge>}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{episode.title ?? episode.assetName ?? '—'}</div>
|
||||
{episode.title && (
|
||||
<div className="truncate text-xs text-muted-foreground">
|
||||
{episode.assetName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">
|
||||
{formatDuration(episode.durationSeconds)}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{episode.assetStatus && (
|
||||
<Badge variant={episode.assetStatus === 'Ready' ? 'default' : 'muted'}>
|
||||
{t(`admin.media.statuses.${episode.assetStatus}`)}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => removeMutation.mutate(episode.id)}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">
|
||||
{formatDuration(episode.durationSeconds)}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{episode.assetStatus && (
|
||||
<Badge variant={episode.assetStatus === 'Ready' ? 'default' : 'muted'}>
|
||||
{t(`admin.media.statuses.${episode.assetStatus}`)}
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => removeMutation.mutate(episode.id)}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{show.episodes.length === 0 && (
|
||||
|
||||
@@ -95,7 +95,8 @@ export function ShowMetadataCard({
|
||||
mutationFn: async () => {
|
||||
const yearNum = year.trim() ? Number(year) : null
|
||||
const infoChanged =
|
||||
(description.trim() || null) !== (show.description ?? null) || yearNum !== (show.year ?? null)
|
||||
(description.trim() || null) !== (show.description ?? null) ||
|
||||
yearNum !== (show.year ?? 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)
|
||||
@@ -137,226 +138,238 @@ export function ShowMetadataCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('admin.metadata.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 sm:flex-row">
|
||||
{/* Постер */}
|
||||
<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.posterImageId ? (
|
||||
<img
|
||||
src={imageUrl(show.posterImageId)}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">{t('admin.metadata.noPoster')}</span>
|
||||
)}
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
||||
{t('admin.metadata.pickPoster')}
|
||||
</Button>
|
||||
<ImageGallery
|
||||
open={galleryOpen}
|
||||
onOpenChange={setGalleryOpen}
|
||||
category="ShowPoster"
|
||||
onSelect={(img) => setPoster.mutate(img.id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Название + поиск + ручная правка */}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.name')}</Label>
|
||||
<Input value={name} maxLength={256} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.originalName')}</Label>
|
||||
<Input
|
||||
value={originalName}
|
||||
maxLength={256}
|
||||
placeholder={t('admin.metadata.originalNamePlaceholder')}
|
||||
onChange={(e) => {
|
||||
setOriginalName(e.target.value)
|
||||
setSearched(false)
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.metadata.originalNameHint')}</p>
|
||||
</div>
|
||||
|
||||
{providers && providers.length > 0 && (searched || results.length > 0) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{searched && results.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">{t('admin.metadata.nothingFound')}</p>
|
||||
)}
|
||||
|
||||
{results.length > 0 && (
|
||||
<ul className="crt-panel max-h-72 divide-y divide-border overflow-y-auto rounded-md">
|
||||
{results.map((r) => (
|
||||
<li key={r.externalId} className="flex items-start gap-3 p-2">
|
||||
{r.posterUrl ? (
|
||||
<img src={r.posterUrl} alt="" className="h-16 w-11 shrink-0 rounded object-cover" />
|
||||
) : (
|
||||
<div className="h-16 w-11 shrink-0 rounded bg-muted/40" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
|
||||
<span>
|
||||
{r.title}
|
||||
{r.year != null && (
|
||||
<span className="text-muted-foreground"> ({r.year})</span>
|
||||
)}
|
||||
</span>
|
||||
{/* В выдаче OMDb сериалы и полнометражки идут вперемешку — без метки
|
||||
одноимённые фильм и сериал не различить. */}
|
||||
{r.kind && (
|
||||
<Badge variant="muted">{t(`admin.shows.kinds.${r.kind}`)}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{r.overview && (
|
||||
<p className="line-clamp-2 text-xs text-muted-foreground">{r.overview}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={apply.isPending}
|
||||
onClick={() => apply.mutate(r.externalId)}
|
||||
>
|
||||
{t('admin.metadata.apply')}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.overview')}</Label>
|
||||
<textarea
|
||||
className="min-h-20 w-full rounded-sm border border-border bg-transparent px-3 py-2 text-sm"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('admin.metadata.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 sm:flex-row">
|
||||
{/* Постер */}
|
||||
<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.posterImageId ? (
|
||||
<img
|
||||
src={imageUrl(show.posterImageId)}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-24 flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.year')}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={year}
|
||||
onChange={(e) => setYear(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{providers && providers.length > 0 && (
|
||||
<>
|
||||
<Select value={effectiveProvider} onValueChange={setProvider}>
|
||||
<SelectTrigger className="h-9 w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{p.toUpperCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={search.isPending || !searchTerm}
|
||||
onClick={() => search.mutate()}
|
||||
>
|
||||
{t('admin.metadata.searchBtn')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" disabled={save.isPending || !name.trim()} onClick={() => save.mutate()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
{linked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={refreshEpisodes.isPending}
|
||||
onClick={() => refreshEpisodes.mutate()}
|
||||
>
|
||||
{refreshEpisodes.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{refreshEpisodes.isPending
|
||||
? t('admin.metadata.refreshing')
|
||||
: t('admin.metadata.refreshEpisodes')}
|
||||
</Button>
|
||||
)}
|
||||
{linked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={findMissing.isPending}
|
||||
onClick={() => findMissing.mutate()}
|
||||
>
|
||||
{findMissing.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{t('admin.metadata.findMissing')}
|
||||
</Button>
|
||||
)}
|
||||
{(show.metadataProvider || show.posterImageId) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={clear.isPending}
|
||||
onClick={() => clear.mutate()}
|
||||
>
|
||||
{t('admin.metadata.clear')}
|
||||
</Button>
|
||||
)}
|
||||
{show.metadataProvider && (
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.sourceLabel')}: {show.metadataProvider}
|
||||
{t('admin.metadata.noPoster')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
||||
{t('admin.metadata.pickPoster')}
|
||||
</Button>
|
||||
<ImageGallery
|
||||
open={galleryOpen}
|
||||
onOpenChange={setGalleryOpen}
|
||||
category="ShowPoster"
|
||||
onSelect={(img) => setPoster.mutate(img.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={missing != null} onOpenChange={(open) => !open && setMissing(null)}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.metadata.missingTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{missing && missing.seasons.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t('admin.metadata.missingNoSeasons')}</p>
|
||||
)}
|
||||
{missing && missing.seasons.length > 0 && (
|
||||
<div className="flex max-h-[60vh] flex-col gap-3 overflow-y-auto">
|
||||
{missing.seasons.map((s) => (
|
||||
<div key={s.season} className="rounded-md border border-border p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{t('admin.metadata.seasonN', { n: s.season })}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.loadedOf', {
|
||||
loaded: s.loaded,
|
||||
total: s.expected ?? '?',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<SeasonGapNote gap={s} />
|
||||
{/* Название + поиск + ручная правка */}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.name')}</Label>
|
||||
<Input value={name} maxLength={256} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.originalName')}</Label>
|
||||
<Input
|
||||
value={originalName}
|
||||
maxLength={256}
|
||||
placeholder={t('admin.metadata.originalNamePlaceholder')}
|
||||
onChange={(e) => {
|
||||
setOriginalName(e.target.value)
|
||||
setSearched(false)
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.originalNameHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{providers && providers.length > 0 && (searched || results.length > 0) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{searched && results.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.nothingFound')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{results.length > 0 && (
|
||||
<ul className="crt-panel max-h-72 divide-y divide-border overflow-y-auto rounded-md">
|
||||
{results.map((r) => (
|
||||
<li key={r.externalId} className="flex items-start gap-3 p-2">
|
||||
{r.posterUrl ? (
|
||||
<img
|
||||
src={r.posterUrl}
|
||||
alt=""
|
||||
className="h-16 w-11 shrink-0 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-16 w-11 shrink-0 rounded bg-muted/40" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
|
||||
<span>
|
||||
{r.title}
|
||||
{r.year != null && (
|
||||
<span className="text-muted-foreground"> ({r.year})</span>
|
||||
)}
|
||||
</span>
|
||||
{/* В выдаче OMDb сериалы и полнометражки идут вперемешку — без метки
|
||||
одноимённые фильм и сериал не различить. */}
|
||||
{r.kind && (
|
||||
<Badge variant="muted">{t(`admin.shows.kinds.${r.kind}`)}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{r.overview && (
|
||||
<p className="line-clamp-2 text-xs text-muted-foreground">
|
||||
{r.overview}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={apply.isPending}
|
||||
onClick={() => apply.mutate(r.externalId)}
|
||||
>
|
||||
{t('admin.metadata.apply')}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.overview')}</Label>
|
||||
<textarea
|
||||
className="min-h-20 w-full rounded-sm border border-border bg-transparent px-3 py-2 text-sm"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-24 flex-col gap-1.5">
|
||||
<Label>{t('admin.metadata.year')}</Label>
|
||||
<Input type="number" value={year} onChange={(e) => setYear(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{providers && providers.length > 0 && (
|
||||
<>
|
||||
<Select value={effectiveProvider} onValueChange={setProvider}>
|
||||
<SelectTrigger className="h-9 w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
{p.toUpperCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={search.isPending || !searchTerm}
|
||||
onClick={() => search.mutate()}
|
||||
>
|
||||
{t('admin.metadata.searchBtn')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={save.isPending || !name.trim()}
|
||||
onClick={() => save.mutate()}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
{linked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={refreshEpisodes.isPending}
|
||||
onClick={() => refreshEpisodes.mutate()}
|
||||
>
|
||||
{refreshEpisodes.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{refreshEpisodes.isPending
|
||||
? t('admin.metadata.refreshing')
|
||||
: t('admin.metadata.refreshEpisodes')}
|
||||
</Button>
|
||||
)}
|
||||
{linked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={findMissing.isPending}
|
||||
onClick={() => findMissing.mutate()}
|
||||
>
|
||||
{findMissing.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
{t('admin.metadata.findMissing')}
|
||||
</Button>
|
||||
)}
|
||||
{(show.metadataProvider || show.posterImageId) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={clear.isPending}
|
||||
onClick={() => clear.mutate()}
|
||||
>
|
||||
{t('admin.metadata.clear')}
|
||||
</Button>
|
||||
)}
|
||||
{show.metadataProvider && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.sourceLabel')}: {show.metadataProvider}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={missing != null} onOpenChange={(open) => !open && setMissing(null)}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.metadata.missingTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{missing && missing.seasons.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t('admin.metadata.missingNoSeasons')}</p>
|
||||
)}
|
||||
{missing && missing.seasons.length > 0 && (
|
||||
<div className="flex max-h-[60vh] flex-col gap-3 overflow-y-auto">
|
||||
{missing.seasons.map((s) => (
|
||||
<div key={s.season} className="rounded-md border border-border p-3 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">
|
||||
{t('admin.metadata.seasonN', { n: s.season })}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('admin.metadata.loadedOf', {
|
||||
loaded: s.loaded,
|
||||
total: s.expected ?? '?',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<SeasonGapNote gap={s} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@ export function applyMetadata(showId: string, provider: string, externalId: stri
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMetadata(showId: string, body: { description: string | null; year: number | null }) {
|
||||
export function updateMetadata(
|
||||
showId: string,
|
||||
body: { description: string | null; year: number | null },
|
||||
) {
|
||||
return apiRequest<void>(`/admin/metadata/shows/${showId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user