From 8709a8e108e8f729c97c5654a36134d5d22d1dfe Mon Sep 17 00:00:00 2001 From: Ciaran O'Reilly Date: Sun, 17 Nov 2024 01:17:03 +0100 Subject: [PATCH] feat: improve download modal and handle errors --- src/App.tsx | 108 +++++++++++----------- src/components/DownloadModal.tsx | 139 +++++++++++++++++++++++------ src/components/LoadingOverlay.tsx | 51 ----------- src/components/RegenerateModal.tsx | 31 ++++--- src/locales/ca.json | 13 ++- src/locales/en.json | 13 ++- src/locales/es.json | 13 ++- src/services/FFmpegService.ts | 15 +++- src/styles/designSystem.ts | 30 +++++++ 9 files changed, 264 insertions(+), 149 deletions(-) delete mode 100644 src/components/LoadingOverlay.tsx diff --git a/src/App.tsx b/src/App.tsx index bf64377..1bfeada 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,6 @@ import { DubbingAPIService } from './services/DubbingAPIService'; import { audioService } from './services/AudioService'; import VideoOptions from './components/VideoOptions'; import DownloadModal from './components/DownloadModal'; -import LoadingOverlay from './components/LoadingOverlay'; import { speakerService } from './services/SpeakerService'; import { Voice } from './types/Voice'; import { AudioTrack } from './types/AudioTrack'; @@ -149,7 +148,6 @@ function App() { const [selectedSubtitles, setSelectedSubtitles] = useState('none'); const [showSpeakerColors, setShowSpeakerColors] = useState(true); const [showDownloadModal, setShowDownloadModal] = useState(false); - const [isRebuilding, setIsRebuilding] = useState(false); const [appMode, setAppMode] = useState<'dubbing' | 'transcription' | 'file' | null>( process.env.APP_MODE as 'dubbing' | 'transcription' | 'file' | null ); @@ -160,6 +158,7 @@ function App() { const [isEditMode, setIsEditMode] = useState(false); const [timelineVisible, setTimelineVisible] = useState(false); const [showRegenerateModal, setShowRegenerateModal] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(''); useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -300,11 +299,10 @@ function App() { }, [serviceParam]); const loadChunksInBackground = useCallback(async (uuid: string, tracks: Track[]) => { - const dubbedTracks = tracks.filter(track => track.dubbed_path && track.for_dubbing); - const totalChunks = dubbedTracks.length; - let loadedChunks = 0; - try { + const dubbedTracks = tracks.filter(track => track.dubbed_path && track.for_dubbing); + const totalChunks = dubbedTracks.length; + let loadedChunks = 0; const newChunkBuffers: { [key: string]: ArrayBuffer } = {}; @@ -419,57 +417,57 @@ function App() { setShowDownloadModal(false); }; - const handleDownloadWithSelectedTracks = async (selectedAudioTracks: string[], selectedSubtitles: string[]) => { + const handleDownloadWithSelectedTracks = async ( + selectedAudioTracks: string[], + selectedSubtitles: string[], + ) => { if (mediaUrl && tracks.length > 0) { - try { - console.log("handleDownloadWithSelectedTracks", selectedAudioTracks, selectedSubtitles); - setIsRebuilding(true); - let fileToProcess: File | string = mediaFile || mediaUrl; - - // Download and decode background audio - const backgroundArrayBuffer = await audioService.downloadAudioURL(audioTracks.background.url); - const backgroundBuffer = await audioService.decodeAudioData(backgroundArrayBuffer); - - const selectedAudioBuffers: { buffer: AudioBuffer, label: string }[] = []; - - for (const selectedAudioTrack of selectedAudioTracks) { - let audioBuffer: AudioBuffer | null = null; - if (selectedAudioTrack === 'dubbed') { - audioBuffer = dubbedAudioBuffer; - } else { - const audioTrack = audioTracks[selectedAudioTrack]; - if (audioTrack) { - const audioArrayBuffer = await audioService.downloadAudioURL(audioTrack.url); - audioBuffer = await audioService.decodeAudioData(audioArrayBuffer); - } - } - if (audioBuffer && backgroundBuffer) { - const finalAudioBuffer = await audioService.mixAudioBuffers( - backgroundBuffer, - audioBuffer - ); - selectedAudioBuffers.push({ buffer: finalAudioBuffer, label: selectedAudioTrack }); - } else { - throw new Error(`Audio track ${selectedAudioTrack} not found`); + let fileToProcess: File | string = mediaFile || mediaUrl; + + setDownloadProgress(t('downloadingBackgroundAudio')); + // Download and decode background audio + const backgroundArrayBuffer = await audioService.downloadAudioURL(audioTracks.background.url); + const backgroundBuffer = await audioService.decodeAudioData(backgroundArrayBuffer); + + const selectedAudioBuffers: { buffer: AudioBuffer, label: string }[] = []; + + for (const selectedAudioTrack of selectedAudioTracks) { + setDownloadProgress(t('downloadingTrack', { track: audioTracks[selectedAudioTrack]?.label.toLowerCase() })); + let audioBuffer: AudioBuffer | null = null; + if (selectedAudioTrack === 'dubbed') { + audioBuffer = dubbedAudioBuffer; + } else { + const audioTrack = audioTracks[selectedAudioTrack]; + if (audioTrack) { + const audioArrayBuffer = await audioService.downloadAudioURL(audioTrack.url); + audioBuffer = await audioService.decodeAudioData(audioArrayBuffer); } } - console.log("selectedAudioBuffers", selectedAudioBuffers); - - const newMediaBlob = await rebuildMedia(fileToProcess, tracks, selectedAudioBuffers, selectedSubtitles); - const url = URL.createObjectURL(newMediaBlob); - const a = document.createElement('a'); - a.href = url; - a.download = `output_${mediaFileName}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } catch (error) { - console.error("Error downloading result:", error); - } finally { - setIsRebuilding(false); - setShowDownloadModal(false); + if (audioBuffer && backgroundBuffer) { + setDownloadProgress(t('mixingAudio', { track: audioTracks[selectedAudioTrack]?.label.toLowerCase() })); + const finalAudioBuffer = await audioService.mixAudioBuffers( + backgroundBuffer, + audioBuffer + ); + selectedAudioBuffers.push({ buffer: finalAudioBuffer, label: selectedAudioTrack }); + } else { + throw new Error(`Audio track ${selectedAudioTrack} not found`); + } } + + setDownloadProgress(t('rebuildingMediaOnDownload')); + const newMediaBlob = await rebuildMedia(fileToProcess, tracks, selectedAudioBuffers, selectedSubtitles, setDownloadProgress); + + setDownloadProgress(t('preparingDownload')); + throw new Error("test"); + const url = URL.createObjectURL(newMediaBlob); + const a = document.createElement('a'); + a.href = url; + a.download = `output_${mediaFileName}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); } }; @@ -755,10 +753,10 @@ function App() { subtitles={['original', 'dubbed']} onClose={handleDownloadModalClose} onDownload={handleDownloadWithSelectedTracks} - isRebuilding={isRebuilding} + onRegenerate={() => setShowRegenerateModal(true)} + progressMessage={downloadProgress} /> )} - {isRebuilding && } {showRegenerateModal && ( setShowRegenerateModal(false)} diff --git a/src/components/DownloadModal.tsx b/src/components/DownloadModal.tsx index f50d765..2b92b0b 100644 --- a/src/components/DownloadModal.tsx +++ b/src/components/DownloadModal.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; import { useTranslation } from 'react-i18next'; -import { Button, Label, ModalOverlay, colors } from '../styles/designSystem'; +import { Button, Label, ModalOverlay, colors, Title, ErrorMessage, Message, ErrorBox } from '../styles/designSystem'; import { AudioTrack } from '../types/AudioTrack'; const ModalContent = styled.div` @@ -12,11 +12,6 @@ const ModalContent = styled.div` width: 100%; `; -const Title = styled.h2` - color: ${colors.primary}; - margin-bottom: 20px; -`; - const TrackList = styled.div` margin-bottom: 20px; `; @@ -37,18 +32,54 @@ const ButtonContainer = styled.div` gap: 10px; `; +const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +`; + +const Spinner = styled.div` + border: 4px solid ${colors.background}; + border-top: 4px solid ${colors.primary}; + border-radius: 50%; + width: 40px; + height: 40px; + animation: ${spin} 1s linear infinite; +`; + +const LoadingContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + padding: 20px; +`; + +const Center = styled.div` + text-align: center; +`; + interface DownloadModalProps { audioTracks: { [key: string]: AudioTrack }; subtitles: string[]; onClose: () => void; - onDownload: (selectedAudioTracks: string[], selectedSubtitles: string[]) => void; - isRebuilding: boolean; + onDownload: (selectedAudioTracks: string[], selectedSubtitles: string[]) => Promise; + onRegenerate?: () => void; + progressMessage?: string; } -const DownloadModal: React.FC = ({ audioTracks, subtitles, onClose, onDownload, isRebuilding }) => { +const DownloadModal: React.FC = ({ + audioTracks, + subtitles, + onClose, + onDownload, + onRegenerate, + progressMessage, +}) => { const { t } = useTranslation(); - const [selectedAudioTracks, setSelectedAudioTracks] = useState([]); + const [selectedAudioTracks, setSelectedAudioTracks] = useState(['dubbed']); const [selectedSubtitles, setSelectedSubtitles] = useState([]); + const [isDownloading, setIsDownloading] = useState(false); + const [error, setError] = useState(null); const handleAudioTrackToggle = (id: string) => { setSelectedAudioTracks(prev => @@ -62,24 +93,63 @@ const DownloadModal: React.FC = ({ audioTracks, subtitles, o ); }; - const handleDownload = () => { - onDownload(selectedAudioTracks, selectedSubtitles); - onClose(); + const handleDownload = async () => { + setIsDownloading(true); + setError(null); + try { + await onDownload( + selectedAudioTracks, + selectedSubtitles + ); + onClose(); + } catch (error) { + console.error('Error downloading video:', error); + setError(`${error}`); + } finally { + setIsDownloading(false); + } + }; + + const handleRegenerateClick = () => { + if (onRegenerate) { + onRegenerate(); + onClose(); + } }; + if (error) { + return ( + + + {t('downloadError')} + {t('downloadErrorMessage')} + {error} + + + {onRegenerate && ( + + )} + + + + ); + } + return ( - {t('selectTracksForDownload')} - -

{t('audioTracks')}

- {Object.entries(audioTracks).filter(([id, track]) => id !== 'background').map(([id, track], index) => ( + {!isDownloading ? t('selectTracksForDownload') : t('preparingDownload')} + {!isDownloading ? ( + <> + +

{t('audioTracks')}

+ {Object.entries(audioTracks).filter(([id]) => id !== 'background').map(([id, track], index) => ( handleAudioTrackToggle(id)} - disabled={isRebuilding} + disabled={isDownloading} /> @@ -93,16 +163,33 @@ const DownloadModal: React.FC = ({ audioTracks, subtitles, o type="checkbox" checked={selectedSubtitles.includes(subtitle)} onChange={() => handleSubtitleToggle(subtitle)} - disabled={isRebuilding} + disabled={isDownloading} /> - ))} -
- - - - + ))} +
+ + + + + + ) : ( + + +
+ {progressMessage || t('downloading')} +
+
+ )}
); diff --git a/src/components/LoadingOverlay.tsx b/src/components/LoadingOverlay.tsx deleted file mode 100644 index 3d77c8e..0000000 --- a/src/components/LoadingOverlay.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import styled, { keyframes } from 'styled-components'; -import { colors } from '../styles/designSystem'; - -const spin = keyframes` - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -`; - -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; -`; - -const Spinner = styled.div` - border: 4px solid ${colors.background}; - border-top: 4px solid ${colors.primary}; - border-radius: 50%; - width: 40px; - height: 40px; - animation: ${spin} 1s linear infinite; -`; - -const LoadingText = styled.p` - color: ${colors.white}; - margin-top: 20px; - font-size: 18px; -`; - -interface LoadingOverlayProps { - message: string; -} - -const LoadingOverlay: React.FC = ({ message }) => ( - -
- - {message} -
-
-); - -export default LoadingOverlay; diff --git a/src/components/RegenerateModal.tsx b/src/components/RegenerateModal.tsx index 4b76c06..4760e73 100644 --- a/src/components/RegenerateModal.tsx +++ b/src/components/RegenerateModal.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; -import { Button, ModalOverlay } from '../styles/designSystem'; +import { Button, ModalOverlay, Title, Message, ErrorMessage, ErrorBox } from '../styles/designSystem'; const ModalContent = styled.div` background: white; @@ -18,11 +18,6 @@ const ButtonContainer = styled.div` margin-top: 20px; `; -const Message = styled.p` - margin: 0 0 20px 0; - line-height: 1.5; -`; - interface RegenerateModalProps { onClose: () => void; onRegenerate: () => Promise; @@ -32,28 +27,38 @@ const RegenerateModal: React.FC = ({ onClose, onRegenerate const { t } = useTranslation(); const [isRegenerating, setIsRegenerating] = useState(false); const [isComplete, setIsComplete] = useState(false); + const [error, setError] = useState(null); const handleRegenerate = async () => { setIsRegenerating(true); try { + throw new Error("test"); await onRegenerate(); setIsComplete(true); } catch (error) { console.error('Error regenerating video:', error); - // You might want to show an error message here + setError(`${error}`); } }; return ( - - {isComplete - ? t('regenerateRequestSent') - : t('regenerateDescription')} - + {t('regenerate')} + {error ? ( + <> + {t('errorRegenerating')} + {error} + + ) : ( + + {isComplete + ? t('regenerateRequestSent') + : t('regenerateDescription')} + + )} - {!isComplete ? ( + {!isComplete && !error ? ( <>