Update
This commit is contained in:
@@ -1,32 +1,115 @@
|
|||||||
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>
|
||||||
|
|
||||||
|
{!isExporting && !isComplete && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="bg-muted/30 rounded-xl border p-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="flex items-center gap-1 font-medium">
|
||||||
|
<Clock10Icon className="h-4 w-4" /> Estimated export time
|
||||||
|
</p>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{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">
|
<div className="mb-4 flex items-center space-x-2">
|
||||||
<Checkbox id="show-debug" checked={showDebug} onCheckedChange={setShowDebug} />
|
<Checkbox id="show-debug" checked={showDebug} onCheckedChange={setShowDebug} />
|
||||||
<label
|
<label
|
||||||
@@ -35,13 +118,60 @@ const VideoDownloadModal = ({ isOpen, onClose, ffmpegCommand, handleDownloadButt
|
|||||||
>
|
>
|
||||||
Debug FFMPEG command
|
Debug FFMPEG command
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{showDebug && <Textarea value={ffmpegCommand} readOnly className="mb-4" />}
|
{showDebug && <Textarea value={ffmpegCommand} readOnly className="mb-4" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button onClick={handleDownloadButton}>{isExporting ? <Spinner className="text-secondary h-4 w-4" /> : 'Export Video'}</Button>
|
{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>
|
||||||
|
|
||||||
{/* Add your content here */}
|
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user