diff --git a/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx b/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx index 8bae104..63efb24 100644 --- a/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx +++ b/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx @@ -16,7 +16,8 @@ import { HlsVideo } from '@/shared/ui/hls-video' import { Input } from '@/shared/ui/input' import { BlockBuilder } from './BlockBuilder' import { ClipGroupPanel } from './ClipGroupPanel' -import { listInterstitialBlocks, listInterstitials, mediaPreviewUrl } from './api' +import { mediaPreviewUrl } from '@/features/admin/media/api' +import { listInterstitialBlocks, listInterstitials } from './api' import { setDragItem } from './dnd' import { formatClock } from './format' diff --git a/frontend/src/features/admin/interstitials/api.ts b/frontend/src/features/admin/interstitials/api.ts index c9a6638..88185d3 100644 --- a/frontend/src/features/admin/interstitials/api.ts +++ b/frontend/src/features/admin/interstitials/api.ts @@ -16,8 +16,3 @@ export function importInterstitials(mediaAssetIds: string[]) { body: { mediaAssetIds }, }) } - -/** Плейлист обработанного ассета под admin-роутом (JWT) — грузится через hls.js. */ -export function mediaPreviewUrl(assetId: string) { - return `/api/admin/media/${assetId}/preview/index.m3u8` -} diff --git a/frontend/src/features/admin/media/MediaPanel.tsx b/frontend/src/features/admin/media/MediaPanel.tsx index 3b8c055..01ca71b 100644 --- a/frontend/src/features/admin/media/MediaPanel.tsx +++ b/frontend/src/features/admin/media/MediaPanel.tsx @@ -1,18 +1,20 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { FolderInput, Upload } from 'lucide-react' +import { FolderInput, Play, Upload } from 'lucide-react' import { qk } from '@/shared/api/query-keys' import type { MediaAssetDto, MediaAssetStatus } from '@/shared/api/types' import { useApiError } from '@/shared/lib/use-api-error' import { Badge, type BadgeProps } from '@/shared/ui/badge' import { Button } from '@/shared/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog' +import { HlsVideo } from '@/shared/ui/hls-video' import { Pager } from '@/shared/ui/pager' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select' import { toast } from '@/shared/ui/toast-store' import { useTableSort } from '@/shared/lib/table-sort' import { SortHeader } from '@/shared/ui/sortable' -import { deleteMedia, getMediaStats, listMedia } from './api' +import { deleteMedia, getMediaStats, listMedia, mediaPreviewUrl } from './api' import { formatDuration, splitEta } from './format' import { MediaImportDialog } from './MediaImportDialog' import { useUploadStore } from './upload-store' @@ -45,6 +47,7 @@ export function MediaPanel() { const [page, setPage] = useState(1) const { sort, toggle } = useTableSort('created', true) const [importOpen, setImportOpen] = useState(false) + const [preview, setPreview] = useState(null) const enqueue = useUploadStore((s) => s.enqueue) const sortColumn = (key: string) => { @@ -230,6 +233,7 @@ export function MediaPanel() { setPreview(asset)} onDelete={() => deleteMutation.mutate(asset.id)} /> ))} @@ -249,6 +253,17 @@ export function MediaPanel() { totalPages={data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1} onChange={setPage} /> + + !open && setPreview(null)}> + + + {preview?.originalFileName} + + {/* Смотрят нарезку, а не исходник: в эфир идёт именно она, и битый звук или чёрный кадр + видно только здесь. Кнопка в списке — это «плей», поэтому запускаем сразу. */} + {preview && } + + ) } @@ -268,7 +283,11 @@ function EtaValue({ seconds }: Readonly<{ seconds: number }>) { return <>~{[hours, minutes].filter(Boolean).join(' ')} } -function MediaRow({ asset, onDelete }: Readonly<{ asset: MediaAssetDto; onDelete: () => void }>) { +function MediaRow({ + asset, + onPreview, + onDelete, +}: Readonly<{ asset: MediaAssetDto; onPreview: () => void; onDelete: () => void }>) { const { t } = useTranslation() return ( @@ -286,9 +305,22 @@ function MediaRow({ asset, onDelete }: Readonly<{ asset: MediaAssetDto; onDelete {asset.status === 'Ready' ? formatDuration(asset.processingSeconds) : '—'} - +
+ {/* Смотреть можно только нарезанное: у необработанного ассета плейлиста ещё нет. */} + {asset.status === 'Ready' && ( + + )} + +
) diff --git a/frontend/src/features/admin/media/api.ts b/frontend/src/features/admin/media/api.ts index 4c015d5..25b13e5 100644 --- a/frontend/src/features/admin/media/api.ts +++ b/frontend/src/features/admin/media/api.ts @@ -161,3 +161,11 @@ export function importMovies(items: MovieImportItem[], provider: string) { body: { items, provider }, }) } + +/** + * Плейлист обработанного ассета под admin-роутом (JWT) — грузится через hls.js, а не ставится + * в