Saving and Loading Progress Bar
This commit is contained in:
parent
593dd7d1a1
commit
32f5cdc494
3 changed files with 142 additions and 19 deletions
|
|
@ -8,9 +8,11 @@ interface ProgressPopupProps {
|
|||
estimatedTimeRemaining?: string;
|
||||
onCancel: () => void;
|
||||
isProcessing: boolean;
|
||||
statusMessage?: string;
|
||||
operationType?: 'sync' | 'load';
|
||||
}
|
||||
|
||||
export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCancel, isProcessing }: ProgressPopupProps) {
|
||||
export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCancel, isProcessing, statusMessage, operationType }: ProgressPopupProps) {
|
||||
return (
|
||||
<Transition
|
||||
show={isOpen}
|
||||
|
|
@ -22,7 +24,7 @@ export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCanc
|
|||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-4"
|
||||
>
|
||||
<div className="fixed inset-x-0 top-2 z-50 pointer-events-none">
|
||||
<div className="fixed inset-x-0 top-2 z-[60] pointer-events-none">
|
||||
<div className="w-full max-w-sm mx-auto">
|
||||
<div className="w-full transform rounded-lg bg-offbase p-4 shadow-xl pointer-events-auto">
|
||||
<div className="space-y-2 truncate">
|
||||
|
|
@ -33,12 +35,20 @@ export function ProgressPopup({ isOpen, progress, estimatedTimeRemaining, onCanc
|
|||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm text-muted text-xs sm:text-sm">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<span>{Math.round(progress)}% complete</span>
|
||||
{estimatedTimeRemaining && <div>
|
||||
<span>•</span>
|
||||
<span>{` ~${estimatedTimeRemaining}`}</span>
|
||||
</div>}
|
||||
<div className="flex flex-col gap-1">
|
||||
{operationType && (
|
||||
<span className="text-accent font-semibold text-xs uppercase tracking-wide">
|
||||
{operationType === 'sync' ? 'Saving to Server' : 'Loading from Server'}
|
||||
</span>
|
||||
)}
|
||||
{statusMessage && <span className="text-foreground font-medium">{statusMessage}</span>}
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<span>{Math.round(progress)}% complete</span>
|
||||
{estimatedTimeRemaining && <div>
|
||||
<span>•</span>
|
||||
<span>{` ~${estimatedTimeRemaining}`}</span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -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<AbortController | null>(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'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSync}
|
||||
|
|
@ -441,7 +492,7 @@ export function SettingsModal() {
|
|||
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-accent
|
||||
disabled:opacity-50"
|
||||
>
|
||||
{isSyncing ? 'Saving...' : 'Save local to Server'}
|
||||
{isSyncing ? `Saving... ${Math.round(progress)}%` : 'Save local to Server'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>}
|
||||
|
|
@ -498,6 +549,27 @@ export function SettingsModal() {
|
|||
confirmText="Delete"
|
||||
isDangerous={true}
|
||||
/>
|
||||
|
||||
<ProgressPopup
|
||||
isOpen={showProgress}
|
||||
progress={progress}
|
||||
estimatedTimeRemaining={estimatedTimeRemaining || undefined}
|
||||
onCancel={() => {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
setShowProgress(false);
|
||||
setProgress(0);
|
||||
setIsSyncing(false);
|
||||
setIsLoading(false);
|
||||
setStatusMessage('');
|
||||
setOperationType('sync');
|
||||
setAbortController(null);
|
||||
}}
|
||||
isProcessing={isSyncing || isLoading}
|
||||
statusMessage={statusMessage}
|
||||
operationType={operationType}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() };
|
||||
|
|
|
|||
Loading…
Reference in a new issue