forked from 77media/video-flow
缩略图区域
This commit is contained in:
parent
1d5a98bb7d
commit
de9b28e935
@ -165,7 +165,7 @@
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-items: self-start;
|
||||
}
|
||||
.videoContainer-qteKNi {
|
||||
flex: 1;
|
||||
@ -181,7 +181,7 @@
|
||||
object-position: center;
|
||||
background-color: #0003;
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
/* height: 100%; */
|
||||
}
|
||||
.container-kIPoeH {
|
||||
box-sizing: border-box;
|
||||
|
||||
@ -448,8 +448,13 @@ Please process this video editing request.`;
|
||||
ref={containerRef}
|
||||
>
|
||||
{isDesktop ? (
|
||||
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
||||
<div className={`relative heroVideo-FIzuK1`}
|
||||
style={{
|
||||
height: taskObject.final.url ? 'calc(100vh - 8rem)' : 'calc(100vh - 12rem)'
|
||||
}}
|
||||
>
|
||||
<MediaViewer
|
||||
key={taskObject.currentStage+'_'+currentSketchIndex}
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
@ -471,62 +476,103 @@ Please process this video editing request.`;
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
aspectRatio={aspectRatio}
|
||||
/>
|
||||
|
||||
{['scene', 'character', 'video', 'final_video'].includes(taskObject.currentStage) && (
|
||||
<div className={`h-14 absolute bottom-[0.5rem] left-[50%] translate-x-[-50%] z-[21]`}
|
||||
style={{
|
||||
maxWidth: 'calc(100% - 5.5rem)'
|
||||
}}>
|
||||
<ThumbnailGrid
|
||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||
taskObject={taskObject}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
onSketchSelect={(index) => {
|
||||
if (index === -1 && taskObject.final.url) {
|
||||
// 点击最终视频
|
||||
setSelectedView('final');
|
||||
setCurrentSketchIndex(0);
|
||||
} else {
|
||||
// 点击普通视频
|
||||
taskObject.final.url && setSelectedView('video');
|
||||
setCurrentSketchIndex(index);
|
||||
}
|
||||
}}
|
||||
onRetryVideo={handleRetryVideo}
|
||||
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[33%]')}
|
||||
cols={isDesktop ? '20%' : isTablet ? '25%' : '33%'}
|
||||
selectedView={selectedView}
|
||||
aspectRatio={aspectRatio}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<H5MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
selectedView={selectedView}
|
||||
mode={mode}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
setCurrentSketchIndex={setCurrentSketchIndex}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
onSelectView={(view) => setSelectedView(view)}
|
||||
enableVideoEdit={true}
|
||||
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||||
projectId={episodeId}
|
||||
/>
|
||||
<div className="relative w-full">
|
||||
<H5MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
selectedView={selectedView}
|
||||
mode={mode}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
setCurrentSketchIndex={setCurrentSketchIndex}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
onSelectView={(view) => setSelectedView(view)}
|
||||
enableVideoEdit={true}
|
||||
onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
|
||||
projectId={episodeId}
|
||||
aspectRatio={aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '16:9' : '9:16'}
|
||||
/>
|
||||
|
||||
{['scene', 'character', 'video', 'final_video'].includes(taskObject.currentStage) && (
|
||||
<div className={`h-14 absolute left-[50%] translate-x-[-50%] z-[21] ${isMobile ? '' : 'bottom-[180px]'}`}
|
||||
style={{
|
||||
maxWidth: 'calc(100vw - 5.5rem)',
|
||||
bottom: (aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE') ? '-2.5rem' : '0.5rem'
|
||||
}}>
|
||||
<ThumbnailGrid
|
||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||
taskObject={taskObject}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
onSketchSelect={(index) => {
|
||||
if (index === -1) {
|
||||
// 点击最终视频
|
||||
setSelectedView('final');
|
||||
setCurrentSketchIndex(0);
|
||||
} else {
|
||||
// 点击普通视频
|
||||
setSelectedView('video');
|
||||
setCurrentSketchIndex(index);
|
||||
}
|
||||
}}
|
||||
onRetryVideo={handleRetryVideo}
|
||||
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[33%]')}
|
||||
cols={isDesktop ? '20%' : isTablet ? '25%' : '33%'}
|
||||
selectedView={selectedView}
|
||||
aspectRatio={aspectRatio}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{taskObject.currentStage !== 'script' && (
|
||||
<div className={`h-[123px] ${!isDesktop ? '!w-full' : 'w-[calc((100vh-6rem-200px)/9*16)]'}`}>
|
||||
<ThumbnailGrid
|
||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow || isFocusChatInput}
|
||||
taskObject={taskObject}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
onSketchSelect={(index) => {
|
||||
if (index === -1) {
|
||||
// 点击最终视频
|
||||
setSelectedView('final');
|
||||
setCurrentSketchIndex(0);
|
||||
} else {
|
||||
// 点击普通视频
|
||||
setSelectedView('video');
|
||||
setCurrentSketchIndex(index);
|
||||
}
|
||||
}}
|
||||
onRetryVideo={handleRetryVideo}
|
||||
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[33%]')}
|
||||
cols={isDesktop ? '20%' : isTablet ? '25%' : '33%'}
|
||||
selectedView={selectedView}
|
||||
aspectRatio={aspectRatio}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -49,6 +49,8 @@ interface H5MediaViewerProps {
|
||||
onVideoEditDescriptionSubmit?: (editPoint: any, description: string) => void;
|
||||
/** 项目ID */
|
||||
projectId?: string;
|
||||
/** 视频比例 */
|
||||
aspectRatio?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,7 +78,8 @@ export function H5MediaViewer({
|
||||
onSelectView,
|
||||
enableVideoEdit,
|
||||
onVideoEditDescriptionSubmit,
|
||||
projectId
|
||||
projectId,
|
||||
aspectRatio
|
||||
}: H5MediaViewerProps) {
|
||||
const carouselRef = useRef<CarouselRef>(null);
|
||||
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
|
||||
@ -85,10 +88,34 @@ export function H5MediaViewer({
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
|
||||
|
||||
/** 解析形如 "16:9" 的比例字符串 */
|
||||
const parseAspect = (input?: string): { w: number; h: number } => {
|
||||
const parts = (typeof input === 'string' ? input.split(':') : []);
|
||||
const w = Number(parts[0]);
|
||||
const h = Number(parts[1]);
|
||||
return {
|
||||
w: Number.isFinite(w) && w > 0 ? w : 16,
|
||||
h: Number.isFinite(h) && h > 0 ? h : 9
|
||||
};
|
||||
};
|
||||
|
||||
/** 视频轮播容器高度:按 aspectRatio 计算(基于视口宽度) */
|
||||
const videoWrapperHeight = useMemo(() => {
|
||||
const { w, h } = parseAspect(aspectRatio);
|
||||
return `calc(100vw * ${h} / ${w})`;
|
||||
}, [aspectRatio]);
|
||||
|
||||
/** 图片轮播容器高度:默认 16:9 */
|
||||
const imageWrapperHeight = useMemo(() => {
|
||||
// return 'calc(100vw * 9 / 16)';
|
||||
const { w, h } = parseAspect(aspectRatio);
|
||||
return `calc(100vw * ${h} / ${w})`;
|
||||
}, [aspectRatio]);
|
||||
|
||||
// 计算当前阶段类型
|
||||
const stage = (selectedView === 'final' && taskObject.final?.url)
|
||||
? 'final_video'
|
||||
: (!['script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage);
|
||||
: (!['init', 'script', 'character', 'scene'].includes(taskObject.currentStage) ? 'video' : taskObject.currentStage);
|
||||
|
||||
// 生成各阶段对应的 slides 数据
|
||||
const videoUrls = useMemo(() => {
|
||||
@ -103,16 +130,19 @@ export function H5MediaViewer({
|
||||
return [];
|
||||
}, [stage, taskObject.final?.url, taskObject.videos?.data]);
|
||||
|
||||
const imageUrls = useMemo(() => {
|
||||
const imageItems = useMemo(() => {
|
||||
if (stage === 'scene' || stage === 'character') {
|
||||
const roles = (taskObject.roles?.data ?? []) as Array<any>;
|
||||
const scenes = (taskObject.scenes?.data ?? []) as Array<any>;
|
||||
console.log('h5-media-viewer:stage', stage);
|
||||
console.log('h5-media-viewer:roles', roles);
|
||||
console.log('h5-media-viewer:scenes', scenes);
|
||||
return [...roles, ...scenes].map(item => item?.url).filter(Boolean) as string[];
|
||||
return [...roles, ...scenes].map(item => ({
|
||||
url: item?.url as string | undefined,
|
||||
status: item?.status as number | undefined,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
return [] as Array<{ url?: string; status?: number }>;
|
||||
}, [stage, taskObject.roles?.data, taskObject.scenes?.data]);
|
||||
|
||||
// 占位,避免未使用警告
|
||||
@ -164,7 +194,7 @@ export function H5MediaViewer({
|
||||
// 渲染视频 slide
|
||||
const renderVideoSlides = () => (
|
||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg" style={{
|
||||
height: 'calc(100vh - 20rem)',
|
||||
height: videoWrapperHeight,
|
||||
}}>
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
@ -190,7 +220,7 @@ export function H5MediaViewer({
|
||||
ref={(el) => (videoRefs.current[idx] = el)}
|
||||
className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 20rem)',
|
||||
maxHeight: '100%',
|
||||
}}
|
||||
src={url}
|
||||
preload="metadata"
|
||||
@ -224,7 +254,7 @@ export function H5MediaViewer({
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status" style={{
|
||||
height: 'calc(100vh - 20rem)',
|
||||
height: videoWrapperHeight,
|
||||
}}>
|
||||
{status === 0 && (
|
||||
<span className="text-blue-500 text-base">Generating...</span>
|
||||
@ -250,11 +280,11 @@ export function H5MediaViewer({
|
||||
// 渲染图片 slide
|
||||
const renderImageSlides = () => (
|
||||
<div data-alt="carousel-wrapper" className="relative w-full aspect-auto min-h-[200px] overflow-hidden rounded-lg" style={{
|
||||
height: 'calc(100vh - 20rem)',
|
||||
height: imageWrapperHeight,
|
||||
}}>
|
||||
<Carousel
|
||||
ref={carouselRef}
|
||||
key={`h5-carousel-image-${stage}-${imageUrls.length}`}
|
||||
key={`h5-carousel-image-${stage}-${imageItems.length}`}
|
||||
arrows
|
||||
dots={false}
|
||||
infinite={false}
|
||||
@ -263,13 +293,34 @@ export function H5MediaViewer({
|
||||
slidesToShow={1}
|
||||
adaptiveHeight
|
||||
>
|
||||
{imageUrls.map((url, idx) => (
|
||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full flex justify-center">
|
||||
<img src={url} alt="scene" className="w-full h-full object-contain" style={{
|
||||
maxHeight: 'calc(100vh - 20rem)',
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
{imageItems.map((item, idx) => {
|
||||
const status = item?.status;
|
||||
const url = item?.url;
|
||||
const showImage = status === 1 && typeof url === 'string' && url.length > 0;
|
||||
return (
|
||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full flex justify-center">
|
||||
{showImage ? (
|
||||
<img src={url} alt="scene" className="w-full h-full object-contain" style={{
|
||||
maxHeight: '100%',
|
||||
}} />
|
||||
) : (
|
||||
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="image-status" style={{
|
||||
height: imageWrapperHeight,
|
||||
}}>
|
||||
{status === 0 && (
|
||||
<span className="text-blue-500 text-base">Generating...</span>
|
||||
)}
|
||||
{status === 2 && (
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<div className="text-4xl">⚠️</div>
|
||||
<span className="text-red-500 text-base">Generate failed</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
@ -285,7 +336,10 @@ export function H5MediaViewer({
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div data-alt="script-content" className="w-full h-full">
|
||||
<div data-alt="script-content" className="w-full overflow-auto"
|
||||
style={{
|
||||
height: 'calc(100vh - 10rem)'
|
||||
}}>
|
||||
{scriptData ? (
|
||||
<>
|
||||
<ScriptRenderer
|
||||
@ -370,13 +424,16 @@ export function H5MediaViewer({
|
||||
|
||||
// 其他阶段:使用 Carousel
|
||||
return (
|
||||
<div ref={rootRef} data-alt="h5-media-viewer" className={`w-[100vw] relative ${stage === 'final_video' ? '' : 'px-4'}`}>
|
||||
<div ref={rootRef} data-alt="h5-media-viewer" className={`relative`}
|
||||
style={{
|
||||
width: 'calc(100vw - 2rem)'
|
||||
}}>
|
||||
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
|
||||
{(stage === 'scene' || stage === 'character') && imageItems.length > 0 && renderImageSlides()}
|
||||
{/* 全局固定操作区(右下角)视频暂停时展示 */}
|
||||
{(stage === 'video' || stage === 'final_video') && !isPlaying && (
|
||||
<div data-alt="global-video-actions" className="absolute bottom-0 right-4 z-[60] flex flex-col items-center gap-2">
|
||||
<div data-alt="global-video-actions" className="absolute top-0 right-4 z-[60] flex flex-col items-center gap-2">
|
||||
{stage === 'video' && (
|
||||
<>
|
||||
<GlassIconButton
|
||||
@ -471,10 +528,13 @@ export function H5MediaViewer({
|
||||
)}
|
||||
<style jsx global>{`
|
||||
[data-alt='carousel-wrapper'] .slick-slide { display: flex !important;justify-content: center; }
|
||||
.slick-slider { height: 100% !important;display: flex !important; }
|
||||
.slick-slider { height: 100% !important; }
|
||||
.ant-carousel { height: 100% !important; }
|
||||
.slick-list { width: 100%;height: 100% !important;max-height: calc(100vh - 20rem); }
|
||||
.slick-list { width: 100%;height: 100% !important;max-height: 100%; }
|
||||
.slick-track { display: flex !important; align-items: center;height: 100% !important; }
|
||||
[data-alt='carousel-wrapper'] .slick-arrow { z-index: 70 !important; }
|
||||
[data-alt='carousel-wrapper'] .slick-prev { left: 8px; }
|
||||
[data-alt='carousel-wrapper'] .slick-next { right: 8px; }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -15,6 +15,8 @@ import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||
import { VideoEditOverlay } from './video-edit/VideoEditOverlay';
|
||||
import { EditPoint as EditPointType } from './video-edit/types';
|
||||
import { isVideoModificationEnabled } from '@/lib/server-config';
|
||||
import error_image from '@/public/assets/error.webp';
|
||||
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||
|
||||
interface MediaViewerProps {
|
||||
taskObject: TaskObject;
|
||||
@ -38,6 +40,7 @@ interface MediaViewerProps {
|
||||
enableVideoEdit?: boolean;
|
||||
onVideoEditDescriptionSubmit?: (editPoint: EditPointType, description: string) => void;
|
||||
projectId?: string;
|
||||
aspectRatio: AspectRatioValue;
|
||||
}
|
||||
|
||||
export const MediaViewer = React.memo(function MediaViewer({
|
||||
@ -61,7 +64,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
onRetryVideo,
|
||||
enableVideoEdit = true,
|
||||
onVideoEditDescriptionSubmit,
|
||||
projectId
|
||||
projectId,
|
||||
aspectRatio
|
||||
}: MediaViewerProps) {
|
||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||
@ -212,6 +216,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
<video
|
||||
ref={finalVideoRef}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
style={{
|
||||
aspectRatio: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? "16 / 9" : "9 / 16"
|
||||
}}
|
||||
src={taskObject.final.url}
|
||||
autoPlay={isFinalVideoPlaying}
|
||||
loop
|
||||
@ -396,47 +403,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
playing: boolean
|
||||
) => (
|
||||
<div
|
||||
className="absolute left-0 right-0 bottom-2 z-[21] px-6"
|
||||
className="absolute left-0 right-0 bottom-4 z-[21] px-2"
|
||||
data-alt={isFinal ? 'final-controls' : 'video-controls'}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 播放/暂停 */}
|
||||
<GlassIconButton icon={playing ? Pause : Play} onClick={onPlayToggle} size="sm" />
|
||||
|
||||
{/* 静音,仅图标 */}
|
||||
<GlassIconButton icon={isMuted ? VolumeX : Volume2} onClick={toggleMute} size="sm" />
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="flex-1 flex items-center">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
value={progressPercent}
|
||||
onChange={(e) => seekTo(parseFloat(e.target.value))}
|
||||
className="w-full h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
|
||||
[&::-webkit-slider-thumb]:cursor-pointer
|
||||
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
|
||||
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:border-none"
|
||||
style={{
|
||||
background: `linear-gradient(to right, white 0%, white ${progressPercent}%, rgba(255,255,255,0.2) ${progressPercent}%, rgba(255,255,255,0.2) 100%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 剩余时间 */}
|
||||
<div className="text-white/80 text-sm w-14 text-right select-none" data-alt="time-remaining">
|
||||
{formatRemaining(duration, currentTime)}
|
||||
</div>
|
||||
|
||||
{/* 画中画 */}
|
||||
<GlassIconButton icon={PictureInPicture2} onClick={requestPip} size="sm" />
|
||||
|
||||
{/* 全屏 */}
|
||||
<GlassIconButton icon={isFullscreen ? Minimize : Maximize} onClick={toggleFullscreen} size="sm" />
|
||||
<GlassIconButton className="group-hover:block hidden animate-in duration-300" icon={isFullscreen ? Minimize : Maximize} onClick={toggleFullscreen} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -456,11 +430,14 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
<motion.div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
|
||||
animate={{ filter: "blur(20px)", scale: 1.1, opacity: 0.5 }}
|
||||
animate={{ filter: "blur(20px)", scale: 1, opacity: 0.5 }}
|
||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||
>
|
||||
<video
|
||||
className="w-full h-full rounded-lg object-contain object-center"
|
||||
style={{
|
||||
aspectRatio: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? "16 / 9" : "9 / 16"
|
||||
}}
|
||||
src={taskObject.final.url}
|
||||
loop
|
||||
playsInline
|
||||
@ -484,8 +461,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
</motion.div>
|
||||
|
||||
{/* 编辑和剪辑按钮 */}
|
||||
<div className="absolute top-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||
right: toosBtnRight
|
||||
<div className="absolute top-2 right-2 z-[21] flex items-center gap-2 transition-right duration-100"
|
||||
style={{
|
||||
right: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? toosBtnRight : ''
|
||||
}}>
|
||||
<Tooltip placement="top" title='Edit'>
|
||||
<GlassIconButton
|
||||
@ -535,6 +513,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
className="relative w-full h-full rounded-lg group"
|
||||
key={`render-video-${urls}`}
|
||||
ref={videoContentRef}
|
||||
style={{
|
||||
width: taskObject.videos.data[currentSketchIndex].video_status !== 1 ? 'calc(100vw - 8rem)' : '100%'
|
||||
}}
|
||||
>
|
||||
{/* 背景模糊的图片 */}
|
||||
{taskObject.videos.data[currentSketchIndex].video_status !== 1 && (
|
||||
@ -580,6 +561,9 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
ref={mainVideoRef}
|
||||
key={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||
className="w-full h-full rounded-lg object-contain object-center relative z-10"
|
||||
style={{
|
||||
aspectRatio: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? "16 / 9" : "9 / 16"
|
||||
}}
|
||||
src={taskObject.videos.data[currentSketchIndex].urls[0]}
|
||||
poster={getFirstFrame(taskObject.videos.data[currentSketchIndex].urls[0])}
|
||||
preload="none"
|
||||
@ -612,8 +596,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
</motion.div>
|
||||
|
||||
{/* 跳转剪辑按钮 */}
|
||||
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||
right: toosBtnRight
|
||||
<div className="absolute top-2 right-2 z-[21] flex items-center gap-2 transition-right duration-100" style={{
|
||||
right: aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? toosBtnRight : ''
|
||||
}}>
|
||||
{/* 视频编辑模式切换按钮 - 通过服务器配置控制显示 */}
|
||||
{enableVideoEdit && showVideoModification && (
|
||||
@ -702,8 +686,11 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full rounded-lg group"
|
||||
className="relative w-full h-full rounded-lg group overflow-hidden"
|
||||
key={`render-sketch-${currentSketch.url}`}
|
||||
style={{
|
||||
width: currentSketch.status === 1 ? '100%' : 'calc(100vw - 8rem)'
|
||||
}}
|
||||
>
|
||||
{/* 状态 */}
|
||||
{currentSketch.status === 0 && (
|
||||
@ -715,8 +702,8 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
</div>
|
||||
)}
|
||||
{currentSketch.status === 2 && (
|
||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center">
|
||||
<div className="text-2xl mb-4">⚠️</div>
|
||||
<div className="absolute inset-0 bg-[#fcb0ba1a] flex items-center justify-center">
|
||||
<img src={error_image.src} alt="error" className="w-12 h-12" />
|
||||
</div>
|
||||
)}
|
||||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||||
@ -785,7 +772,10 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
// 渲染剧本
|
||||
const renderScriptContent = () => {
|
||||
return (
|
||||
<div className="relative w-full h-full rounded-lg overflow-hidden p-2">
|
||||
<div className="relative h-full rounded-lg overflow-hidden p-2"
|
||||
style={{
|
||||
width: 'calc(100vw - 8rem)'
|
||||
}}>
|
||||
{scriptData ? (
|
||||
<ScriptRenderer
|
||||
data={scriptData}
|
||||
|
||||
@ -220,23 +220,6 @@ export function TaskInfo({
|
||||
) : currentLoadingText}
|
||||
</div>
|
||||
|
||||
{/* 主题 彩色标签tags */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{taskObject?.tags?.map((tag: string) => (
|
||||
<div
|
||||
key={tag}
|
||||
data-alt="tag-item"
|
||||
className="flex items-center gap-2 text-sm text-[#ececec] rounded-full px-3 py-1.5 bg-white/10 backdrop-blur-sm shadow-[0_4px_12px_rgba(0,0,0,0.2)]"
|
||||
>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: tagColors[tag] }}
|
||||
/>
|
||||
{tag}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ScriptModal
|
||||
isOpen={isScriptModalOpen}
|
||||
onClose={() => {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw, CircleAlert, Film } from 'lucide-react';
|
||||
import { CircleAlert, Film } from 'lucide-react';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { getFirstFrame } from '@/utils/tools';
|
||||
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||
import error_image from '@/public/assets/error.webp';
|
||||
|
||||
interface ThumbnailGridProps {
|
||||
isDisabledFocus: boolean;
|
||||
@ -93,37 +91,43 @@ export function ThumbnailGrid({
|
||||
return [];
|
||||
}, [taskObject.currentStage, taskObject.videos.data, taskObject.roles.data, taskObject.scenes.data]);
|
||||
|
||||
// 使用 useRef 存储前一次的数据,避免触发重渲染
|
||||
const prevDataRef = useRef<any[]>([]);
|
||||
/** Store previous status snapshot for change detection */
|
||||
const prevStatusRef = useRef<Array<number | undefined>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentData = getCurrentData();
|
||||
if (currentData && currentData.length > 0) {
|
||||
const currentDataStr = JSON.stringify(currentData);
|
||||
const prevDataStr = JSON.stringify(prevDataRef.current);
|
||||
if (!currentData || currentData.length === 0) return;
|
||||
|
||||
// 只有当数据真正发生变化时才进行处理
|
||||
if (currentDataStr !== prevDataStr) {
|
||||
// 找到最新更新的数据项的索引
|
||||
const changedIndex = currentData.findIndex((item, index) => {
|
||||
// 检查是否是新增的数据
|
||||
if (index >= prevDataRef.current.length) return true;
|
||||
// 检查数据是否发生变化(包括状态变化)
|
||||
return JSON.stringify(item) !== JSON.stringify(prevDataRef.current[index]);
|
||||
});
|
||||
// Extract status fields only to detect meaningful changes
|
||||
const currentStatuses: Array<number | undefined> = currentData.map((item: any) => (
|
||||
taskObject.currentStage === 'video' ? item?.video_status : item?.status
|
||||
));
|
||||
|
||||
console.log('changedIndex_thumbnail-grid', changedIndex, 'currentData:', currentData, 'prevData:', prevDataRef.current);
|
||||
const prevStatuses = prevStatusRef.current;
|
||||
|
||||
// 如果找到变化的项,自动选择该项
|
||||
if (changedIndex !== -1) {
|
||||
onSketchSelect(changedIndex);
|
||||
}
|
||||
|
||||
// 更新前一次的数据快照
|
||||
prevDataRef.current = JSON.parse(JSON.stringify(currentData));
|
||||
// Find first changed or newly added index
|
||||
let changedIndex = -1;
|
||||
for (let i = 0; i < currentStatuses.length; i += 1) {
|
||||
if (i >= prevStatuses.length) {
|
||||
changedIndex = i; // new item
|
||||
break;
|
||||
}
|
||||
if (currentStatuses[i] !== prevStatuses[i]) {
|
||||
changedIndex = i; // status changed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [taskObject, getCurrentData, onSketchSelect]);
|
||||
|
||||
console.log('changedIndex_thumbnail-grid', changedIndex);
|
||||
|
||||
|
||||
if (changedIndex !== -1) {
|
||||
onSketchSelect(changedIndex);
|
||||
}
|
||||
|
||||
// Update snapshot
|
||||
prevStatusRef.current = currentStatuses.slice();
|
||||
}, [taskObject, getCurrentData]);
|
||||
|
||||
// 处理键盘左右键事件
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
@ -192,49 +196,45 @@ export function ThumbnailGrid({
|
||||
{taskObject?.final?.url && (
|
||||
<div
|
||||
key="video-final"
|
||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
||||
${selectedView === 'final' ? 'ring-2 ring-amber-500 z-10' : 'hover:ring-2 hover:ring-amber-500/50'}
|
||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||
data-alt="final-thumbnail"
|
||||
className={`relative w-8 h-8 shrink-0 hover:brightness-50 rounded-full duration-300 transition-all ring-offset-surface-tertiary ring-white size-8
|
||||
${selectedView === 'final' ? '!w-16 !h-10 ring-2 ring-white ring-offset-2 ring-offset-neutral-900 shadow-[0_0_0_3px_rgba(255,255,255,0.6)] z-10' : 'ring-1 ring-white/20 hover:ring-2 hover:ring-white/60'}
|
||||
`}
|
||||
onClick={() => !isDragging && !disabled && onSketchSelect(-1)}
|
||||
>
|
||||
{/* 视频层 */}
|
||||
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||
<div
|
||||
className="w-full h-full relative"
|
||||
onMouseEnter={() => handleMouseEnter(-1)}
|
||||
onMouseLeave={() => handleMouseLeave(-1)}
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={getFirstFrame(taskObject.final.url)}
|
||||
draggable="false"
|
||||
alt="final video thumbnail"
|
||||
/>
|
||||
{hoveredIndex === -1 && (
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
src={taskObject.final.url}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
loop
|
||||
poster={getFirstFrame(taskObject.final.url)}
|
||||
preload="none"
|
||||
<div className="rounded-full overflow-hidden w-full h-full">
|
||||
{/* 视频层 */}
|
||||
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-300">
|
||||
<div
|
||||
className="w-full h-full relative"
|
||||
onMouseEnter={() => handleMouseEnter(-1)}
|
||||
onMouseLeave={() => handleMouseLeave(-1)}
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-cover"
|
||||
src={getFirstFrame(taskObject.final.url)}
|
||||
draggable="false"
|
||||
alt="final video thumbnail"
|
||||
/>
|
||||
)}
|
||||
{hoveredIndex === -1 && (
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
src={taskObject.final.url}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
loop
|
||||
poster={getFirstFrame(taskObject.final.url)}
|
||||
preload="none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 左上角三角标签 */}
|
||||
<div className='absolute top-0 left-0 z-20'>
|
||||
<div className="relative w-12 h-12">
|
||||
{/* 三角形背景 */}
|
||||
<div className="absolute top-0 left-0 w-0 h-0 border-t-[3rem] border-t-amber-400/60 border-r-[3rem] border-r-transparent" />
|
||||
{/* 图标 */}
|
||||
<div className="absolute top-1 left-1 flex flex-col items-center gap-0.5">
|
||||
<Film className="w-3.5 h-3.5 text-white" />
|
||||
</div>
|
||||
{/* 最终视频徽标 */}
|
||||
<div className="absolute -top-1 -left-1 z-20">
|
||||
<div className="w-4 h-4 rounded-full bg-amber-500/60 flex items-center justify-center">
|
||||
<Film className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -247,25 +247,27 @@ export function ThumbnailGrid({
|
||||
return (
|
||||
<div
|
||||
key={`video-${urls}-${index}`}
|
||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
||||
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||
data-alt={`video-thumbnail-${index+1}`}
|
||||
className={`relative overflow-hidden w-8 h-8 shrink-0 rounded-full duration-300 transition-all ring-offset-surface-tertiary ring-white size-8
|
||||
${(currentSketchIndex === index && !disabled && selectedView !== 'final') ? '!w-16 !h-10 ring-2 ring-white ring-offset-2 ring-offset-neutral-900 shadow-[0_0_0_3px_rgba(255,255,255,0.6)] z-10' : 'ring-1 ring-white/20 hover:ring-2 hover:ring-white/60'}
|
||||
`}
|
||||
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
||||
>
|
||||
|
||||
{/* 视频层 */}
|
||||
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||
<div className="relative w-full h-full transform hover:scale-105 transition-transform duration-300">
|
||||
{taskObject.videos.data[index].video_status === 0 && (
|
||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
|
||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
||||
<Loader2 className="w-10 h-10 animate-spin" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-20">
|
||||
<div className="relative size-12">
|
||||
<span className="absolute inset-0 rounded-full border-2 border-white/40 animate-ping" />
|
||||
<span className="absolute inset-2 rounded-full border-2 border-white/20 animate-ping [animation-delay:150ms]" />
|
||||
<span className="absolute inset-0 rounded-full bg-white/5" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{taskObject.videos.data[index].video_status === 2 && (
|
||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center z-20">
|
||||
<div className="text-2xl mb-4">⚠️</div>
|
||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||
<img src={error_image.src} alt="error" className="w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -283,14 +285,14 @@ export function ThumbnailGrid({
|
||||
onMouseLeave={() => handleMouseLeave(index)}
|
||||
>
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
className="w-full h-full object-cover"
|
||||
src={getFirstFrame(taskObject.videos.data[index].urls[0])}
|
||||
draggable="false"
|
||||
alt="video thumbnail"
|
||||
/>
|
||||
{hoveredIndex === index && (
|
||||
<video
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
src={taskObject.videos.data[index].urls[0]}
|
||||
autoPlay
|
||||
muted
|
||||
@ -302,27 +304,10 @@ export function ThumbnailGrid({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||
|
||||
</div>
|
||||
<div className="w-full h-full" />
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
|
||||
<Video className="w-3 h-3 text-green-400 mr-1" />
|
||||
<span className="text-xs text-green-400">{index + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
|
||||
<span className="text-xs text-white/90">Scene {index + 1}</span>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -337,63 +322,40 @@ export function ThumbnailGrid({
|
||||
return (
|
||||
<div
|
||||
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
||||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||
data-alt={`sketch-thumbnail-${index+1}`}
|
||||
className={`relative overflow-hidden w-8 h-8 shrink-0 hover:brightness-50 rounded-full duration-300 transition-all ring-offset-surface-tertiary ring-white size-8
|
||||
${currentSketchIndex === index ? '!w-16 !h-10 ring-2 ring-white ring-offset-2 ring-offset-neutral-900 shadow-[0_0_0_3px_rgba(255,255,255,0.6)] z-10' : 'ring-1 ring-white/20 hover:ring-2 hover:ring-white/60'}
|
||||
`}
|
||||
onClick={() => !isDragging && onSketchSelect(index)}
|
||||
>
|
||||
|
||||
{/* 状态 */}
|
||||
{sketch.status === 0 && (
|
||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
|
||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
||||
<Loader2 className="w-10 h-10 animate-spin" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="relative size-12">
|
||||
<span className="absolute inset-0 rounded-full border-2 border-white/40 animate-ping" />
|
||||
<span className="absolute inset-2 rounded-full border-2 border-white/20 animate-ping [animation-delay:150ms]" />
|
||||
<span className="absolute inset-0 rounded-full bg-white/5" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sketch.status === 2 && (
|
||||
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center z-20">
|
||||
<div className="text-2xl mb-4">⚠️</div>
|
||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||
<img src={error_image.src} alt="error" className="w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||||
{(sketch.status === 1) && (
|
||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-300">
|
||||
<img
|
||||
className="w-full h-full object-contain select-none"
|
||||
className="w-full h-full object-cover select-none"
|
||||
src={sketch.url}
|
||||
draggable="false"
|
||||
alt={sketch.type ? String(sketch.type) : 'sketch'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
||||
{/* 角色类型 */}
|
||||
{sketch.type === 'role' && (
|
||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
|
||||
<SquareUserRound className="w-3 h-3 text-purple-400 mr-1" />
|
||||
<span className="text-xs text-purple-400">Role</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 场景类型 */}
|
||||
{sketch.type === 'scene' && (
|
||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
|
||||
<MapPinHouse className="w-3 h-3 text-purple-400 mr-1" />
|
||||
<span className="text-xs text-purple-400">Scene</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 分镜类型 */}
|
||||
{(!sketch.type || sketch.type === 'shot_sketch') && (
|
||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm">
|
||||
<Clapperboard className="w-3 h-3 text-cyan-400 mr-1" />
|
||||
<span className="text-xs text-cyan-400">{index + 1}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 极简圆形预览,不显示类型徽标 */}
|
||||
|
||||
|
||||
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
|
||||
@ -409,7 +371,8 @@ export function ThumbnailGrid({
|
||||
<div
|
||||
ref={thumbnailsRef}
|
||||
tabIndex={0}
|
||||
className={`w-full h-full grid grid-flow-col gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none auto-cols-[${cols}] ${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? '' : '!auto-cols-min'}`}
|
||||
data-alt="thumbnail-strip"
|
||||
className={`w-full h-full grid grid-flow-col items-center gap-2 px-3 overflow-x-auto hide-scrollbar cursor-grab active:cursor-grabbing focus:outline-none select-none rounded-full ring-1 ring-white/10 shadow-inner backdrop-blur-md bg-white/10 auto-cols-[${cols}] !auto-cols-min border border-white/20`}
|
||||
autoFocus
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
||||
BIN
public/assets/error.webp
Normal file
BIN
public/assets/error.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Loading…
x
Reference in New Issue
Block a user