This commit is contained in:
ct
2025-07-03 02:47:11 +08:00
parent 2f9f34ebc7
commit 08935c8a82
3 changed files with 171 additions and 33 deletions

View File

@@ -1,47 +1,177 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Spinner } from '@/components/ui/spinner'; import { Progress } from '@/components/ui/progress';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { CheckCircle, Clock10Icon, Download, Droplets } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
const VideoDownloadModal = ({ isOpen, onClose, ffmpegCommand, handleDownloadButton, isExporting, exportProgress, exportStatus }) => { const VideoDownloadModal = ({
nonWatermarkVideosLeft = 0,
isOpen,
onClose,
ffmpegCommand,
handleDownloadButton,
isExporting,
exportProgress,
exportStatus,
}) => {
const [showDebug, setShowDebug] = useState(false); const [showDebug, setShowDebug] = useState(false);
const [exportType, setExportType] = useState(null);
const handleExportWithoutWatermark = () => {
setExportType('without');
handleDownloadButton();
};
const handleExportWithWatermark = () => {
setExportType('with');
handleDownloadButton();
};
const handleClose = () => {
onClose();
setTimeout(() => {
setExportType(null);
}, 300);
};
const isComplete = exportProgress >= 100 && !isExporting;
const exportTimes = [
{
icon: '🐇',
label: 'Modern and fast devices',
time: '1-2 mins',
},
{
icon: '🐢',
label: 'Medium range devices',
time: '3-5 mins',
},
{
icon: '🥔',
label: 'Older / potato devices',
time: '>5 mins',
},
];
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={!isExporting ? onClose : undefined}>
<DialogContent> <DialogContent
<DialogHeader> className="border-2 shadow-2xl sm:max-w-lg [&>button]:hidden"
<DialogTitle>Export Video</DialogTitle> onInteractOutside={(e) => isExporting && e.preventDefault()}
{exportStatus || onEscapeKeyDown={(e) => isExporting && e.preventDefault()}
(exportProgress > 0 && ( >
<DialogDescription> <DialogHeader className="space-y-4">
<div className="flex items-center justify-center"> <DialogTitle className="text-2xl font-semibold tracking-tight">Export Video</DialogTitle>
<div className="flex items-center space-x-2"> <DialogDescription className="hidden text-base leading-relaxed"></DialogDescription>
<span className="text-sm font-medium">{exportStatus}</span>
<span className="text-xs font-medium">{exportProgress}%</span>
</div>
<Spinner className={'h-5 w-5'} />
</div>
</DialogDescription>
))}
</DialogHeader> </DialogHeader>
<div className="mb-4 flex items-center space-x-2"> {!isExporting && !isComplete && (
<Checkbox id="show-debug" checked={showDebug} onCheckedChange={setShowDebug} /> <div className="space-y-8">
<label <div className="bg-muted/30 rounded-xl border p-6">
htmlFor="show-debug" <div className="flex items-start gap-3">
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" <div className="space-y-3">
> <p className="flex items-center gap-1 font-medium">
Debug FFMPEG command <Clock10Icon className="h-4 w-4" /> Estimated export time
</label> </p>
</div> <div className="text-muted-foreground space-y-2 text-sm">
{exportTimes.map((exportTime, index) => (
<div className="grid items-center justify-between sm:flex sm:gap-3">
<div className="grid items-center text-base sm:flex sm:gap-1">
<span className="text-2xl sm:text-base">{exportTime.icon}</span>
<span>{exportTime.label}</span>
</div>
<span className="font-mono text-xs sm:text-base">{exportTime.time}</span>
</div>
))}
</div>
</div>
</div>
</div>
{showDebug && <Textarea value={ffmpegCommand} readOnly className="mb-4" />} {nonWatermarkVideosLeft > 0 && (
<div className="space-y-4">
<Button
onClick={handleExportWithoutWatermark}
className="h-14 w-full text-base font-medium shadow-md transition-all duration-200 hover:shadow-lg"
size="lg"
>
Export without watermark ({nonWatermarkVideosLeft} left)
</Button>
<Button
onClick={handleExportWithWatermark}
variant="outline"
className="hover:bg-muted/50 h-14 w-full border-2 bg-transparent text-base font-medium transition-all duration-200"
size="lg"
>
Export
</Button>
</div>
)}
{/*
<div className="mb-4 flex items-center space-x-2">
<Checkbox id="show-debug" checked={showDebug} onCheckedChange={setShowDebug} />
<label
htmlFor="show-debug"
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Debug FFMPEG command
</label>
</div> */}
<Button onClick={handleDownloadButton}>{isExporting ? <Spinner className="text-secondary h-4 w-4" /> : 'Export Video'}</Button> {showDebug && <Textarea value={ffmpegCommand} readOnly className="mb-4" />}
</div>
)}
{/* Add your content here */} {isExporting && (
<div className="space-y-8 py-4">
<div className="space-y-4 text-center">
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
{exportType === 'without' ? (
<Download className="h-8 w-8 animate-pulse" />
) : (
<Droplets className="h-8 w-8 animate-pulse" />
)}
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold">Exporting {exportType === 'without' ? 'without' : 'with'} watermark</h3>
<p className="text-muted-foreground">Please wait while we prepare your export...</p>
{exportStatus && <p className="text-muted-foreground text-sm text-wrap">{exportStatus}</p>}
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Progress</span>
<span className="font-mono text-sm font-semibold">{Math.round(exportProgress)}%</span>
</div>
<Progress value={exportProgress} className="h-3" />
</div>
</div>
)}
{isComplete && (
<div className="space-y-8 py-4">
<div className="space-y-4 text-center">
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
<CheckCircle className="h-8 w-8" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold">Export Complete!</h3>
<p className="text-muted-foreground">
Your video has been exported successfully {exportType === 'without' ? 'without' : 'with'} watermark.
</p>
</div>
</div>
<div className="flex justify-center">
<Button onClick={handleClose} className="h-12 w-full font-medium">
Close
</Button>
</div>
</div>
)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@@ -1,5 +1,6 @@
import { useMitt } from '@/plugins/MittContext'; import { useMitt } from '@/plugins/MittContext';
import useMediaStore from '@/stores/MediaStore'; import useMediaStore from '@/stores/MediaStore';
import useUserStore from '@/stores/UserStore';
import useVideoEditorStore from '@/stores/VideoEditorStore'; import useVideoEditorStore from '@/stores/VideoEditorStore';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import SINGLE_CAPTION_TEMPLATE from '../../templates/single_caption_meme_background.json'; import SINGLE_CAPTION_TEMPLATE from '../../templates/single_caption_meme_background.json';
@@ -9,6 +10,8 @@ import useVideoExport from './video-export';
import VideoPreview from './video-preview'; import VideoPreview from './video-preview';
const VideoEditor = ({ width, height, onOpenTextSidebar }) => { const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
const { user_usage } = useUserStore();
const [isDownloadModalOpen, setIsDownloadModalOpen] = useState(false); const [isDownloadModalOpen, setIsDownloadModalOpen] = useState(false);
const [showConsoleLogs] = useState(true); const [showConsoleLogs] = useState(true);
@@ -609,6 +612,7 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
/> />
</div> </div>
<VideoDownloadModal <VideoDownloadModal
nonWatermarkVideosLeft={user_usage?.non_watermark_videos_left}
isOpen={isDownloadModalOpen} isOpen={isDownloadModalOpen}
onClose={() => setIsDownloadModalOpen(false)} onClose={() => setIsDownloadModalOpen(false)}
ffmpegCommand={ffmpegCommand} ffmpegCommand={ffmpegCommand}

View File

@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
// Import centralized font management // Import centralized font management
import { getFontStyle, loadTimelineFonts, WATERMARK_CONFIG } from '@/modules/editor/fonts'; import { getFontStyle, loadTimelineFonts, WATERMARK_CONFIG } from '@/modules/editor/fonts';
import { toast } from 'sonner';
const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermarked = false }) => { const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermarked = false }) => {
const [showConsoleLogs] = useState(true); const [showConsoleLogs] = useState(true);
@@ -542,7 +543,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermark
setExportStatus('Starting export...'); setExportStatus('Starting export...');
try { try {
setExportStatus('Loading FFmpeg...'); setExportStatus('Setting up...');
const ffmpeg = new FFmpeg(); const ffmpeg = new FFmpeg();
@@ -710,7 +711,10 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermark
path: error.path, path: error.path,
error: error, error: error,
}); });
setExportStatus(`Failed: ${error.message}`);
setExportStatus('Export failed.');
toast.error('Export failed. Please try again! If the problem persists, please contact us.');
} finally { } finally {
setTimeout(() => { setTimeout(() => {
setIsExporting(false); setIsExporting(false);