Saving and Loading Progress Bar

This commit is contained in:
RobbyV2 2025-07-19 20:14:36 -07:00
parent 593dd7d1a1
commit 32f5cdc494
3 changed files with 142 additions and 19 deletions

View file

@ -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>&bull;</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>&bull;</span>
<span>{` ~${estimatedTimeRemaining}`}</span>
</div>}
</div>
</div>
<button
type="button"

View file

@ -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}
/>
</>
);
}

View file

@ -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() };