diff --git a/src/components/DocumentSettings.tsx b/src/components/DocumentSettings.tsx index 9f2d34c..02b6870 100644 --- a/src/components/DocumentSettings.tsx +++ b/src/components/DocumentSettings.tsx @@ -133,6 +133,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html }: { estimatedTimeRemaining={estimatedTimeRemaining || undefined} onCancel={handleCancel} isProcessing={epub ? isAudioCombining : isPDFAudioCombining} + cancelText="Cancel and download" /> diff --git a/src/components/ProgressPopup.tsx b/src/components/ProgressPopup.tsx index aedc4e2..65ded31 100644 --- a/src/components/ProgressPopup.tsx +++ b/src/components/ProgressPopup.tsx @@ -8,9 +8,12 @@ interface ProgressPopupProps { estimatedTimeRemaining?: string; onCancel: () => void; isProcessing: boolean; + statusMessage?: string; + operationType?: 'sync' | 'load'; + cancelText?: string; } -export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCancel, isProcessing }: ProgressPopupProps) { +export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCancel, isProcessing, statusMessage, operationType, cancelText = 'Cancel' }: ProgressPopupProps) { return ( -
+
@@ -33,28 +36,32 @@ export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCanc />
-
- {Math.round(progress)}% complete - {estimatedTimeRemaining &&
- - {` ~${estimatedTimeRemaining}`} -
} +
+ {operationType && ( + + {operationType === 'sync' ? 'Saving to Server' : 'Loading from Server'} + + )} + {statusMessage && {statusMessage}} +
+ {Math.round(progress)}% complete + {estimatedTimeRemaining &&
+ + {` ~${estimatedTimeRemaining}`} +
} +
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index a865752..ab16939 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -26,6 +26,8 @@ import { indexedDBService } from '@/utils/indexedDB'; import { useDocuments } from '@/contexts/DocumentContext'; import { setItem, getItem } from '@/utils/indexedDB'; import { ConfirmDialog } from '@/components/ConfirmDialog'; +import { ProgressPopup } from '@/components/ProgressPopup'; +import { useTimeEstimation } from '@/hooks/useTimeEstimation'; import { THEMES } from '@/contexts/ThemeContext'; const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null; @@ -48,9 +50,14 @@ export function SettingsModal() { const [localTTSInstructions, setLocalTTSInstructions] = useState(ttsInstructions); const [isSyncing, setIsSyncing] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [showProgress, setShowProgress] = useState(false); + const [statusMessage, setStatusMessage] = useState(''); + const [operationType, setOperationType] = useState<'sync' | 'load'>('sync'); + const [abortController, setAbortController] = useState(null); const selectedTheme = themes.find(t => t.id === theme) || themes[0]; const [showClearLocalConfirm, setShowClearLocalConfirm] = useState(false); const [showClearServerConfirm, setShowClearServerConfirm] = useState(false); + const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation(); const ttsModels = useMemo(() => [ { id: 'tts-1', name: 'TTS-1' }, @@ -84,25 +91,69 @@ export function SettingsModal() { }, [apiKey, baseUrl, ttsModel, ttsModels, ttsInstructions, checkFirstVist]); const handleSync = async () => { + const controller = new AbortController(); + setAbortController(controller); + try { setIsSyncing(true); - await indexedDBService.syncToServer(); + setShowProgress(true); + setProgress(0); + setOperationType('sync'); + setStatusMessage('Preparing documents...'); + await indexedDBService.syncToServer((progress, status) => { + if (controller.signal.aborted) return; + setProgress(progress); + if (status) setStatusMessage(status); + }, controller.signal); } catch (error) { - console.error('Sync failed:', error); + if (controller.signal.aborted) { + console.log('Sync operation cancelled'); + setStatusMessage('Operation cancelled'); + } else { + console.error('Sync failed:', error); + setStatusMessage('Sync failed. Please try again.'); + } } finally { setIsSyncing(false); + setShowProgress(false); + setProgress(0); + setStatusMessage(''); + setAbortController(null); } }; const handleLoad = async () => { + const controller = new AbortController(); + setAbortController(controller); + try { setIsLoading(true); - await indexedDBService.loadFromServer(); + setShowProgress(true); + setProgress(0); + setOperationType('load'); + setStatusMessage('Downloading documents from server...'); + await indexedDBService.loadFromServer((progress, status) => { + if (controller.signal.aborted) return; + setProgress(progress); + if (status) setStatusMessage(status); + }, controller.signal); + if (controller.signal.aborted) return; + setStatusMessage('Refreshing document list...'); await Promise.all([refreshPDFs(), refreshEPUBs()]); } catch (error) { - console.error('Load failed:', error); + if (controller.signal.aborted) { + console.log('Load operation cancelled'); + setStatusMessage('Operation cancelled'); + } else { + console.error('Load failed:', error); + setStatusMessage('Load failed. Please try again.'); + } } finally { setIsLoading(false); + setShowProgress(false); + setProgress(0); + setStatusMessage(''); + setAbortController(null); } }; @@ -430,7 +481,7 @@ export function SettingsModal() { transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-accent disabled:opacity-50" > - {isLoading ? 'Loading...' : 'Load docs from Server'} + {isLoading ? `Loading... ${Math.round(progress)}%` : 'Load docs from Server'}
} @@ -498,6 +549,28 @@ export function SettingsModal() { confirmText="Delete" isDangerous={true} /> + + { + if (abortController) { + abortController.abort(); + } + setShowProgress(false); + setProgress(0); + setIsSyncing(false); + setIsLoading(false); + setStatusMessage(''); + setOperationType('sync'); + setAbortController(null); + }} + isProcessing={isSyncing || isLoading} + statusMessage={statusMessage} + operationType={operationType} + cancelText="Cancel" + /> ); } diff --git a/src/utils/indexedDB.ts b/src/utils/indexedDB.ts index 6e3b875..973df19 100644 --- a/src/utils/indexedDB.ts +++ b/src/utils/indexedDB.ts @@ -635,11 +635,13 @@ class IndexedDBService { }); } - async syncToServer(): Promise<{ lastSync: number }> { + async syncToServer(onProgress?: (progress: number, status?: string) => void, signal?: AbortSignal): Promise<{ lastSync: number }> { const pdfDocs = await this.getAllDocuments(); const epubDocs = await this.getAllEPUBDocuments(); const documents = []; + const totalDocs = pdfDocs.length + epubDocs.length; + let processedDocs = 0; // Process PDF documents - convert ArrayBuffer to array for JSON serialization for (const doc of pdfDocs) { @@ -648,6 +650,10 @@ class IndexedDBService { type: 'pdf', data: Array.from(new Uint8Array(doc.data)) }); + processedDocs++; + if (onProgress) { + onProgress((processedDocs / totalDocs) * 50, `Processing ${processedDocs}/${totalDocs} documents...`); + } } // Process EPUB documents @@ -657,31 +663,57 @@ class IndexedDBService { type: 'epub', data: Array.from(new Uint8Array(doc.data)) }); + processedDocs++; + if (onProgress) { + onProgress((processedDocs / totalDocs) * 50, `Processing ${processedDocs}/${totalDocs} documents...`); + } + } + + if (onProgress) { + onProgress(50, 'Uploading to server...'); } const response = await fetch('/api/documents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ documents }) + body: JSON.stringify({ documents }), + signal }); if (!response.ok) { throw new Error('Failed to sync documents to server'); } + if (onProgress) { + onProgress(100, 'Upload complete!'); + } + return { lastSync: Date.now() }; } - async loadFromServer(): Promise<{ lastSync: number }> { - const response = await fetch('/api/documents'); + async loadFromServer(onProgress?: (progress: number, status?: string) => void, signal?: AbortSignal): Promise<{ lastSync: number }> { + if (onProgress) { + onProgress(10, 'Starting download...'); + } + + const response = await fetch('/api/documents', { signal }); if (!response.ok) { throw new Error('Failed to fetch documents from server'); } + if (onProgress) { + onProgress(30, 'Download complete'); + } + const { documents } = await response.json(); + if (onProgress) { + onProgress(40, 'Parsing documents...'); + } + // Process each document - for (const doc of documents) { + for (let i = 0; i < documents.length; i++) { + const doc = documents[i]; // Convert the numeric array back to ArrayBuffer const uint8Array = new Uint8Array(doc.data); const documentData = { @@ -700,6 +732,15 @@ class IndexedDBService { } else { console.warn(`Unknown document type: ${doc.type}`); } + + if (onProgress) { + // Progress from 40% to 90% for document processing + onProgress(40 + ((i + 1) / documents.length) * 50, `Processing document ${i + 1}/${documents.length}...`); + } + } + + if (onProgress) { + onProgress(100, 'Load complete!'); } return { lastSync: Date.now() };