forked from 77media/video-flow
309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import React, { useMemo, useEffect, useState } from 'react'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { Heart, Camera, Film, Scissors, type LucideIcon } from 'lucide-react'
|
|
import { TaskObject } from '@/api/DTO/movieEdit'
|
|
|
|
interface H5ProgressBarProps {
|
|
taskObject: TaskObject
|
|
scriptData: any
|
|
/** Loading text for stage detection */
|
|
currentLoadingText: string
|
|
className?: string
|
|
}
|
|
|
|
/** Stage configuration map */
|
|
const stageIconMap: Record<number, { icon: LucideIcon; color: string; label: string }> = {
|
|
0: { icon: Heart, color: '#6bf5f9', label: 'Script' },
|
|
1: { icon: Camera, color: '#88bafb', label: 'Roles & Scenes' },
|
|
2: { icon: Film, color: '#a285fd', label: 'Shots' },
|
|
3: { icon: Scissors, color: '#c73dff', label: 'Final' }
|
|
}
|
|
|
|
const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
|
taskObject,
|
|
scriptData,
|
|
currentLoadingText,
|
|
className
|
|
}) => {
|
|
/** Check if task has failed */
|
|
const isFailed = taskObject.status === 'FAILED'
|
|
|
|
/** Calculate current stage based on taskObject state */
|
|
const currentStage = useMemo(() => {
|
|
/** If task failed, show at final stage */
|
|
if (isFailed) {
|
|
return 3 /** Final stage to show failure */
|
|
}
|
|
|
|
/** Check if roles & scenes are completed */
|
|
const rolesCompleted =
|
|
taskObject.roles?.total_count > 0 &&
|
|
taskObject.roles?.data?.filter(v => v.status !== 0).length === taskObject.roles?.total_count
|
|
const scenesCompleted =
|
|
taskObject.scenes?.total_count > 0 &&
|
|
taskObject.scenes?.data?.filter(v => v.status !== 0).length === taskObject.scenes?.total_count
|
|
const rolesAndScenesCompleted = rolesCompleted && scenesCompleted
|
|
|
|
/** Check if videos are completed or nearly completed */
|
|
const videosCount = taskObject.videos?.data?.filter(v => v.video_status !== 0).length || 0
|
|
const videosTotal = taskObject.videos?.total_count || 0
|
|
const videosCompleted = videosTotal > 0 && videosCount === videosTotal
|
|
const videosNearlyComplete = videosTotal > 0 && videosCount >= videosTotal * 0.9 /** 90% complete */
|
|
|
|
/** Check if final video exists */
|
|
const finalVideoExists = !!taskObject.final?.url
|
|
|
|
/** Determine stage based on conditions */
|
|
if (finalVideoExists) {
|
|
return 4 /** Completed - all done */
|
|
}
|
|
|
|
/** Enter final video stage when videos are completed or nearly complete, or stage is explicitly set */
|
|
if (videosCompleted || videosNearlyComplete || taskObject.currentStage === 'final_video') {
|
|
return 3 /** Final video stage */
|
|
}
|
|
|
|
if (rolesAndScenesCompleted || taskObject.currentStage === 'video') {
|
|
return 2 /** Shots/video stage */
|
|
}
|
|
|
|
if (scriptData || taskObject.currentStage === 'character' || taskObject.currentStage === 'scene') {
|
|
return 1 /** Roles & scenes stage */
|
|
}
|
|
|
|
if (taskObject.currentStage === 'script') {
|
|
return 0 /** Script stage */
|
|
}
|
|
|
|
return 0 /** Default to script stage */
|
|
}, [
|
|
isFailed,
|
|
taskObject.currentStage,
|
|
taskObject.roles?.data,
|
|
taskObject.roles?.total_count,
|
|
taskObject.scenes?.data,
|
|
taskObject.scenes?.total_count,
|
|
taskObject.videos?.data,
|
|
taskObject.videos?.total_count,
|
|
taskObject.final?.url,
|
|
scriptData
|
|
])
|
|
|
|
/** Generate progress segments */
|
|
const segments = useMemo(() => {
|
|
/** Calculate progress for each stage */
|
|
const calculateStageProgress = (stage: number): number => {
|
|
const isCompleted = stage < currentStage
|
|
const isCurrent = stage === currentStage
|
|
const isNext = stage === currentStage + 1
|
|
|
|
/** Completed stages are always 100% */
|
|
if (isCompleted) {
|
|
return 100
|
|
}
|
|
|
|
/** Non-current and non-next stages are 0% */
|
|
if (!isCurrent && !isNext) {
|
|
return 0
|
|
}
|
|
|
|
/** Calculate current stage progress */
|
|
switch (stage) {
|
|
case 0: /** Script stage */
|
|
/** If scriptData exists or moved to next stage, show 100% */
|
|
if (scriptData || currentStage > 0) {
|
|
return 100
|
|
}
|
|
return 40
|
|
|
|
case 1: /** Roles & Scenes stage */
|
|
const rolesCount = taskObject.roles?.data?.filter(v => v.status !== 0).length || 0
|
|
const rolesTotal = taskObject.roles?.total_count || 0
|
|
const scenesCount = taskObject.scenes?.data?.filter(v => v.status !== 0).length || 0
|
|
const scenesTotal = taskObject.scenes?.total_count || 0
|
|
const totalItems = rolesTotal + scenesTotal
|
|
|
|
if (totalItems === 0) {
|
|
return 40
|
|
}
|
|
|
|
const completedItems = rolesCount + scenesCount
|
|
return Math.min(Math.round((completedItems / totalItems) * 100), 95)
|
|
|
|
case 2: /** Shots/Video stage */
|
|
const videosCount = taskObject.videos?.data?.filter(v => v.video_status !== 0).length || 0
|
|
const videosTotal = taskObject.videos?.total_count || 0
|
|
|
|
if (videosTotal === 0) {
|
|
return 40
|
|
}
|
|
|
|
return Math.min(Math.round((videosCount / videosTotal) * 100), 95)
|
|
|
|
case 3: /** Final video stage */
|
|
/** If task failed, show 60% progress */
|
|
if (isFailed) {
|
|
return 60
|
|
}
|
|
/** If final.url exists, show 100% */
|
|
if (taskObject.final?.url) {
|
|
return 100
|
|
}
|
|
/** If this is the next stage (not current), show initial progress */
|
|
if (isNext) {
|
|
return 0
|
|
}
|
|
/** Current stage in progress */
|
|
return 60
|
|
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
return [0, 1, 2, 3].map((stage) => {
|
|
const config = stageIconMap[stage]
|
|
const isCompleted = stage < currentStage
|
|
const isCurrent = stage === currentStage
|
|
const segmentProgress = calculateStageProgress(stage)
|
|
|
|
return {
|
|
stage,
|
|
config,
|
|
isCompleted,
|
|
isCurrent,
|
|
segmentProgress
|
|
}
|
|
})
|
|
}, [
|
|
currentStage,
|
|
isFailed,
|
|
scriptData,
|
|
taskObject.roles?.data,
|
|
taskObject.roles?.total_count,
|
|
taskObject.scenes?.data,
|
|
taskObject.scenes?.total_count,
|
|
taskObject.videos?.data,
|
|
taskObject.videos?.total_count,
|
|
taskObject.final?.url
|
|
])
|
|
|
|
return (
|
|
<div
|
|
data-alt="h5-progress-bar-container"
|
|
className={`w-full py-2 ${className || ''}`}
|
|
>
|
|
<div data-alt="progress-segments" className="flex items-center gap-1 relative">
|
|
{segments.map(({ stage, config, isCompleted, isCurrent, segmentProgress }) => {
|
|
const Icon = config.icon
|
|
/** Check if this is the last stage and task has failed */
|
|
const isFailedStage = isFailed && stage === 3
|
|
|
|
return (
|
|
<div
|
|
key={stage}
|
|
data-alt={`progress-segment-${stage}`}
|
|
className="flex-1 relative h-[0.35rem] bg-slate-700/50 rounded-full overflow-visible"
|
|
>
|
|
{/* Progress fill */}
|
|
<motion.div
|
|
data-alt="progress-fill"
|
|
className="absolute inset-0 rounded-full z-0 backdrop-blur-md"
|
|
style={{
|
|
background: isFailedStage ? '#ef444480' : `${config.color}80`
|
|
}}
|
|
initial={{ width: '0%' }}
|
|
animate={{ width: `${segmentProgress}%` }}
|
|
transition={{
|
|
duration: 0.6,
|
|
ease: 'easeInOut'
|
|
}}
|
|
/>
|
|
|
|
{/* Animated icon for current stage only */}
|
|
<AnimatePresence>
|
|
{isCurrent && !currentLoadingText.includes('Task completed') && segmentProgress < 100 && (
|
|
<motion.div
|
|
data-alt="stage-icon-moving"
|
|
className="absolute -top-1/2 -translate-y-1/2 z-20"
|
|
initial={{ left: '0%', x: '-50%', opacity: 0, scale: 0.5 }}
|
|
animate={{
|
|
left: `${segmentProgress}%`,
|
|
x: '-50%',
|
|
opacity: 1,
|
|
scale: 1,
|
|
rotate: isFailedStage ? 0 : [0, 360],
|
|
transition: {
|
|
left: { duration: 0.6, ease: 'easeInOut' },
|
|
x: { duration: 0 },
|
|
opacity: { duration: 0.3 },
|
|
scale: { duration: 0.3 },
|
|
rotate: isFailedStage ? { duration: 0 } : { duration: 2, repeat: Infinity, ease: 'linear' }
|
|
}
|
|
}}
|
|
exit={{
|
|
opacity: 0,
|
|
scale: 0.5,
|
|
transition: { duration: 0.3 }
|
|
}}
|
|
>
|
|
<div
|
|
data-alt="icon-wrapper"
|
|
className="w-3 h-3 rounded-full bg-white/90 backdrop-blur-sm flex items-center justify-center shadow-lg"
|
|
style={{
|
|
boxShadow: isFailedStage ? '0 0 8px #ef444480' : `0 0 8px ${config.color}80`,
|
|
background: isFailedStage ? '#ef4444' : `${config.color}`
|
|
}}
|
|
>
|
|
{/* <Icon className="w-2 h-2" style={{ color: config.color }} /> */}
|
|
<Icon className="w-2 h-2" style={{ color: '#fff', fontWeight: 'bold' }} />
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Glow effect for current stage */}
|
|
{isCurrent && segmentProgress < 100 && !isFailedStage && (
|
|
<motion.div
|
|
data-alt="glow-effect"
|
|
className="absolute inset-0 rounded-full z-10"
|
|
style={{
|
|
background: `linear-gradient(to right, transparent, ${config.color}40, transparent)`
|
|
}}
|
|
animate={{
|
|
x: ['-100%', '100%'],
|
|
transition: {
|
|
duration: 1.5,
|
|
repeat: Infinity,
|
|
ease: 'linear'
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Stage labels (optional) */}
|
|
{/* <div data-alt="stage-labels" className="flex items-center justify-between mt-1 px-1">
|
|
{segments.map(({ stage, config, isCurrent }) => (
|
|
<span
|
|
key={stage}
|
|
data-alt={`stage-label-${stage}`}
|
|
className={`text-[10px] ${isCurrent ? 'text-white font-medium' : 'text-slate-400'}`}
|
|
style={isCurrent ? { color: config.color } : {}}
|
|
>
|
|
{config.label}
|
|
</span>
|
|
))}
|
|
</div> */}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default H5ProgressBar
|
|
|