forked from 77media/video-flow
H5下载按钮合并
This commit is contained in:
parent
f22af3df3a
commit
f2050530e7
@ -11,6 +11,7 @@ import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||
import { Drawer } from 'antd';
|
||||
import error_image from '@/public/assets/error.webp';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
|
||||
interface H5MediaViewerProps {
|
||||
/** 任务对象,包含各阶段数据 */
|
||||
@ -54,6 +55,142 @@ interface H5MediaViewerProps {
|
||||
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()}
|
||||
>
|
||||
<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>
|
||||
<button
|
||||
data-alt="cancel-button"
|
||||
className="w-full text-sm text-purple-400 underline underline-offset-2 decoration-purple-400/60"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</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 的媒体预览组件。
|
||||
* - 除剧本阶段外,统一使用 antd Carousel 展示 图片/视频。
|
||||
@ -455,35 +592,40 @@ export function H5MediaViewer({
|
||||
}}
|
||||
/>
|
||||
<GlassIconButton
|
||||
data-alt="download-all-button"
|
||||
data-alt="download-button"
|
||||
className="w-8 h-8 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
|
||||
icon={ArrowDownWideNarrow}
|
||||
icon={Download}
|
||||
size="sm"
|
||||
aria-label="download-all"
|
||||
onClick={async () => {
|
||||
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
|
||||
await downloadAllVideos(all);
|
||||
aria-label="download"
|
||||
onClick={() => {
|
||||
const current = (taskObject.videos?.data ?? [])[activeIndex] as any;
|
||||
const status = current?.video_status;
|
||||
const hasUrl = current && Array.isArray(current.urls) && current.urls.length > 0;
|
||||
const hasFinalVideo = taskObject.final?.url;
|
||||
const baseVideoCount = (taskObject.videos?.data ?? []).length;
|
||||
const totalVideos = hasFinalVideo ? baseVideoCount + 1 : baseVideoCount;
|
||||
const isCurrentVideoFailed = status === 2;
|
||||
|
||||
showDownloadOptionsModal({
|
||||
currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex,
|
||||
totalVideos,
|
||||
isCurrentVideoFailed,
|
||||
onDownloadCurrent: async () => {
|
||||
if (hasUrl) {
|
||||
await downloadVideo(current.urls[0]);
|
||||
}
|
||||
},
|
||||
onDownloadAll: async () => {
|
||||
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
|
||||
if (hasFinalVideo) {
|
||||
all.push(taskObject.final.url);
|
||||
}
|
||||
console.log('h5-media-viewer:all', all);
|
||||
await downloadAllVideos(all);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
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-purple-600/80 to-purple-700/80 backdrop-blur-xl rounded-full flex items-center justify-center 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 ? (
|
||||
@ -503,30 +645,36 @@ export function H5MediaViewer({
|
||||
</>
|
||||
)}
|
||||
{stage === 'final_video' && (
|
||||
<>
|
||||
<GlassIconButton
|
||||
data-alt="download-all-button"
|
||||
className="w-8 h-8 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
|
||||
icon={ArrowDownWideNarrow}
|
||||
size="sm"
|
||||
aria-label="download-all"
|
||||
onClick={async () => {
|
||||
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
|
||||
await downloadAllVideos(all);
|
||||
}}
|
||||
/>
|
||||
<GlassIconButton
|
||||
data-alt="download-final-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-final"
|
||||
onClick={async () => {
|
||||
const url = videoUrls[0];
|
||||
if (url) await downloadVideo(url);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<GlassIconButton
|
||||
data-alt="download-button"
|
||||
className="w-8 h-8 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
|
||||
icon={Download}
|
||||
size="sm"
|
||||
aria-label="download"
|
||||
onClick={() => {
|
||||
const totalVideos = (taskObject.videos?.data ?? []).length + 1;
|
||||
const finalUrl = videoUrls[0];
|
||||
|
||||
showDownloadOptionsModal({
|
||||
currentVideoIndex: 0,
|
||||
totalVideos,
|
||||
isCurrentVideoFailed: false,
|
||||
isFinalStage: true,
|
||||
onDownloadCurrent: async () => {
|
||||
if (finalUrl) {
|
||||
await downloadVideo(finalUrl);
|
||||
}
|
||||
},
|
||||
onDownloadAll: async () => {
|
||||
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []);
|
||||
if (finalUrl) {
|
||||
all.push(finalUrl);
|
||||
}
|
||||
await downloadAllVideos(all);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -443,7 +443,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
if (task.task_name === 'combiner_videos') {
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
taskCurrent.currentStage = 'final_video';
|
||||
taskCurrent.final.url = task.task_result.video;
|
||||
taskCurrent.final.url = task.task_result.video_url;
|
||||
taskCurrent.final.note = 'combiner';
|
||||
taskCurrent.status = 'COMPLETED';
|
||||
}
|
||||
@ -463,7 +463,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
if (task.task_name === 'watermark_videos') {
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
taskCurrent.currentStage = 'final_video';
|
||||
taskCurrent.final.url = task.task_result.video;
|
||||
taskCurrent.final.url = task.task_result.video_url;
|
||||
taskCurrent.final.note = 'watermark';
|
||||
taskCurrent.status = 'COMPLETED';
|
||||
// 停止轮询
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user