forked from 77media/video-flow
183 lines
7.1 KiB
TypeScript
183 lines
7.1 KiB
TypeScript
'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} />
|
||
);
|
||
}
|
||
|