import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; import { Spinner } from '@/components/ui/spinner.js'; import { Textarea } from '@/components/ui/textarea'; import useUserStore from '@/stores/UserStore'; import { Clock10Icon, Download } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; const VideoDownloadModal = ({ nonWatermarkVideosLeft = 0, isOpen, onClose, ffmpegCommand, handleDownloadButton, isExporting, exportProgress, exportStatus, videoBlob, videoBlobFilename, }) => { const [showDebug, setShowDebug] = useState(false); const [isPremiumExport, setIsPremiumExport] = useState(false); const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null); const [status, setStatus] = useState('start'); // 'start', 'processing', 'complete' const [downloadState, setDownloadState] = useState('idle'); // 'idle', 'downloading', 'downloaded' const [exportToken, setExportToken] = useState(null); const exportStartTime = useRef(null); const lastProgressTime = useRef(null); const lastProgress = useRef(0); const { premiumExportRequest, premiumExportComplete, basicExportRequest, basicExportComplete } = 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 () => { setIsPremiumExport(true); setEstimatedTimeRemaining(null); // Call premiumExportRequest and check response const response = await premiumExportRequest(); if (response?.error) { // Halt the process if there's an error setIsPremiumExport(false); return; } if (response?.success) { // Store the export token const token = response.success.data.export_token; console.log('Received export token:', token); setExportToken(token); // Continue with export if successful setStatus('processing'); handleDownloadButton(); } }; const handleExportWithWatermark = async () => { setIsPremiumExport(false); setEstimatedTimeRemaining(null); // Call basicExportRequest and check response const response = await basicExportRequest(); if (response?.error) { // Halt the process if there's an error setIsPremiumExport(false); return; } if (response?.success) { // Store the export token const token = response.success.data.export_token; console.log('Received basic export token:', token); setExportToken(token); // Continue with export if successful setStatus('processing'); handleDownloadButton(); } }; const handleClose = async () => { onClose(); setTimeout(() => { setIsPremiumExport(false); setEstimatedTimeRemaining(null); setStatus('start'); setExportToken(null); exportStartTime.current = null; lastProgressTime.current = null; lastProgress.current = 0; }, 300); }; // Update status based on export progress - transition to complete immediately when 100% useEffect(() => { if (status === 'processing' && exportProgress >= 100) { setStatus('complete'); // Call appropriate export complete method based on export type if (isPremiumExport && exportToken) { console.log('Calling premiumExportComplete with token:', exportToken); premiumExportComplete(exportToken); } else if (!isPremiumExport && exportToken) { console.log('Calling basicExportComplete with token:', exportToken); basicExportComplete(exportToken); } else if (!exportToken) { console.error('Export completed but no token available'); } } }, [exportProgress, status, isPremiumExport, exportToken, premiumExportComplete, basicExportComplete]); // Calculate estimated time remaining based on progress speed useEffect(() => { if (status !== 'processing' || exportProgress === 0) { exportStartTime.current = null; lastProgressTime.current = null; lastProgress.current = 0; setEstimatedTimeRemaining(null); return; } // Clear estimate when complete if (exportProgress >= 100) { setEstimatedTimeRemaining(0); return; } const now = Date.now(); // Initialize timing on first progress if (!exportStartTime.current) { exportStartTime.current = now; lastProgress.current = exportProgress; console.log('Initialized timing at', exportProgress + '%'); return; } // Calculate every time progress changes after initial 3 seconds const timeSinceStart = now - exportStartTime.current; const progressDelta = exportProgress - lastProgress.current; console.log( 'Progress:', exportProgress + '%', 'Time since start:', Math.round(timeSinceStart / 1000) + 's', 'Progress delta:', progressDelta + '%', ); if (timeSinceStart > 2000 && progressDelta > 0) { const progressRate = progressDelta / timeSinceStart; // progress per ms const remainingProgress = 100 - exportProgress; const estimatedMs = remainingProgress / progressRate; const estimatedSeconds = Math.round(estimatedMs / 1000); console.log('Progress rate:', progressRate, 'Estimated seconds:', estimatedSeconds); if (estimatedSeconds >= 1 && estimatedSeconds <= 600) { setEstimatedTimeRemaining(estimatedSeconds); console.log('Set estimated time:', estimatedSeconds + 's'); } else { console.log('Time estimate out of range:', estimatedSeconds + 's'); } } }, [status, exportProgress]); const formatTimeRemaining = (seconds) => { if (seconds < 60) { return `~${seconds}s remaining`; } else { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `~${minutes}m ${remainingSeconds}s remaining`; } }; 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 ( status === 'processing' && e.preventDefault()} onEscapeKeyDown={(e) => status === 'processing' && e.preventDefault()} > Export Video {status === 'start' && (

Estimated export time

{exportTimes.map((exportTime, index) => (
{exportTime.icon} {exportTime.label}
{exportTime.time}
))}
{nonWatermarkVideosLeft > 0 && ( )}
{showDebug &&