forked from 77media/video-flow
H5 适配竖屏
This commit is contained in:
parent
4b163db814
commit
7a7339e6e4
@ -540,9 +540,11 @@ Please process this video editing request.`;
|
|||||||
setCurrentSketchIndex(index);
|
setCurrentSketchIndex(index);
|
||||||
}}
|
}}
|
||||||
onRetryVideo={handleRetryVideo}
|
onRetryVideo={handleRetryVideo}
|
||||||
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[40%]')}
|
className={isDesktop ? 'auto-cols-[20%]' : (isTablet ? 'auto-cols-[25%]' : 'auto-cols-[33%]')}
|
||||||
|
cols={isDesktop ? '20%' : isTablet ? '25%' : '33%'}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
aspectRatio={aspectRatio}
|
aspectRatio={aspectRatio}
|
||||||
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -182,12 +182,15 @@ export function H5MediaViewer({
|
|||||||
const status = raw?.video_status;
|
const status = raw?.video_status;
|
||||||
const videoId = raw?.video_id as string | undefined;
|
const videoId = raw?.video_id as string | undefined;
|
||||||
return (
|
return (
|
||||||
<div key={`h5-video-${idx}`} data-alt="video-slide" className="relative w-full h-full">
|
<div key={`h5-video-${idx}`} data-alt="video-slide" className="relative w-full h-full flex justify-center">
|
||||||
{hasUrl ? (
|
{hasUrl ? (
|
||||||
<>
|
<>
|
||||||
<video
|
<video
|
||||||
ref={(el) => (videoRefs.current[idx] = el)}
|
ref={(el) => (videoRefs.current[idx] = el)}
|
||||||
className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
|
className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
|
||||||
|
style={{
|
||||||
|
maxHeight: 'calc(100vh - 20rem)',
|
||||||
|
}}
|
||||||
src={url}
|
src={url}
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
playsInline
|
playsInline
|
||||||
@ -217,38 +220,11 @@ export function H5MediaViewer({
|
|||||||
<Play className="w-8 h-8" />
|
<Play className="w-8 h-8" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 顶部操作按钮 */}
|
|
||||||
<div data-alt="video-actions" className="absolute top-2 right-2 z-10 flex items-center gap-2">
|
|
||||||
{showGotoCutButton && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onGotoCut}
|
|
||||||
data-alt="goto-cut-button"
|
|
||||||
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
|
|
||||||
aria-label={'Go to editing platform'}
|
|
||||||
>
|
|
||||||
<Scissors className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!!setVideoPreview && !!onOpenChat && hasUrl && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setVideoPreview(url, videoId || '');
|
|
||||||
onOpenChat();
|
|
||||||
}}
|
|
||||||
data-alt="open-chat-button"
|
|
||||||
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
|
|
||||||
aria-label={'Open chat for editing'}
|
|
||||||
>
|
|
||||||
<MessageCircleMore className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status">
|
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="video-status" style={{
|
||||||
|
maxHeight: 'calc(100vh - 20rem)',
|
||||||
|
}}>
|
||||||
{status === 0 && (
|
{status === 0 && (
|
||||||
<span className="text-blue-500 text-base">Generating...</span>
|
<span className="text-blue-500 text-base">Generating...</span>
|
||||||
)}
|
)}
|
||||||
@ -256,24 +232,11 @@ export function H5MediaViewer({
|
|||||||
<div className="flex flex-col items-center justify-center gap-3">
|
<div className="flex flex-col items-center justify-center gap-3">
|
||||||
<div className="text-4xl">⚠️</div>
|
<div className="text-4xl">⚠️</div>
|
||||||
<span className="text-red-500 text-base">Generate failed</span>
|
<span className="text-red-500 text-base">Generate failed</span>
|
||||||
{onRetryVideo && videoId && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onRetryVideo(videoId)}
|
|
||||||
data-alt="retry-button"
|
|
||||||
className="px-3 py-1.5 rounded-full bg-black/60 text-white text-sm inline-flex items-center gap-1 active:scale-95"
|
|
||||||
aria-label={'Retry'}
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-4 h-4" />
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status !== 0 && status !== 2 && (
|
{status !== 0 && status !== 2 && (
|
||||||
<span className="text-white/70 text-base">Pending</span>
|
<span className="text-white/70 text-base">Pending</span>
|
||||||
)}
|
)}
|
||||||
{/* 失败重试按钮改为全局固定渲染,移出 slide */}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -298,8 +261,10 @@ export function H5MediaViewer({
|
|||||||
adaptiveHeight
|
adaptiveHeight
|
||||||
>
|
>
|
||||||
{imageUrls.map((url, idx) => (
|
{imageUrls.map((url, idx) => (
|
||||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
|
<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" />
|
<img src={url} alt="scene" className="w-full h-full object-contain" style={{
|
||||||
|
maxHeight: 'calc(100vh - 20rem)',
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
@ -403,8 +368,8 @@ export function H5MediaViewer({
|
|||||||
// 其他阶段:使用 Carousel
|
// 其他阶段:使用 Carousel
|
||||||
return (
|
return (
|
||||||
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
|
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
|
||||||
{/* 左侧最终视频缩略图栏(H5) */}
|
{/* 左侧最终视频缩略图栏(H5) 视频暂停时展示 */}
|
||||||
{taskObject?.final?.url && (
|
{taskObject?.final?.url && !isPlaying && (
|
||||||
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
|
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
{isFinalBarOpen && (
|
{isFinalBarOpen && (
|
||||||
@ -426,8 +391,8 @@ export function H5MediaViewer({
|
|||||||
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
|
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||||
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
|
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||||
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
|
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
|
||||||
{/* 全局固定操作区(右上角) */}
|
{/* 全局固定操作区(右上角)视频暂停时展示 */}
|
||||||
{(stage === 'video' || stage === 'final_video') && (
|
{(stage === 'video' || stage === 'final_video') && !isPlaying && (
|
||||||
<div data-alt="global-video-actions" className="absolute top-2 right-6 z-[60] flex items-center gap-2">
|
<div data-alt="global-video-actions" className="absolute top-2 right-6 z-[60] flex items-center gap-2">
|
||||||
{stage === 'video' && (
|
{stage === 'video' && (
|
||||||
<>
|
<>
|
||||||
@ -456,20 +421,25 @@ export function H5MediaViewer({
|
|||||||
await downloadAllVideos(all);
|
await downloadAllVideos(all);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<GlassIconButton
|
{(() => {
|
||||||
data-alt="download-current-button"
|
const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
|
||||||
className="w-8 h-8 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
|
return status === 1 ? (
|
||||||
icon={Download}
|
<GlassIconButton
|
||||||
size="sm"
|
data-alt="download-current-button"
|
||||||
aria-label="download-current"
|
className="w-8 h-8 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
|
||||||
onClick={async () => {
|
icon={Download}
|
||||||
const current = (taskObject.videos?.data ?? [])[activeIndex] as any;
|
size="sm"
|
||||||
const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0;
|
aria-label="download-current"
|
||||||
if (hasUrl) {
|
onClick={async () => {
|
||||||
await downloadVideo(current.urls[0]);
|
const current = (taskObject.videos?.data ?? [])[activeIndex] as any;
|
||||||
}
|
const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0;
|
||||||
}}
|
if (hasUrl) {
|
||||||
/>
|
await downloadVideo(current.urls[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
{(() => {
|
{(() => {
|
||||||
const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
|
const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
|
||||||
return status === 2 ? (
|
return status === 2 ? (
|
||||||
@ -518,7 +488,7 @@ export function H5MediaViewer({
|
|||||||
)}
|
)}
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
[data-alt='carousel-wrapper'] .slick-slide { display: block; }
|
[data-alt='carousel-wrapper'] .slick-slide { display: block; }
|
||||||
.slick-list { width: 100%;height: 100%; }
|
.slick-list { width: 100%;height: 100%;max-height: calc(100vh - 20rem); }
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -80,7 +80,7 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
|||||||
if (selectedView === 'final' && taskObject.final?.url) {
|
if (selectedView === 'final' && taskObject.final?.url) {
|
||||||
return 'Final 1/1'
|
return 'Final 1/1'
|
||||||
}
|
}
|
||||||
if (selectedView === 'video') {
|
if (selectedView === 'video' && !['scene', 'character'].includes(taskObject.currentStage)) {
|
||||||
const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
|
const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
|
||||||
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
|
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,8 @@ interface ThumbnailGridProps {
|
|||||||
className: string;
|
className: string;
|
||||||
selectedView?: 'final' | 'video' | null;
|
selectedView?: 'final' | 'video' | null;
|
||||||
aspectRatio: AspectRatioValue;
|
aspectRatio: AspectRatioValue;
|
||||||
|
cols: string;
|
||||||
|
isMobile: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,7 +33,9 @@ export function ThumbnailGrid({
|
|||||||
onRetryVideo,
|
onRetryVideo,
|
||||||
className,
|
className,
|
||||||
selectedView,
|
selectedView,
|
||||||
aspectRatio
|
aspectRatio,
|
||||||
|
cols,
|
||||||
|
isMobile
|
||||||
}: ThumbnailGridProps) {
|
}: ThumbnailGridProps) {
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
@ -191,7 +195,7 @@ export function ThumbnailGrid({
|
|||||||
key={`video-${urls}-${index}`}
|
key={`video-${urls}-${index}`}
|
||||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
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'}
|
${(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-[210px]' : 'min-w-[70px]'}
|
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||||
`}
|
`}
|
||||||
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
||||||
>
|
>
|
||||||
@ -251,12 +255,16 @@ export function ThumbnailGrid({
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
{!isMobile && (
|
||||||
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
|
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
||||||
<Video className="w-3 h-3 text-green-400 mr-1" />
|
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
|
||||||
<span className="text-xs text-green-400">{index + 1}</span>
|
<Video className="w-3 h-3 text-green-400 mr-1" />
|
||||||
|
<span className="text-xs text-green-400">{index + 1}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
|
{/* <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>
|
<span className="text-xs text-white/90">Scene {index + 1}</span>
|
||||||
@ -276,7 +284,7 @@ export function ThumbnailGrid({
|
|||||||
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
key={sketch.uniqueId || `sketch-${sketch.url}-${index}`}
|
||||||
className={`relative aspect-auto rounded-lg overflow-hidden
|
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'}
|
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}
|
||||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? 'min-w-[210px]' : 'min-w-[70px]'}
|
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||||
`}
|
`}
|
||||||
onClick={() => !isDragging && onSketchSelect(index)}
|
onClick={() => !isDragging && onSketchSelect(index)}
|
||||||
>
|
>
|
||||||
@ -307,29 +315,33 @@ export function ThumbnailGrid({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
|
||||||
{/* 角色类型 */}
|
{!isMobile && (
|
||||||
{sketch.type === 'role' && (
|
<div className='absolute bottom-0 left-0 right-0 p-2'>
|
||||||
<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" />
|
{sketch.type === 'role' && (
|
||||||
<span className="text-xs text-purple-400">Role</span>
|
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
|
||||||
</div>
|
<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" />
|
{sketch.type === 'scene' && (
|
||||||
<span className="text-xs text-purple-400">Scene</span>
|
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
|
||||||
</div>
|
<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" />
|
{(!sketch.type || sketch.type === 'shot_sketch') && (
|
||||||
<span className="text-xs text-cyan-400">{index + 1}</span>
|
<div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm">
|
||||||
</div>
|
<Clapperboard className="w-3 h-3 text-cyan-400 mr-1" />
|
||||||
)}
|
<span className="text-xs text-cyan-400">{index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* <div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
|
{/* <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">{sketch.type === 'role' ? 'Role' : (sketch.type === 'scene' ? 'Scene' : 'Shot')} {index + 1}</span>
|
<span className="text-xs text-white/90">{sketch.type === 'role' ? 'Role' : (sketch.type === 'scene' ? 'Scene' : 'Shot')} {index + 1}</span>
|
||||||
@ -344,7 +356,7 @@ export function ThumbnailGrid({
|
|||||||
<div
|
<div
|
||||||
ref={thumbnailsRef}
|
ref={thumbnailsRef}
|
||||||
tabIndex={0}
|
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 ${className} auto-cols-max`}
|
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-max'}`}
|
||||||
autoFocus
|
autoFocus
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user