video-flow-b/components/pages/work-flow/download-options-modal.tsx
2025-10-11 11:45:30 +08:00

148 lines
5.9 KiB
TypeScript

'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} />
);
}