Update
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Spinner } from '@/components/ui/spinner.js';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import useUserStore from '@/stores/UserStore';
|
||||||
import { Clock10Icon, Download, Droplets } from 'lucide-react';
|
import { Clock10Icon, Download, Droplets } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useUserStore from '@/stores/UserStore';
|
|
||||||
|
|
||||||
const VideoDownloadModal = ({
|
const VideoDownloadModal = ({
|
||||||
nonWatermarkVideosLeft = 0,
|
nonWatermarkVideosLeft = 0,
|
||||||
@@ -15,31 +16,94 @@ const VideoDownloadModal = ({
|
|||||||
isExporting,
|
isExporting,
|
||||||
exportProgress,
|
exportProgress,
|
||||||
exportStatus,
|
exportStatus,
|
||||||
|
videoBlob,
|
||||||
|
videoBlobFilename,
|
||||||
}) => {
|
}) => {
|
||||||
const [showDebug, setShowDebug] = useState(false);
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
const [isPremiumExport, setIsPremiumExport] = useState(false);
|
const [isPremiumExport, setIsPremiumExport] = useState(false);
|
||||||
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
|
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
|
||||||
const [status, setStatus] = useState('start'); // 'start', 'processing', 'complete'
|
const [status, setStatus] = useState('start'); // 'start', 'processing', 'complete'
|
||||||
|
const [downloadState, setDownloadState] = useState('idle'); // 'idle', 'downloading', 'downloaded'
|
||||||
|
|
||||||
const exportStartTime = useRef(null);
|
const exportStartTime = useRef(null);
|
||||||
const lastProgressTime = useRef(null);
|
const lastProgressTime = useRef(null);
|
||||||
const lastProgress = useRef(0);
|
const lastProgress = useRef(0);
|
||||||
|
|
||||||
const { premiumExportRequest, premiumExportComplete } = useUserStore();
|
const { premiumExportRequest, premiumExportComplete } = useUserStore();
|
||||||
|
|
||||||
|
const handleShareOrDownload = async () => {
|
||||||
|
if (!videoBlob || !videoBlobFilename) {
|
||||||
|
console.error('No video blob available for sharing/download');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDownloadState('downloading');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if mobile and supports navigator.share
|
||||||
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||||
|
const canShare = isMobile && navigator.share && navigator.canShare;
|
||||||
|
|
||||||
|
if (canShare) {
|
||||||
|
try {
|
||||||
|
const files = [new File([videoBlob], videoBlobFilename, { type: 'video/mp4' })];
|
||||||
|
if (navigator.canShare({ files })) {
|
||||||
|
await navigator.share({ files });
|
||||||
|
} else {
|
||||||
|
// Fallback to download if sharing files isn't supported
|
||||||
|
const url = URL.createObjectURL(videoBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = videoBlobFilename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
} catch (shareError) {
|
||||||
|
console.log('Share failed, falling back to download:', shareError);
|
||||||
|
// Fallback to download
|
||||||
|
const url = URL.createObjectURL(videoBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = videoBlobFilename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Desktop or unsupported mobile - use download
|
||||||
|
const url = URL.createObjectURL(videoBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = videoBlobFilename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to downloaded state
|
||||||
|
setDownloadState('downloaded');
|
||||||
|
|
||||||
|
// Reset to idle after 1.5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
setDownloadState('idle');
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download/share failed:', error);
|
||||||
|
setDownloadState('idle');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleExportWithoutWatermark = async () => {
|
const handleExportWithoutWatermark = async () => {
|
||||||
setIsPremiumExport(true);
|
setIsPremiumExport(true);
|
||||||
setEstimatedTimeRemaining(null);
|
setEstimatedTimeRemaining(null);
|
||||||
|
|
||||||
// Call premiumExportRequest and check response
|
// Call premiumExportRequest and check response
|
||||||
const response = await premiumExportRequest();
|
const response = await premiumExportRequest();
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
// Halt the process if there's an error
|
// Halt the process if there's an error
|
||||||
setIsPremiumExport(false);
|
setIsPremiumExport(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response?.success) {
|
if (response?.success) {
|
||||||
// Continue with export if successful
|
// Continue with export if successful
|
||||||
setStatus('processing');
|
setStatus('processing');
|
||||||
@@ -227,11 +291,7 @@ const VideoDownloadModal = ({
|
|||||||
<div className="space-y-8 py-4">
|
<div className="space-y-8 py-4">
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
|
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
|
||||||
{isPremiumExport ? (
|
{isPremiumExport ? <Download className="h-8 w-8 animate-pulse" /> : <Droplets className="h-8 w-8 animate-pulse" />}
|
||||||
<Download className="h-8 w-8 animate-pulse" />
|
|
||||||
) : (
|
|
||||||
<Droplets className="h-8 w-8 animate-pulse" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xl font-semibold">Exporting {isPremiumExport ? 'without watermark' : ''}</h3>
|
<h3 className="text-xl font-semibold">Exporting {isPremiumExport ? 'without watermark' : ''}</h3>
|
||||||
@@ -273,9 +333,36 @@ const VideoDownloadModal = ({
|
|||||||
<h3 className="text-xl font-semibold">Export Complete!</h3>
|
<h3 className="text-xl font-semibold">Export Complete!</h3>
|
||||||
<p className="text-muted-foreground text-sm">Your video has been successfully exported.</p>
|
<p className="text-muted-foreground text-sm">Your video has been successfully exported.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={handleClose} className="w-full" size="lg">
|
<div className="space-y-3">
|
||||||
Close
|
<Button
|
||||||
</Button>
|
onClick={handleShareOrDownload}
|
||||||
|
disabled={downloadState !== 'idle'}
|
||||||
|
className="h-14 w-full text-base font-medium shadow-md transition-all duration-200 hover:shadow-lg"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{downloadState === 'downloading' && (
|
||||||
|
<>
|
||||||
|
<Spinner className="w-4 h-4 mr-2" />
|
||||||
|
Downloading
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{downloadState === 'downloaded' && (
|
||||||
|
<>
|
||||||
|
<Download className="mr-2 h-5 w-5" />
|
||||||
|
Downloaded!
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{downloadState === 'idle' && (
|
||||||
|
<>
|
||||||
|
<Download className="mr-2 h-5 w-5" />
|
||||||
|
Download Video
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={handleClose} className="w-full" size="lg">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
|||||||
|
|
||||||
const totalDuration = Math.max(...timelineElements.map((el) => el.startTime + el.duration));
|
const totalDuration = Math.max(...timelineElements.map((el) => el.startTime + el.duration));
|
||||||
|
|
||||||
const { isExporting, exportProgress, exportStatus, ffmpegCommand, copyFFmpegCommand, exportVideo } = useVideoExport({
|
const { isExporting, exportProgress, exportStatus, ffmpegCommand, copyFFmpegCommand, exportVideo, videoBlob, videoBlobFilename } = useVideoExport({
|
||||||
timelineElements,
|
timelineElements,
|
||||||
dimensions,
|
dimensions,
|
||||||
totalDuration,
|
totalDuration,
|
||||||
@@ -620,6 +620,8 @@ const VideoEditor = ({ width, height, onOpenTextSidebar }) => {
|
|||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
exportProgress={exportProgress}
|
exportProgress={exportProgress}
|
||||||
exportStatus={exportStatus}
|
exportStatus={exportStatus}
|
||||||
|
videoBlob={videoBlob}
|
||||||
|
videoBlobFilename={videoBlobFilename}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermark
|
|||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
const [exportProgress, setExportProgress] = useState(0);
|
const [exportProgress, setExportProgress] = useState(0);
|
||||||
const [exportStatus, setExportStatus] = useState('');
|
const [exportStatus, setExportStatus] = useState('');
|
||||||
|
const [videoBlob, setVideoBlob] = useState(null);
|
||||||
|
const [videoBlobFilename, setVideoBlobFilename] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(JSON.stringify(timelineElements));
|
console.log(JSON.stringify(timelineElements));
|
||||||
@@ -692,43 +694,9 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermark
|
|||||||
const epochTimestamp = Date.now();
|
const epochTimestamp = Date.now();
|
||||||
const fileName = `memeaigen-${epochTimestamp}.mp4`;
|
const fileName = `memeaigen-${epochTimestamp}.mp4`;
|
||||||
|
|
||||||
// Check if mobile and supports navigator.share
|
// Store the blob and filename in state instead of auto-downloading
|
||||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
setVideoBlob(blob);
|
||||||
const canShare = isMobile && navigator.share && navigator.canShare;
|
setVideoBlobFilename(fileName);
|
||||||
|
|
||||||
if (canShare) {
|
|
||||||
try {
|
|
||||||
const files = [new File([blob], fileName, { type: "video/mp4" })];
|
|
||||||
if (navigator.canShare({ files })) {
|
|
||||||
await navigator.share({ files });
|
|
||||||
} else {
|
|
||||||
// Fallback to download if sharing files isn't supported
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = fileName;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
} catch (shareError) {
|
|
||||||
console.log('Share failed, falling back to download:', shareError);
|
|
||||||
// Fallback to download
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = fileName;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Desktop or unsupported mobile - use download
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = fileName;
|
|
||||||
link.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
setExportProgress(100);
|
setExportProgress(100);
|
||||||
setExportStatus('Complete!');
|
setExportStatus('Complete!');
|
||||||
@@ -763,6 +731,8 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration, watermark
|
|||||||
exportProgress,
|
exportProgress,
|
||||||
exportStatus,
|
exportStatus,
|
||||||
ffmpegCommand,
|
ffmpegCommand,
|
||||||
|
videoBlob,
|
||||||
|
videoBlobFilename,
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
copyFFmpegCommand,
|
copyFFmpegCommand,
|
||||||
|
|||||||
Reference in New Issue
Block a user