forked from 77media/video-flow
视频修改功能开发
This commit is contained in:
parent
89255f9feb
commit
ca4408f5df
@ -20,8 +20,7 @@ import { showEditingNotification } from "@/components/pages/work-flow/editing-no
|
||||
// import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
|
||||
import { exportVideoWithRetry } from '@/utils/export-service';
|
||||
import { getFirstFrame } from '@/utils/tools';
|
||||
// 临时禁用视频编辑功能
|
||||
// import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
||||
import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
||||
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
|
||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||
import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast';
|
||||
@ -303,9 +302,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
// setAiEditingInProgress(false); // 已移除该状态变量
|
||||
}, []);
|
||||
|
||||
// 临时禁用视频编辑功能
|
||||
// 视频编辑描述提交处理函数
|
||||
/*const handleVideoEditDescriptionSubmit = useCallback((editPoint: EditPointType, description: string) => {
|
||||
const handleVideoEditDescriptionSubmit = useCallback((editPoint: EditPointType, description: string) => {
|
||||
console.log('🎬 视频编辑描述提交:', { editPoint, description });
|
||||
|
||||
// 构造编辑消息发送到SmartChatBox
|
||||
@ -333,7 +331,7 @@ Please process this video editing request.`;
|
||||
description: `Your edit request for timestamp ${Math.floor(editPoint.timestamp)}s has been submitted successfully.`,
|
||||
duration: 3
|
||||
});
|
||||
}, [currentSketchIndex, isSmartChatBoxOpen]);*/
|
||||
}, [currentSketchIndex, isSmartChatBoxOpen]);
|
||||
|
||||
// 测试导出接口的处理函数(使用封装的导出服务)
|
||||
const handleTestExport = useCallback(async () => {
|
||||
@ -520,7 +518,9 @@ Please process this video editing request.`;
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
onSelectView={(view) => setSelectedView(view)}
|
||||
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||||
enableVideoEdit={true}
|
||||
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||||
projectId={episodeId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -43,6 +43,12 @@ interface H5MediaViewerProps {
|
||||
onRetryVideo?: (video_id: string) => void;
|
||||
/** 切换选择视图(final 或 video) */
|
||||
onSelectView?: (view: 'final' | 'video') => void;
|
||||
/** 启用视频编辑功能 */
|
||||
enableVideoEdit?: boolean;
|
||||
/** 视频编辑描述提交回调 */
|
||||
onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void;
|
||||
/** 项目ID */
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +73,10 @@ export function H5MediaViewer({
|
||||
onGotoCut,
|
||||
isSmartChatBoxOpen,
|
||||
onRetryVideo,
|
||||
onSelectView
|
||||
onSelectView,
|
||||
enableVideoEdit,
|
||||
onVideoEditDescriptionSubmit,
|
||||
projectId
|
||||
}: H5MediaViewerProps) {
|
||||
const carouselRef = useRef<CarouselRef>(null);
|
||||
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert /*, PenTool*/ } from 'lucide-react';
|
||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, CircleAlert, PenTool } from 'lucide-react';
|
||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
@ -12,9 +12,8 @@ import ScriptLoading from './script-loading';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||
// 临时禁用视频编辑功能
|
||||
// import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
|
||||
// import { EditPoint as EditPointType } from './video-edit/types';
|
||||
import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
|
||||
import { EditPoint as EditPointType } from './video-edit/types';
|
||||
|
||||
interface MediaViewerProps {
|
||||
taskObject: TaskObject;
|
||||
@ -35,9 +34,9 @@ interface MediaViewerProps {
|
||||
onGotoCut: () => void;
|
||||
isSmartChatBoxOpen: boolean;
|
||||
onRetryVideo?: (video_id: string) => void;
|
||||
// 临时禁用视频编辑功能
|
||||
// enableVideoEdit?: boolean;
|
||||
// onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
|
||||
enableVideoEdit?: boolean;
|
||||
onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export const MediaViewer = React.memo(function MediaViewer({
|
||||
@ -58,10 +57,10 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
showGotoCutButton,
|
||||
onGotoCut,
|
||||
isSmartChatBoxOpen,
|
||||
onRetryVideo
|
||||
// 临时禁用视频编辑功能
|
||||
// enableVideoEdit = true,
|
||||
// onVideoEditDescriptionSubmit
|
||||
onRetryVideo,
|
||||
enableVideoEdit = true,
|
||||
onVideoEditDescriptionSubmit,
|
||||
projectId
|
||||
}: MediaViewerProps) {
|
||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||
@ -78,8 +77,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
const [toosBtnRight, setToodsBtnRight] = useState('1rem');
|
||||
const [isLoadingDownloadBtn, setIsLoadingDownloadBtn] = useState(false);
|
||||
const [isLoadingDownloadAllVideosBtn, setIsLoadingDownloadAllVideosBtn] = useState(false);
|
||||
// 临时禁用视频编辑功能
|
||||
// const [isVideoEditMode, setIsVideoEditMode] = useState(false);
|
||||
const [isVideoEditMode, setIsVideoEditMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmartChatBoxOpen) {
|
||||
@ -506,11 +504,10 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 临时禁用视频编辑功能 */}
|
||||
{/* 视频编辑覆盖层 */}
|
||||
{/*enableVideoEdit && isVideoEditMode && (
|
||||
{enableVideoEdit && isVideoEditMode && (
|
||||
<VideoEditOverlay
|
||||
projectId={taskObject.project_id || ''}
|
||||
projectId={projectId || ''}
|
||||
userId={JSON.parse(localStorage.getItem("currentUser") || '{}').id || 0}
|
||||
currentVideo={{
|
||||
id: taskObject.videos.data[currentSketchIndex].video_id,
|
||||
@ -522,16 +519,15 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
onDescriptionSubmit={onVideoEditDescriptionSubmit}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
)*/}
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* 跳转剪辑按钮 */}
|
||||
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||
right: toosBtnRight
|
||||
}}>
|
||||
{/* 临时禁用视频编辑功能 */}
|
||||
{/* 视频编辑模式切换按钮 */}
|
||||
{/*enableVideoEdit && (
|
||||
{/* 视频编辑模式切换按钮 - 临时注释 */}
|
||||
{/* {enableVideoEdit && (
|
||||
<Tooltip placement="top" title={isVideoEditMode ? "Exit edit mode" : "Enter edit mode"}>
|
||||
<GlassIconButton
|
||||
icon={PenTool}
|
||||
@ -540,7 +536,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
className={isVideoEditMode ? 'bg-blue-500/20 border-blue-500/50' : ''}
|
||||
/>
|
||||
</Tooltip>
|
||||
)*/}
|
||||
)} */}
|
||||
{/* 添加到chat去编辑 按钮 */}
|
||||
<Tooltip placement="top" title="Edit video with chat">
|
||||
<GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => {
|
||||
|
||||
@ -69,7 +69,7 @@ function calculateCurvePath({
|
||||
export function calculateInputPosition(
|
||||
editPointPosition: { x: number; y: number },
|
||||
containerSize: { width: number; height: number },
|
||||
inputBoxSize: { width: number; height: number } = { width: 200, height: 80 }
|
||||
inputBoxSize: { width: number; height: number } = { width: 300, height: 50 }
|
||||
): InputBoxPosition {
|
||||
const { x: pointX, y: pointY } = editPointPosition;
|
||||
const { width: containerWidth, height: containerHeight } = containerSize;
|
||||
@ -151,26 +151,79 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
|
||||
animated = true
|
||||
}) => {
|
||||
const {
|
||||
color = 'rgba(255, 255, 255, 0.8)',
|
||||
color = 'rgba(255, 255, 255, 0.9)', // White color to match the reference image
|
||||
strokeWidth = 2,
|
||||
dashArray = '5,5'
|
||||
dashArray = '8,4' // Dashed line to match the reference image
|
||||
} = style;
|
||||
|
||||
// 计算路径
|
||||
const path = useMemo(() =>
|
||||
// 计算箭头几何参数
|
||||
const arrowSize = 8;
|
||||
const arrowHalfHeight = 4;
|
||||
|
||||
// 计算连接方向和角度
|
||||
const connectionVector = useMemo(() => {
|
||||
const dx = endPoint.x - startPoint.x;
|
||||
const dy = endPoint.y - startPoint.y;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
return {
|
||||
dx: dx / length,
|
||||
dy: dy / length,
|
||||
angle: Math.atan2(dy, dx)
|
||||
};
|
||||
}, [startPoint, endPoint]);
|
||||
|
||||
// 计算箭头的正确位置和线条终点
|
||||
const arrowGeometry = useMemo(() => {
|
||||
const { dx, dy, angle } = connectionVector;
|
||||
|
||||
// 箭头尖端位置(原endPoint)
|
||||
const arrowTip = { x: endPoint.x, y: endPoint.y };
|
||||
|
||||
// 箭头底部中心点(线条应该连接到这里)
|
||||
const arrowBase = {
|
||||
x: endPoint.x - dx * arrowSize,
|
||||
y: endPoint.y - dy * arrowSize
|
||||
};
|
||||
|
||||
// 计算箭头三角形的三个顶点
|
||||
const perpX = -dy; // 垂直向量X
|
||||
const perpY = dx; // 垂直向量Y
|
||||
|
||||
const arrowPoints = [
|
||||
arrowTip, // 尖端
|
||||
{
|
||||
x: arrowBase.x + perpX * arrowHalfHeight,
|
||||
y: arrowBase.y + perpY * arrowHalfHeight
|
||||
},
|
||||
{
|
||||
x: arrowBase.x - perpX * arrowHalfHeight,
|
||||
y: arrowBase.y - perpY * arrowHalfHeight
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
tip: arrowTip,
|
||||
base: arrowBase,
|
||||
points: arrowPoints,
|
||||
angle
|
||||
};
|
||||
}, [endPoint, connectionVector, arrowSize, arrowHalfHeight]);
|
||||
|
||||
// 计算路径(线条终止于箭头底部中心)
|
||||
const path = useMemo(() =>
|
||||
calculateCurvePath({
|
||||
start: startPoint,
|
||||
end: endPoint,
|
||||
end: arrowGeometry.base, // 连接到箭头底部中心而不是尖端
|
||||
containerSize,
|
||||
curvature
|
||||
}), [startPoint, endPoint, containerSize, curvature]);
|
||||
}), [startPoint, arrowGeometry.base, containerSize, curvature]);
|
||||
|
||||
// 计算路径长度用于动画
|
||||
const pathLength = useMemo(() => {
|
||||
const dx = endPoint.x - startPoint.x;
|
||||
const dy = endPoint.y - startPoint.y;
|
||||
const dx = arrowGeometry.base.x - startPoint.x;
|
||||
const dy = arrowGeometry.base.y - startPoint.y;
|
||||
return Math.sqrt(dx * dx + dy * dy) * 1.2; // 弧线比直线长约20%
|
||||
}, [startPoint, endPoint]);
|
||||
}, [startPoint, arrowGeometry.base]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
@ -179,7 +232,7 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
|
||||
height={containerSize.height}
|
||||
style={{ zIndex: 10 }}
|
||||
>
|
||||
{/* 连接线路径 */}
|
||||
{/* Curved dashed line - properly aligned to arrow base center */}
|
||||
<motion.path
|
||||
d={path}
|
||||
fill="none"
|
||||
@ -201,15 +254,13 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
|
||||
opacity: { duration: 0.3 }
|
||||
} : {}}
|
||||
style={{
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))'
|
||||
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 连接线末端的小圆点 */}
|
||||
<motion.circle
|
||||
cx={endPoint.x}
|
||||
cy={endPoint.y}
|
||||
r={3}
|
||||
|
||||
{/* Properly aligned arrow head with geometric precision */}
|
||||
<motion.polygon
|
||||
points={arrowGeometry.points.map(p => `${p.x},${p.y}`).join(' ')}
|
||||
fill={color}
|
||||
initial={animated ? {
|
||||
scale: 0,
|
||||
@ -227,32 +278,32 @@ export const EditConnection: React.FC<EditConnectionProps> = ({
|
||||
damping: 25
|
||||
} : {}}
|
||||
style={{
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
|
||||
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 动画流动效果(可选) */}
|
||||
{animated && (
|
||||
<motion.circle
|
||||
r={2}
|
||||
fill="rgba(255, 255, 255, 0.9)"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
offsetDistance: ["0%", "100%"]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
delay: 0.6
|
||||
}}
|
||||
style={{
|
||||
offsetPath: `path('${path}')`,
|
||||
offsetRotate: "0deg"
|
||||
}}
|
||||
/>
|
||||
{/* Debug visualization (remove in production) */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<>
|
||||
{/* Arrow base center point */}
|
||||
<circle
|
||||
cx={arrowGeometry.base.x}
|
||||
cy={arrowGeometry.base.y}
|
||||
r={1}
|
||||
fill="red"
|
||||
opacity={0.5}
|
||||
/>
|
||||
{/* Arrow tip point */}
|
||||
<circle
|
||||
cx={arrowGeometry.tip.x}
|
||||
cy={arrowGeometry.tip.y}
|
||||
r={1}
|
||||
fill="blue"
|
||||
opacity={0.5}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@ -101,7 +101,7 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
|
||||
<AnimatePresence>
|
||||
{editPoint.description && editPoint.status !== EditPointStatus.PENDING && (
|
||||
<>
|
||||
{/* 连接线 */}
|
||||
{/* White dashed connection line to match reference image */}
|
||||
<motion.svg
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
@ -118,30 +118,41 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
|
||||
>
|
||||
<motion.path
|
||||
d={connectionPath}
|
||||
stroke={statusColor}
|
||||
stroke="rgba(255, 255, 255, 0.9)"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
strokeDasharray="4,4"
|
||||
strokeDasharray="8,4"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{
|
||||
pathLength: 1,
|
||||
opacity: 0.8,
|
||||
strokeDashoffset: [0, -8]
|
||||
animate={{
|
||||
pathLength: 1,
|
||||
opacity: 1
|
||||
}}
|
||||
exit={{ pathLength: 0, opacity: 0 }}
|
||||
transition={{
|
||||
pathLength: { duration: 0.8, ease: "easeOut" },
|
||||
opacity: { duration: 0.5 },
|
||||
strokeDashoffset: {
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "linear"
|
||||
}
|
||||
opacity: { duration: 0.5 }
|
||||
}}
|
||||
style={{
|
||||
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Arrow head */}
|
||||
<motion.polygon
|
||||
points={`${connectionEnd.x},${connectionEnd.y} ${connectionEnd.x-8},${connectionEnd.y-4} ${connectionEnd.x-8},${connectionEnd.y+4}`}
|
||||
fill="rgba(255, 255, 255, 0.9)"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0, opacity: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.3 }}
|
||||
style={{
|
||||
filter: 'drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))'
|
||||
}}
|
||||
/>
|
||||
</motion.svg>
|
||||
|
||||
{/* 描述内容框 */}
|
||||
{/* Consistent white text display matching EditInput component */}
|
||||
<motion.div
|
||||
className="absolute cursor-pointer group"
|
||||
data-edit-description="true"
|
||||
@ -149,23 +160,21 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
zIndex: 25,
|
||||
maxWidth: '300px',
|
||||
minWidth: '200px'
|
||||
}}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
y: -10
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
y: -10
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: 0
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: 0
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
y: -10
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
y: -10
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
@ -174,98 +183,67 @@ export const EditDescription: React.FC<EditDescriptionProps> = ({
|
||||
duration: 0.4
|
||||
}}
|
||||
onClick={() => onClick?.(editPoint)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{/* 玻璃态背景 */}
|
||||
<div className="relative bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-lg shadow-lg border border-white/20 dark:border-gray-700/30 overflow-hidden">
|
||||
{/* 状态指示条 */}
|
||||
<div
|
||||
className="h-1 w-full"
|
||||
style={{ backgroundColor: statusColor }}
|
||||
/>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="p-3">
|
||||
{/* 状态标签 */}
|
||||
{statusText && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: statusColor }}
|
||||
/>
|
||||
<span
|
||||
className="text-xs font-medium"
|
||||
style={{ color: statusColor }}
|
||||
>
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 描述文本 */}
|
||||
<div className="text-sm text-gray-800 dark:text-gray-200 leading-relaxed">
|
||||
{editPoint.description}
|
||||
</div>
|
||||
|
||||
{/* 时间戳 */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2 flex items-center justify-between">
|
||||
<span>
|
||||
{Math.floor(editPoint.timestamp)}s
|
||||
</span>
|
||||
<span>
|
||||
{new Date(editPoint.updatedAt).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
{/* White text display with exact same styling as EditInput */}
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="text-white font-bold text-lg tracking-wide uppercase"
|
||||
style={{
|
||||
textShadow: '2px 2px 4px rgba(0, 0, 0, 0.8)',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
letterSpacing: '0.1em'
|
||||
}}
|
||||
>
|
||||
{editPoint.description}
|
||||
</div>
|
||||
|
||||
{/* 悬停时显示的操作按钮 */}
|
||||
<motion.div
|
||||
className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0 }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Interactive edit/delete buttons on hover */}
|
||||
<div className="ml-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-2">
|
||||
{onEdit && (
|
||||
<button
|
||||
className="w-6 h-6 rounded-full bg-blue-500/80 hover:bg-blue-500 text-white text-xs flex items-center justify-center transition-colors"
|
||||
<motion.button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(editPoint.id);
|
||||
}}
|
||||
title="编辑"
|
||||
className="w-6 h-6 rounded-full bg-white/20 hover:bg-white/30 backdrop-blur-sm flex items-center justify-center text-white text-xs transition-colors"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
title="Edit description"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
</motion.button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
className="w-6 h-6 rounded-full bg-red-500/80 hover:bg-red-500 text-white text-xs flex items-center justify-center transition-colors"
|
||||
<motion.button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(editPoint.id);
|
||||
}}
|
||||
title="删除"
|
||||
className="w-6 h-6 rounded-full bg-red-500/20 hover:bg-red-500/30 backdrop-blur-sm flex items-center justify-center text-white text-xs transition-colors"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
title="Delete edit point"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</motion.button>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* 装饰性光效 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 连接点指示器 */}
|
||||
<div
|
||||
className="absolute w-2 h-2 rounded-full border-2 border-white shadow-sm"
|
||||
style={{
|
||||
backgroundColor: statusColor,
|
||||
left: connectionEnd.x - position.x - 4,
|
||||
top: connectionEnd.y - position.y - 4,
|
||||
}}
|
||||
/>
|
||||
{/* Status indicator for processing states */}
|
||||
{editPoint.status === EditPointStatus.PROCESSING && (
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse mr-2"></div>
|
||||
<span className="text-white/70 text-xs uppercase tracking-wide">Processing...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editPoint.status === EditPointStatus.FAILED && (
|
||||
<div className="mt-1 flex items-center">
|
||||
<div className="w-2 h-2 bg-red-400 rounded-full mr-2"></div>
|
||||
<span className="text-white/70 text-xs uppercase tracking-wide">Failed - Click to retry</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -37,11 +37,10 @@ export const EditInput: React.FC<EditInputProps> = ({
|
||||
isSubmitting = false,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
size = { width: 280, height: 120 },
|
||||
placeholder = "Describe your edit request..."
|
||||
size = { width: 300, height: 50 },
|
||||
placeholder = "Describe your edit..."
|
||||
}) => {
|
||||
const [description, setDescription] = useState(editPoint.description || '');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -116,7 +115,6 @@ export const EditInput: React.FC<EditInputProps> = ({
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
width: size.width,
|
||||
}}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
@ -140,105 +138,33 @@ export const EditInput: React.FC<EditInputProps> = ({
|
||||
duration: 0.3
|
||||
}}
|
||||
>
|
||||
{/* 玻璃态背景容器 */}
|
||||
<div className="bg-black/60 backdrop-blur-md rounded-lg border border-white/20 shadow-2xl overflow-hidden">
|
||||
{/* 头部 */}
|
||||
<div className="px-3 py-2 border-b border-white/10 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
|
||||
<span className="text-xs text-white/80 font-medium">
|
||||
Edit Request
|
||||
</span>
|
||||
<span className="text-xs text-white/50">
|
||||
{Math.floor(editPoint.timestamp)}s
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="w-5 h-5 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white/70 hover:text-white transition-colors"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Input interface - focused on input functionality only */}
|
||||
<div className="flex items-center bg-white/90 backdrop-blur-sm rounded-lg px-3 py-2 shadow-lg">
|
||||
<input
|
||||
ref={textareaRef as any}
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 bg-transparent text-gray-800 placeholder-gray-400 text-sm border-none outline-none min-w-[200px]"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="p-3">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-transparent text-white placeholder-white/40 text-sm resize-none border-none outline-none min-h-[60px] max-h-[120px]"
|
||||
style={{ lineHeight: '1.4' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 底部操作区 */}
|
||||
<div className="px-3 py-2 border-t border-white/10 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-white/50">
|
||||
<span>Ctrl+Enter to submit</span>
|
||||
<span>•</span>
|
||||
<span>Esc to cancel</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 字符计数 */}
|
||||
<span className={`text-xs ${
|
||||
description.length > 500 ? 'text-red-400' : 'text-white/50'
|
||||
}`}>
|
||||
{description.length}/500
|
||||
</span>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<motion.button
|
||||
onClick={handleSubmit}
|
||||
disabled={!description.trim() || isSubmitting || description.length > 500}
|
||||
className="flex items-center gap-1 px-3 py-1.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-500 disabled:cursor-not-allowed text-white text-xs rounded-md transition-colors"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
<span>Submitting...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send size={12} />
|
||||
<span>Submit</span>
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 聚焦时的发光边框 */}
|
||||
<AnimatePresence>
|
||||
{isFocused && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-lg border-2 border-blue-400/50 pointer-events-none"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
{/* Submit button */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!description.trim() || isSubmitting}
|
||||
className="ml-2 w-8 h-8 rounded-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-white flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<span className="text-xs">→</span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 输入框指示箭头(可选) */}
|
||||
<div
|
||||
className="absolute w-3 h-3 bg-black/60 border-l border-t border-white/20 transform rotate-45"
|
||||
style={{
|
||||
left: 12,
|
||||
top: -6,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@ -71,44 +71,7 @@ export const EditPoint: React.FC<EditPointProps> = ({
|
||||
onEdit(editPoint.id);
|
||||
}, [onEdit, editPoint.id]);
|
||||
|
||||
// 根据状态获取颜色
|
||||
const getStatusColor = useCallback(() => {
|
||||
switch (editPoint.status) {
|
||||
case EditPointStatus.PENDING:
|
||||
return '#f59e0b'; // 黄色
|
||||
case EditPointStatus.EDITED:
|
||||
return '#10b981'; // 绿色
|
||||
case EditPointStatus.PROCESSING:
|
||||
return '#3b82f6'; // 蓝色
|
||||
case EditPointStatus.COMPLETED:
|
||||
return '#059669'; // 深绿色
|
||||
case EditPointStatus.FAILED:
|
||||
return '#ef4444'; // 红色
|
||||
default:
|
||||
return color;
|
||||
}
|
||||
}, [editPoint.status, color]);
|
||||
|
||||
// 根据状态获取图标
|
||||
const getStatusIcon = useCallback(() => {
|
||||
switch (editPoint.status) {
|
||||
case EditPointStatus.PENDING:
|
||||
return Edit3;
|
||||
case EditPointStatus.EDITED:
|
||||
return Check;
|
||||
case EditPointStatus.PROCESSING:
|
||||
return Loader2;
|
||||
case EditPointStatus.COMPLETED:
|
||||
return Check;
|
||||
case EditPointStatus.FAILED:
|
||||
return X;
|
||||
default:
|
||||
return Edit3;
|
||||
}
|
||||
}, [editPoint.status]);
|
||||
|
||||
const StatusIcon = getStatusIcon();
|
||||
const statusColor = getStatusColor();
|
||||
// Simplified for the image design - just use blue color
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@ -119,7 +82,7 @@ export const EditPoint: React.FC<EditPointProps> = ({
|
||||
top: absolutePosition.y - size / 2,
|
||||
}}
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
animate={{ scale: 1, opacity: 0 }} // Make invisible to match reference image
|
||||
exit={{ scale: 0, opacity: 0 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
@ -129,136 +92,17 @@ export const EditPoint: React.FC<EditPointProps> = ({
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* 脉冲动画背景 */}
|
||||
<AnimatePresence>
|
||||
{(editPoint.status === EditPointStatus.PENDING || isSelected) && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: pulseColor,
|
||||
width: size * 3,
|
||||
height: size * 3,
|
||||
left: -size,
|
||||
top: -size,
|
||||
}}
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [0.6, 0.2, 0.6],
|
||||
}}
|
||||
exit={{ scale: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 主编辑点 */}
|
||||
{/* Invisible edit point - just for click handling */}
|
||||
<motion.div
|
||||
className="relative rounded-full flex items-center justify-center shadow-lg backdrop-blur-sm"
|
||||
className="relative rounded-full"
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: statusColor,
|
||||
border: `2px solid rgba(255, 255, 255, 0.3)`,
|
||||
width: size * 2, // Larger click area
|
||||
height: size * 2,
|
||||
backgroundColor: 'transparent', // Invisible
|
||||
}}
|
||||
whileHover={{ scale: 1.2 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
animate={editPoint.status === EditPointStatus.PROCESSING ? {
|
||||
rotate: 360,
|
||||
transition: { duration: 1, repeat: Infinity, ease: "linear" }
|
||||
} : {}}
|
||||
>
|
||||
<StatusIcon
|
||||
size={size * 0.5}
|
||||
color="white"
|
||||
className={editPoint.status === EditPointStatus.PROCESSING ? "animate-spin" : ""}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* 选中状态的操作按钮 */}
|
||||
<AnimatePresence>
|
||||
{isSelected && editPoint.status !== EditPointStatus.PROCESSING && (
|
||||
<motion.div
|
||||
className="absolute flex gap-1"
|
||||
style={{
|
||||
left: size + 8,
|
||||
top: -4,
|
||||
}}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* 编辑按钮 */}
|
||||
<motion.button
|
||||
className="w-6 h-6 rounded-full bg-blue-500/80 backdrop-blur-sm flex items-center justify-center text-white hover:bg-blue-600/80 transition-colors"
|
||||
onClick={handleEdit}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
title="编辑描述"
|
||||
>
|
||||
<Edit3 size={12} />
|
||||
</motion.button>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<motion.button
|
||||
className="w-6 h-6 rounded-full bg-red-500/80 backdrop-blur-sm flex items-center justify-center text-white hover:bg-red-600/80 transition-colors"
|
||||
onClick={handleDelete}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
title="删除编辑点"
|
||||
>
|
||||
<X size={12} />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 状态提示文本 */}
|
||||
<AnimatePresence>
|
||||
{isSelected && editPoint.description && (
|
||||
<motion.div
|
||||
className="absolute whitespace-nowrap text-xs text-white bg-black/60 backdrop-blur-sm rounded px-2 py-1 pointer-events-none"
|
||||
style={{
|
||||
left: size + 8,
|
||||
top: size + 8,
|
||||
maxWidth: 200,
|
||||
}}
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{editPoint.description.length > 50
|
||||
? `${editPoint.description.substring(0, 50)}...`
|
||||
: editPoint.description
|
||||
}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 时间戳显示 */}
|
||||
<AnimatePresence>
|
||||
{isSelected && (
|
||||
<motion.div
|
||||
className="absolute text-xs text-white/70 bg-black/40 backdrop-blur-sm rounded px-1 py-0.5 pointer-events-none"
|
||||
style={{
|
||||
left: -20,
|
||||
top: size + 8,
|
||||
}}
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{Math.floor(editPoint.timestamp)}s
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -275,7 +275,8 @@ export const VideoEditOverlay: React.FC<VideoEditOverlayProps> = ({
|
||||
<AnimatePresence>
|
||||
{editPoints.map(editPoint => {
|
||||
const elementPosition = elementPositions[editPoint.id];
|
||||
// 只显示已提交且有描述的编辑点
|
||||
|
||||
// 只显示已提交且有描述的编辑点,且不在输入模式
|
||||
if (
|
||||
!editPoint.description ||
|
||||
editPoint.description.trim() === '' ||
|
||||
|
||||
@ -18,20 +18,20 @@ import { createEditPoint, updateEditPoint, deleteEditPoint, getEditPoints } from
|
||||
import { debounce } from './utils';
|
||||
|
||||
/**
|
||||
* 默认编辑配置
|
||||
* 默认编辑配置 - 更新为匹配参考图像的白色样式
|
||||
*/
|
||||
const DEFAULT_CONFIG: VideoEditConfig = {
|
||||
enabled: true,
|
||||
maxEditPoints: 10,
|
||||
connectionStyle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
color: 'rgba(255, 255, 255, 0.9)', // 白色连接线匹配参考图像
|
||||
strokeWidth: 2,
|
||||
dashArray: '5,5'
|
||||
dashArray: '8,4' // 虚线样式匹配参考图像
|
||||
},
|
||||
pointStyle: {
|
||||
size: 12,
|
||||
color: '#3b82f6',
|
||||
pulseColor: 'rgba(59, 130, 246, 0.3)'
|
||||
color: 'transparent', // 透明编辑点匹配参考图像
|
||||
pulseColor: 'transparent' // 透明脉冲动画
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user