Refactor media preview functionality and update localization strings
Moved the mediaPreviewUrl function to the media API for better organization and clarity. Updated the MediaPanel component to include a preview feature for media assets, allowing users to view processed clips. Enhanced the UI to include a dialog for media previews and added corresponding localization strings in both English and Russian for the new preview functionality.
This commit is contained in:
@@ -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<MediaAssetDto | null>(null)
|
||||
const enqueue = useUploadStore((s) => s.enqueue)
|
||||
|
||||
const sortColumn = (key: string) => {
|
||||
@@ -230,6 +233,7 @@ export function MediaPanel() {
|
||||
<MediaRow
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
onPreview={() => 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}
|
||||
/>
|
||||
|
||||
<Dialog open={preview !== null} onOpenChange={(open) => !open && setPreview(null)}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="truncate">{preview?.originalFileName}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* Смотрят нарезку, а не исходник: в эфир идёт именно она, и битый звук или чёрный кадр
|
||||
видно только здесь. Кнопка в списке — это «плей», поэтому запускаем сразу. */}
|
||||
{preview && <HlsVideo src={mediaPreviewUrl(preview.id)} autoPlay />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<tr className="border-b border-border last:border-0">
|
||||
@@ -286,9 +305,22 @@ function MediaRow({ asset, onDelete }: Readonly<{ asset: MediaAssetDto; onDelete
|
||||
{asset.status === 'Ready' ? formatDuration(asset.processingSeconds) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Button size="sm" variant="destructive" onClick={onDelete}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Смотреть можно только нарезанное: у необработанного ассета плейлиста ещё нет. */}
|
||||
{asset.status === 'Ready' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
title={t('admin.media.preview')}
|
||||
onClick={onPreview}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="destructive" onClick={onDelete}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user