refactor(video): extract video status components and fix type names

Extract inline video status rendering logic into separate components for better maintainability. Also fix inconsistent type naming (Videodownloaded -> VideoDownloaded, VideoFailed -> VideoFailedBase) and properly type VideoFailed as OpenAIVideoFailed.
This commit is contained in:
icarus 2025-10-13 14:03:54 +08:00
parent 1e1bfafb88
commit f54e583f34
2 changed files with 116 additions and 79 deletions

View File

@ -9,9 +9,9 @@ import {
Spinner,
useDisclosure
} from '@heroui/react'
import { Video } from '@renderer/types/video'
import { Video, VideoFailed } from '@renderer/types/video'
import { CheckCircleIcon, CircleXIcon } from 'lucide-react'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
export interface VideoViewerProps {
@ -21,49 +21,14 @@ export interface VideoViewerProps {
export const VideoViewer = ({ video }: VideoViewerProps) => {
const { t } = useTranslation()
const [loadSuccess, setLoadSuccess] = useState<boolean | undefined>(undefined)
const { isOpen, onOpen, onClose } = useDisclosure()
return (
<>
<div className="flex h-full w-full items-center justify-center rounded-2xl bg-foreground-200">
{video === undefined && t('video.undefined')}
{video && video.status === 'queued' && (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
<Spinner variant="dots" />
<span>{t('video.status.queued')}</span>
</div>
)}
{video && video.status === 'in_progress' && (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
<Progress
label={t('video.status.in_progress')}
aria-label={t('video.status.in_progress')}
className="max-w-md"
color="primary"
showValueLabel={true}
size="md"
value={video.progress}
/>
</div>
)}
{video && video.status === 'completed' && (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-success-200">
<CheckCircleIcon size={64} className="text-success" />
<span className="font-bold text-2xl">{t('video.status.completed')}</span>
</div>
)}
{video && video.status === 'downloading' && (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
<Progress
label={t('video.status.downloading')}
aria-label={t('video.status.downloading')}
className="max-w-md"
color="primary"
showValueLabel={true}
size="md"
value={video.progress}
/>
</div>
)}
{video && video.status === 'queued' && <QueuedVideo />}
{video && video.status === 'in_progress' && <InProgressVideo progress={video.progress} />}
{video && video.status === 'completed' && <CompletedVideo />}
{video && video.status === 'downloading' && <DownloadingVideo progress={video.progress} />}
{video && video.status === 'downloaded' && loadSuccess !== false && (
<video
controls
@ -73,41 +38,111 @@ export const VideoViewer = ({ video }: VideoViewerProps) => {
<source src="video.mp4" type="video/mp4" />
</video>
)}
{video && video.status === 'failed' && (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
<CircleXIcon size={64} className="fill-danger text-danger-200" />
<span className="font-bold text-2xl">{t('video.status.failed')}</span>
<div className="my-2 flex justify-between gap-2">
<Button onPress={onOpen}>{t('common.detail')}</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalBody>
<ModalContent>
<div className="p-4">
{video.error === null ? (
<Alert color="danger" title={t('error.unknown')} />
) : (
<Alert color="danger" title={video.error.code} description={video.error.message} />
)}
</div>
</ModalContent>
</ModalBody>
<ModalFooter></ModalFooter>
</Modal>
<Button onPress={() => window.toast.info('Not implemented')}>{t('common.retry')}</Button>
</div>
</div>
)}
{video && video.status === 'downloaded' && loadSuccess === false && (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
<CircleXIcon size={64} className="fill-danger text-danger-200" />
<span className="font-bold text-2xl">{t('video.error.load.message')}</span>
<span>{t('video.error.load.reason')}</span>
<div className="my-2 flex justify-between gap-2">
<Button onPress={() => window.toast.info('Not implemented')}>{t('common.redownload')}</Button>
</div>
</div>
)}
{video && video.status === 'failed' && <FailedVideo error={video.error} />}
{video && video.status === 'downloaded' && loadSuccess === false && <LoadFailedVideo />}
</div>
</>
)
}
const QueuedVideo = () => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
<Spinner variant="dots" />
<span>{t('video.status.queued')}</span>
</div>
)
}
const InProgressVideo = ({ progress }: { progress: number }) => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
<Progress
label={t('video.status.in_progress')}
aria-label={t('video.status.in_progress')}
className="max-w-md"
color="primary"
showValueLabel={true}
size="md"
value={progress}
/>
</div>
)
}
const CompletedVideo = () => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-success-200">
<CheckCircleIcon size={64} className="text-success" />
<span className="font-bold text-2xl">{t('video.status.completed')}</span>
</div>
)
}
const DownloadingVideo = ({ progress }: { progress?: number }) => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl">
<Progress
label={t('video.status.downloading')}
aria-label={t('video.status.downloading')}
className="max-w-md"
color="primary"
showValueLabel={true}
size="md"
value={progress}
/>
</div>
)
}
const FailedVideo = ({ error }: { error: VideoFailed['error'] }) => {
const { t } = useTranslation()
const { isOpen, onOpen, onClose } = useDisclosure()
const alert = useMemo(() => {
if (error === null) {
return <Alert color="danger" title={t('error.unknown')} />
} else {
return <Alert color="danger" title={error.code} description={error.message} />
}
}, [error, t])
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
<CircleXIcon size={64} className="fill-danger text-danger-200" />
<span className="font-bold text-2xl">{t('video.status.failed')}</span>
<div className="my-2 flex justify-between gap-2">
<Button onPress={onOpen}>{t('common.detail')}</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalBody>
<ModalContent>
<div className="p-4">{alert}</div>
</ModalContent>
</ModalBody>
<ModalFooter></ModalFooter>
</Modal>
<Button onPress={() => window.toast.info('Not implemented')}>{t('common.retry')}</Button>
</div>
</div>
)
}
const LoadFailedVideo = ({ onRedownload }: { onRedownload?: () => void }) => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl bg-danger-200">
<CircleXIcon size={64} className="fill-danger text-danger-200" />
<span className="font-bold text-2xl">{t('video.error.load.message')}</span>
<span>{t('video.error.load.reason')}</span>
<div className="my-2 flex justify-between gap-2">
<Button onPress={() => onRedownload?.() || window.toast.info('Not implemented')}>
{t('common.redownload')}
</Button>
</div>
</div>
)
}

View File

@ -49,14 +49,14 @@ export interface VideoDownloading extends VideoBase {
/** integer percent */
progress: number
}
export interface Videodownloaded extends VideoBase {
export interface VideoDownloaded extends VideoBase {
status: 'downloaded'
thumbnail: string
/** Managed by fileManager */
fileId: string
}
export interface VideoFailed extends VideoBase {
export interface VideoFailedBase extends VideoBase {
status: 'failed'
error: unknown
}
@ -65,11 +65,13 @@ export interface OpenAIVideoQueued extends VideoQueued, OpenAIVideoBase {}
export interface OpenAIVideoInProgress extends VideoInProgress, OpenAIVideoBase {}
export interface OpenAIVideoCompleted extends VideoCompleted, OpenAIVideoBase {}
export interface OpenAIVideoDownloading extends VideoDownloading, OpenAIVideoBase {}
export interface OpenAIVideoDownloaded extends Videodownloaded, OpenAIVideoBase {}
export interface OpenAIVideoFailed extends VideoFailed, OpenAIVideoBase {
export interface OpenAIVideoDownloaded extends VideoDownloaded, OpenAIVideoBase {}
export interface OpenAIVideoFailed extends VideoFailedBase, OpenAIVideoBase {
error: OpenAI.Videos.Video['error']
}
export type VideoFailed = OpenAIVideoFailed
export type OpenAIVideo =
| OpenAIVideoQueued
| OpenAIVideoInProgress