forked from 77media/video-flow
将下载弹窗 抽离
This commit is contained in:
parent
645e43b7c4
commit
eed86c43d7
@ -11,7 +11,7 @@ import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
|||||||
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||||
import { Drawer } from 'antd';
|
import { Drawer } from 'antd';
|
||||||
import error_image from '@/public/assets/error.webp';
|
import error_image from '@/public/assets/error.webp';
|
||||||
import { createRoot, Root } from 'react-dom/client';
|
import { showDownloadOptionsModal } from './download-options-modal';
|
||||||
|
|
||||||
interface H5MediaViewerProps {
|
interface H5MediaViewerProps {
|
||||||
/** 任务对象,包含各阶段数据 */
|
/** 任务对象,包含各阶段数据 */
|
||||||
@ -51,143 +51,6 @@ interface H5MediaViewerProps {
|
|||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadOptionsModalProps {
|
|
||||||
onDownloadCurrent: () => void;
|
|
||||||
onDownloadAll: () => void;
|
|
||||||
onClose: () => void;
|
|
||||||
currentVideoIndex: number;
|
|
||||||
totalVideos: number;
|
|
||||||
/** 当前视频是否生成失败 */
|
|
||||||
isCurrentVideoFailed: boolean;
|
|
||||||
/** 是否为最终视频阶段 */
|
|
||||||
isFinalStage?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadOptionsModal(props: DownloadOptionsModalProps) {
|
|
||||||
const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false } = props;
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const originalOverflow = document.body.style.overflow;
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = originalOverflow;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
data-alt="download-options-overlay"
|
|
||||||
className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/60"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-alt="download-options-modal"
|
|
||||||
className="relative w-11/12 max-w-sm rounded-2xl bg-white/5 border border-white/10 backdrop-blur-xl shadow-2xl p-6 text-white"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="download-options-title"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
data-alt="close-button"
|
|
||||||
className="absolute top-4 right-4 w-6 h-6 rounded-full bg-white/10 hover:bg-white/20 border border-white/10 flex items-center justify-center text-white transition-all active:scale-95"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
<div data-alt="modal-header" className="flex flex-col items-center text-center gap-2">
|
|
||||||
<div data-alt="modal-icon" className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center text-lg text-purple-400">
|
|
||||||
<Download />
|
|
||||||
</div>
|
|
||||||
<h3 id="download-options-title" data-alt="modal-title" className="text-base font-semibold">
|
|
||||||
Download Options
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-alt="modal-body" className="mt-4">
|
|
||||||
<p data-alt="modal-description" className="text-sm text-white/80 text-center">
|
|
||||||
Choose your download preference
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{!isCurrentVideoFailed && (
|
|
||||||
<div data-alt="stats-info" className="mt-3 rounded-lg bg-white/5 border border-white/10 p-3 text-center">
|
|
||||||
<div className="text-xs text-white/60">Current video</div>
|
|
||||||
<div className="text-sm font-medium">{currentVideoIndex + 1} / {totalVideos}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-alt="modal-actions" className="mt-6 space-y-3">
|
|
||||||
{!isCurrentVideoFailed && (
|
|
||||||
<button
|
|
||||||
data-alt="download-current-button"
|
|
||||||
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-600/80 to-purple-700/80 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
onDownloadCurrent();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Download size={16} />
|
|
||||||
{isFinalStage ? 'Download Final Video' : 'Download Current Video'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
data-alt="download-all-button"
|
|
||||||
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-500/60 to-purple-600/60 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2 border border-purple-400/30"
|
|
||||||
onClick={() => {
|
|
||||||
onDownloadAll();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowDownWideNarrow size={16} />
|
|
||||||
Download All Videos ({totalVideos})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a download options modal with glass morphism style.
|
|
||||||
* @param {DownloadOptionsModalProps} options - download options and callbacks.
|
|
||||||
*/
|
|
||||||
function showDownloadOptionsModal(options: Omit<DownloadOptionsModalProps, 'onClose'>): void {
|
|
||||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mount = document.createElement('div');
|
|
||||||
mount.setAttribute('data-alt', 'download-options-modal-root');
|
|
||||||
document.body.appendChild(mount);
|
|
||||||
|
|
||||||
let root: Root | null = null;
|
|
||||||
try {
|
|
||||||
root = createRoot(mount);
|
|
||||||
} catch {
|
|
||||||
if (mount.parentNode) {
|
|
||||||
mount.parentNode.removeChild(mount);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
try {
|
|
||||||
root?.unmount();
|
|
||||||
} finally {
|
|
||||||
if (mount.parentNode) {
|
|
||||||
mount.parentNode.removeChild(mount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<DownloadOptionsModal {...options} onClose={close} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 面向 H5 的媒体预览组件。
|
* 面向 H5 的媒体预览组件。
|
||||||
* - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。
|
* - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。
|
||||||
|
|||||||
147
components/pages/work-flow/download-options-modal.tsx
Normal file
147
components/pages/work-flow/download-options-modal.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { createRoot, Root } from 'react-dom/client';
|
||||||
|
import { X, Download, ArrowDownWideNarrow } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DownloadOptionsModalProps {
|
||||||
|
onDownloadCurrent: () => void;
|
||||||
|
onDownloadAll: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
currentVideoIndex: number;
|
||||||
|
totalVideos: number;
|
||||||
|
/** 当前视频是否生成失败 */
|
||||||
|
isCurrentVideoFailed: boolean;
|
||||||
|
/** 是否为最终视频阶段 */
|
||||||
|
isFinalStage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download options modal component with glass morphism style.
|
||||||
|
* @param {DownloadOptionsModalProps} props - modal properties.
|
||||||
|
*/
|
||||||
|
function DownloadOptionsModal(props: DownloadOptionsModalProps) {
|
||||||
|
const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false } = props;
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const originalOverflow = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = originalOverflow;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
data-alt="download-options-overlay"
|
||||||
|
className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/60"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-alt="download-options-modal"
|
||||||
|
className="relative w-11/12 max-w-sm rounded-2xl bg-white/5 border border-white/10 backdrop-blur-xl shadow-2xl p-6 text-white"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="download-options-title"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-alt="close-button"
|
||||||
|
className="absolute top-4 right-4 w-6 h-6 rounded-full bg-white/10 hover:bg-white/20 border border-white/10 flex items-center justify-center text-white transition-all active:scale-95"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
<div data-alt="modal-header" className="flex flex-col items-center text-center gap-2">
|
||||||
|
<div data-alt="modal-icon" className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center text-lg text-purple-400">
|
||||||
|
<Download />
|
||||||
|
</div>
|
||||||
|
<h3 id="download-options-title" data-alt="modal-title" className="text-base font-semibold">
|
||||||
|
Download Options
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-alt="modal-body" className="mt-4">
|
||||||
|
<p data-alt="modal-description" className="text-sm text-white/80 text-center">
|
||||||
|
Choose your download preference
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!isCurrentVideoFailed && (
|
||||||
|
<div data-alt="stats-info" className="mt-3 rounded-lg bg-white/5 border border-white/10 p-3 text-center">
|
||||||
|
<div className="text-xs text-white/60">Current video</div>
|
||||||
|
<div className="text-sm font-medium">{currentVideoIndex + 1} / {totalVideos}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-alt="modal-actions" className="mt-6 space-y-3">
|
||||||
|
{!isCurrentVideoFailed && (
|
||||||
|
<button
|
||||||
|
data-alt="download-current-button"
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-600/80 to-purple-700/80 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
onDownloadCurrent();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download size={16} />
|
||||||
|
{isFinalStage ? 'Download Final Video' : 'Download Current Video'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
data-alt="download-all-button"
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-500/60 to-purple-600/60 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2 border border-purple-400/30"
|
||||||
|
onClick={() => {
|
||||||
|
onDownloadAll();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowDownWideNarrow size={16} />
|
||||||
|
Download All Videos ({totalVideos})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a download options modal with glass morphism style.
|
||||||
|
* @param {DownloadOptionsModalProps} options - download options and callbacks.
|
||||||
|
*/
|
||||||
|
export function showDownloadOptionsModal(options: Omit<DownloadOptionsModalProps, 'onClose'>): void {
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mount = document.createElement('div');
|
||||||
|
mount.setAttribute('data-alt', 'download-options-modal-root');
|
||||||
|
document.body.appendChild(mount);
|
||||||
|
|
||||||
|
let root: Root | null = null;
|
||||||
|
try {
|
||||||
|
root = createRoot(mount);
|
||||||
|
} catch {
|
||||||
|
if (mount.parentNode) {
|
||||||
|
mount.parentNode.removeChild(mount);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
try {
|
||||||
|
root?.unmount();
|
||||||
|
} finally {
|
||||||
|
if (mount.parentNode) {
|
||||||
|
mount.parentNode.removeChild(mount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<DownloadOptionsModal {...options} onClose={close} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
|
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, PictureInPicture2, PenTool } from 'lucide-react';
|
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore, Download, ArrowDownWideNarrow, PictureInPicture2, PenTool } from 'lucide-react';
|
||||||
|
import { showDownloadOptionsModal } from './download-options-modal';
|
||||||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||||
@ -505,21 +506,33 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
onClick={() => handleEditClick('3', 'final')}
|
onClick={() => handleEditClick('3', 'final')}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* 下载所有视频按钮 */}
|
|
||||||
<Tooltip placement="top" title="Download all videos">
|
|
||||||
<GlassIconButton icon={ArrowDownWideNarrow} size='sm' loading={isLoadingDownloadAllVideosBtn} onClick={ async () => {
|
|
||||||
setIsLoadingDownloadAllVideosBtn(true);
|
|
||||||
await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
|
|
||||||
setIsLoadingDownloadAllVideosBtn(false);
|
|
||||||
}} />
|
|
||||||
</Tooltip>
|
|
||||||
{/* 下载按钮 */}
|
{/* 下载按钮 */}
|
||||||
<Tooltip placement="top" title="Download video">
|
<Tooltip placement="top" title="Download">
|
||||||
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
|
<GlassIconButton
|
||||||
setIsLoadingDownloadBtn(true);
|
icon={Download}
|
||||||
await downloadVideo(taskObject.final.url);
|
size='sm'
|
||||||
setIsLoadingDownloadBtn(false);
|
onClick={() => {
|
||||||
}} />
|
const totalVideos = taskObject.videos.data.filter((video: any) => video.urls && video.urls.length > 0).length;
|
||||||
|
showDownloadOptionsModal({
|
||||||
|
currentVideoIndex: 0,
|
||||||
|
totalVideos: totalVideos + 1,
|
||||||
|
isCurrentVideoFailed: false,
|
||||||
|
isFinalStage: true,
|
||||||
|
onDownloadCurrent: async () => {
|
||||||
|
setIsLoadingDownloadBtn(true);
|
||||||
|
await downloadVideo(taskObject.final.url);
|
||||||
|
setIsLoadingDownloadBtn(false);
|
||||||
|
},
|
||||||
|
onDownloadAll: async () => {
|
||||||
|
setIsLoadingDownloadAllVideosBtn(true);
|
||||||
|
const all = taskObject.videos.data.flatMap((video: any) => video.urls);
|
||||||
|
all.push(taskObject.final.url);
|
||||||
|
await downloadAllVideos(all);
|
||||||
|
setIsLoadingDownloadAllVideosBtn(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showGotoCutButton && (
|
{showGotoCutButton && (
|
||||||
<Tooltip placement="top" title='Go to AI-powered editing platform'>
|
<Tooltip placement="top" title='Go to AI-powered editing platform'>
|
||||||
@ -648,15 +661,39 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip placement="top" title="Download video">
|
<Tooltip placement="top" title="Download">
|
||||||
<GlassIconButton icon={Download} loading={isLoadingDownloadBtn} size='sm' onClick={async () => {
|
<GlassIconButton
|
||||||
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
icon={Download}
|
||||||
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0) {
|
size='sm'
|
||||||
setIsLoadingDownloadBtn(true);
|
onClick={() => {
|
||||||
await downloadVideo(currentVideo.urls[0]);
|
const currentVideo = taskObject.videos.data[currentSketchIndex];
|
||||||
setIsLoadingDownloadBtn(false);
|
const totalVideos = taskObject.videos.data.filter((video: any) => video.urls && video.urls.length > 0).length;
|
||||||
}
|
const isCurrentVideoFailed = currentVideo.video_status === 2;
|
||||||
}} />
|
|
||||||
|
showDownloadOptionsModal({
|
||||||
|
currentVideoIndex: currentSketchIndex,
|
||||||
|
totalVideos: taskObject.final.url ? totalVideos + 1 : totalVideos,
|
||||||
|
isCurrentVideoFailed: isCurrentVideoFailed,
|
||||||
|
isFinalStage: false,
|
||||||
|
onDownloadCurrent: async () => {
|
||||||
|
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0) {
|
||||||
|
setIsLoadingDownloadBtn(true);
|
||||||
|
await downloadVideo(currentVideo.urls[0]);
|
||||||
|
setIsLoadingDownloadBtn(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDownloadAll: async () => {
|
||||||
|
setIsLoadingDownloadAllVideosBtn(true);
|
||||||
|
const all = taskObject.videos.data.flatMap((video: any) => video.urls);
|
||||||
|
if (taskObject.final.url) {
|
||||||
|
all.push(taskObject.final.url);
|
||||||
|
}
|
||||||
|
await downloadAllVideos(all);
|
||||||
|
setIsLoadingDownloadAllVideosBtn(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -681,14 +718,6 @@ export const MediaViewer = React.memo(function MediaViewer({
|
|||||||
}
|
}
|
||||||
}} />
|
}} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* 下载所有视频按钮 */}
|
|
||||||
<Tooltip placement="top" title="Download all videos">
|
|
||||||
<GlassIconButton icon={ArrowDownWideNarrow} size='sm' loading={isLoadingDownloadAllVideosBtn} onClick={ async () => {
|
|
||||||
setIsLoadingDownloadAllVideosBtn(true);
|
|
||||||
await downloadAllVideos(taskObject.videos.data.flatMap((video: any) => video.urls));
|
|
||||||
setIsLoadingDownloadAllVideosBtn(false);
|
|
||||||
}} />
|
|
||||||
</Tooltip>
|
|
||||||
{/* 跳转剪辑按钮 */}
|
{/* 跳转剪辑按钮 */}
|
||||||
{showGotoCutButton && (
|
{showGotoCutButton && (
|
||||||
<Tooltip placement="top" title='Go to AI-powered editing platform'>
|
<Tooltip placement="top" title='Go to AI-powered editing platform'>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user