"use client" import type { IVerifyStepProps } from "./VerifyStep.d" import type { JSX } from "react" import { MediaPlayer, MediaProvider } from "@vidstack/react" import { defaultLayoutIcons, DefaultVideoLayout, } from "@vidstack/react/player/layouts/default" import "@vidstack/react/player/styles/default/theme.css" import "@vidstack/react/player/styles/default/layouts/video.css" import { AlertTriangle, CheckCircle, FileVideo, HardDrive, Info, Monitor, Music, RefreshCw, } from "lucide-react" import { FunctionComponent, useCallback, useEffect, useMemo, useState, } from "react" import cs from "classnames" import api from "@shared/api" import { useWizard } from "@shared/context/WizardContext" import { useTaskProgressState } from "@shared/hooks/useTaskProgressState" import { Badge, Button, CircularProgress } from "@shared/ui" import { StaticLoader } from "@shared/ui/Loader" import { buildCancelJobPayload, useCancelJob } from "../useCancelJob" import styles from "./VerifyStep.module.scss" function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} Б` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ` if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} МБ` return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} ГБ` } const ERROR_CONVERT_FAILED = "Не удалось запустить конвертацию" export const VerifyStep: FunctionComponent = ({ className, }): JSX.Element => { const { primaryFileKey, videoUrl, originalFileName, activeJobId, activeJobType, goBack, confirmVerify, setFileKey, setActiveJob, startMediaConvert, } = useWizard() const [convertError, setConvertError] = useState(null) const { mutate: cancelJob, isPending: isCancelling } = useCancelJob() /* Derive conversion state from wizard-persisted activeJob */ const convertJobId = activeJobType === "MEDIA_CONVERT" ? activeJobId : null const convertStatus: "idle" | "converting" | "failed" = convertJobId ? "converting" : convertError ? "failed" : "idle" const { data: probeData, isPending: isProbing } = api.useQuery( "get", "/api/media/get_meta/", { params: { query: { file_path: primaryFileKey ?? "" } } }, { enabled: !!primaryFileKey }, ) const mediaInfo = useMemo(() => { if (!probeData) return null const videoStream = probeData.streams?.find((s) => s.codec_type === "video") const audioStream = probeData.streams?.find((s) => s.codec_type === "audio") const format = probeData.format const rawName = originalFileName ?? primaryFileKey?.split("/").pop() ?? null const actualFileName = primaryFileKey?.split("/").pop() ?? rawName const ext = actualFileName?.split(".").pop()?.toUpperCase() ?? null return { filename: rawName, size: format?.size ? Number(format.size) : null, formatName: ext, width: videoStream?.width ?? null, height: videoStream?.height ?? null, audioCodec: audioStream?.codec_name ?? null, } }, [probeData, originalFileName, primaryFileKey]) const needsConversion = useMemo(() => { if (!mediaInfo?.formatName) return false return mediaInfo.formatName !== "MP4" }, [mediaInfo]) /* ---- Conversion logic ---- */ const handleConvert = useCallback(() => { void startMediaConvert().catch(() => { setConvertError(ERROR_CONVERT_FAILED) }) }, [startMediaConvert]) const { progressPct: convertProgressPct, message: convertMessage, status: convertTaskStatus, errorMessage: convertErrorMessage, } = useTaskProgressState({ jobId: convertJobId, enabled: !!convertJobId && convertStatus === "converting", defaultMessage: "Конвертация видео...", }) useEffect(() => { if (!convertJobId || convertStatus !== "converting") return if (convertTaskStatus === "FAILED") { setActiveJob(null) setConvertError(convertErrorMessage ?? "Ошибка конвертации") } }, [convertErrorMessage, convertJobId, convertStatus, convertTaskStatus, setActiveJob]) /* ---- Handlers ---- */ const handleReplace = () => { void setFileKey(null, null, null) } const handleNext = () => { void confirmVerify() } /* ---- Converting view ---- */ if (convertStatus === "converting") { return (
{Math.round(convertProgressPct)}% КОНВЕРТАЦИЯ

{convertMessage}

Конвертация выполняется на сервере. Вы можете покинуть страницу — прогресс сохранится.
) } /* ---- Normal / needs-conversion view ---- */ return (
{/* Video player */}
{isProbing && primaryFileKey ? (
) : needsConversion ? (

Формат {mediaInfo?.formatName ?? ""} не поддерживается

) : videoUrl ? ( ) : (

Видео не загружено

)}
{/* Info sidebar */}
{isProbing && primaryFileKey ? ( Анализ файла... ) : needsConversion ? ( Требуется конвертация ) : ( Готово к обработке )}
Файл {mediaInfo?.filename ?? "—"}
Размер и формат {mediaInfo?.size ? formatFileSize(mediaInfo.size) : "—"}{" "} · {mediaInfo?.formatName ?? "—"}
Разрешение {mediaInfo?.width && mediaInfo?.height ? `${mediaInfo.width}x${mediaInfo.height}` : "—"}
Аудиокодек {mediaInfo?.audioCodec ?? "—"}
{needsConversion ? ( <> {convertError && (

{convertError}

)} ) : null} {!needsConversion && ( )}
{/* Footer */}
) }