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);
|
||||
}}
|
||||
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}
|
||||
aspectRatio={aspectRatio}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -182,12 +182,15 @@ export function H5MediaViewer({
|
||||
const status = raw?.video_status;
|
||||
const videoId = raw?.video_id as string | undefined;
|
||||
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 ? (
|
||||
<>
|
||||
<video
|
||||
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)',
|
||||
}}
|
||||
src={url}
|
||||
preload="metadata"
|
||||
playsInline
|
||||
@ -217,38 +220,11 @@ export function H5MediaViewer({
|
||||
<Play className="w-8 h-8" />
|
||||
</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 && (
|
||||
<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="text-4xl">⚠️</div>
|
||||
<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>
|
||||
)}
|
||||
{status !== 0 && status !== 2 && (
|
||||
<span className="text-white/70 text-base">Pending</span>
|
||||
)}
|
||||
{/* 失败重试按钮改为全局固定渲染,移出 slide */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -298,8 +261,10 @@ export function H5MediaViewer({
|
||||
adaptiveHeight
|
||||
>
|
||||
{imageUrls.map((url, idx) => (
|
||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
|
||||
<img src={url} alt="scene" className="w-full h-full object-contain" />
|
||||
<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>
|
||||
))}
|
||||
</Carousel>
|
||||
@ -403,8 +368,8 @@ export function H5MediaViewer({
|
||||
// 其他阶段:使用 Carousel
|
||||
return (
|
||||
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
|
||||
{/* 左侧最终视频缩略图栏(H5) */}
|
||||
{taskObject?.final?.url && (
|
||||
{/* 左侧最终视频缩略图栏(H5) 视频暂停时展示 */}
|
||||
{taskObject?.final?.url && !isPlaying && (
|
||||
<div className="absolute left-4 top-0 z-[60]" data-alt="final-sidebar-h5">
|
||||
<div className="flex items-start">
|
||||
{isFinalBarOpen && (
|
||||
@ -426,8 +391,8 @@ export function H5MediaViewer({
|
||||
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
|
||||
{(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">
|
||||
{stage === 'video' && (
|
||||
<>
|
||||
@ -456,20 +421,25 @@ export function H5MediaViewer({
|
||||
await downloadAllVideos(all);
|
||||
}}
|
||||
/>
|
||||
<GlassIconButton
|
||||
data-alt="download-current-button"
|
||||
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"
|
||||
icon={Download}
|
||||
size="sm"
|
||||
aria-label="download-current"
|
||||
onClick={async () => {
|
||||
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]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
const status = (taskObject.videos?.data ?? [])[activeIndex]?.video_status;
|
||||
return status === 1 ? (
|
||||
<GlassIconButton
|
||||
data-alt="download-current-button"
|
||||
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"
|
||||
icon={Download}
|
||||
size="sm"
|
||||
aria-label="download-current"
|
||||
onClick={async () => {
|
||||
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;
|
||||
return status === 2 ? (
|
||||
@ -518,7 +488,7 @@ export function H5MediaViewer({
|
||||
)}
|
||||
<style jsx global>{`
|
||||
[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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -80,7 +80,7 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
||||
if (selectedView === 'final' && taskObject.final?.url) {
|
||||
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
|
||||
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ interface ThumbnailGridProps {
|
||||
className: string;
|
||||
selectedView?: 'final' | 'video' | null;
|
||||
aspectRatio: AspectRatioValue;
|
||||
cols: string;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,7 +33,9 @@ export function ThumbnailGrid({
|
||||
onRetryVideo,
|
||||
className,
|
||||
selectedView,
|
||||
aspectRatio
|
||||
aspectRatio,
|
||||
cols,
|
||||
isMobile
|
||||
}: ThumbnailGridProps) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
@ -191,7 +195,7 @@ export function ThumbnailGrid({
|
||||
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-[210px]' : 'min-w-[70px]'}
|
||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||
`}
|
||||
onClick={() => !isDragging && !disabled && onSketchSelect(index)}
|
||||
>
|
||||
@ -251,12 +255,16 @@ export function ThumbnailGrid({
|
||||
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{!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>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* <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>
|
||||
@ -276,7 +284,7 @@ export function ThumbnailGrid({
|
||||
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-[210px]' : 'min-w-[70px]'}
|
||||
${aspectRatio === 'VIDEO_ASPECT_RATIO_LANDSCAPE' ? `min-w-[${cols}]` : 'min-w-[70px]'}
|
||||
`}
|
||||
onClick={() => !isDragging && onSketchSelect(index)}
|
||||
>
|
||||
@ -307,29 +315,33 @@ export function ThumbnailGrid({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{!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">
|
||||
<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
|
||||
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 ${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
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user