video-flow-b/components/pages/work-flow/download-options-modal.tsx
2025-10-12 00:32:56 +08:00

183 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useRef, useState } from 'react';
import { Checkbox } from 'antd';
import { createRoot, Root } from 'react-dom/client';
import { X, Download, ArrowDownWideNarrow } from 'lucide-react';
import { post } from '@/api/request';
interface DownloadOptionsModalProps {
onDownloadCurrent: (withWatermark: boolean) => void;
onDownloadAll: (withWatermark: boolean) => void;
onClose: () => void;
currentVideoIndex: number;
totalVideos: number;
/** 当前视频是否生成失败 */
isCurrentVideoFailed: boolean;
/** 是否为最终视频阶段 */
isFinalStage?: boolean;
/** 项目ID */
projectId?: string;
/** 视频ID分镜视频可用 */
videoId?: string;
}
/**
* 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, projectId, videoId } = props;
const containerRef = useRef<HTMLDivElement | null>(null);
const [withWatermark, setWithWatermark] = useState<boolean>(true);
const [baseAmount, setBaseAmount] = useState<number>(0);
const [isCheckingBalance, setIsCheckingBalance] = useState<boolean>(false);
useEffect(() => {
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalOverflow;
};
}, []);
// 监听水印选择变化,请求价格信息
useEffect(() => {
let aborted = false;
const checkBalance = async () => {
try {
if (!aborted) setIsCheckingBalance(true);
const json: any = await post('/movie/download_video', {
project_id: projectId,
video_id: videoId,
watermark: withWatermark,
check_balance: true
});
const amount = json?.data?.base_amount;
if (!aborted) setBaseAmount(Number.isFinite(amount) ? Number(amount) : 0);
} catch {
if (!aborted) setBaseAmount(0);
} finally {
if (!aborted) setIsCheckingBalance(false);
}
};
void checkBalance();
return () => {
aborted = true;
};
}, [withWatermark]);
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>
<div data-alt="price-indicator" className="mt-2 text-center text-sm font-medium">
{!withWatermark && baseAmount && baseAmount !== 0 ? (
<span className="text-red-400">-{baseAmount} credits</span>
) : (
<span className="text-green-400">free</span>
)}
</div>
<div data-alt="watermark-select" className="mt-3 flex items-center justify-center">
<div data-alt="watermark-toggle" className="inline-flex items-center gap-2 text-sm text-white/90">
<Checkbox
data-alt="watermark-checkbox"
checked={!withWatermark}
onChange={(e) => setWithWatermark(!e.target.checked)}
/>
<span>without watermark</span>
</div>
</div>
{/* stats-info hidden temporarily due to no batch billing support */}
</div>
<div data-alt="modal-actions" className="mt-6 space-y-3">
<button
data-alt="download-single-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 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={isCheckingBalance}
onClick={() => {
onDownloadCurrent(withWatermark);
onClose();
}}
>
<Download size={16} />
Download Video
</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} />
);
}