'use client'; import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Button } from '@headlessui/react'; import { getAppConfig, getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/dexie'; import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-documents'; import { useDocuments } from '@/contexts/DocumentContext'; import type { BaseDocument } from '@/types/documents'; import { cacheStoredDocumentFromBytes } from '@/lib/document-cache'; export function DexieMigrationModal() { const { refreshDocuments } = useDocuments(); const [isOpen, setIsOpen] = useState(false); const [localCount, setLocalCount] = useState(0); const [missingCount, setMissingCount] = useState(0); const [isUploading, setIsUploading] = useState(false); const [progress, setProgress] = useState(0); const [status, setStatus] = useState(''); const checkedRef = useRef(false); const closeDisabled = isUploading; const loadLocalDexieDocs = useCallback(async (): Promise<{ docs: BaseDocument[]; pdfById: Map; epubById: Map; htmlById: Map; }> => { const [pdfs, epubs, htmls] = await Promise.all([getAllPdfDocuments(), getAllEpubDocuments(), getAllHtmlDocuments()]); const docs: BaseDocument[] = [ ...pdfs.map((d) => ({ id: d.id, name: d.name, size: d.size, lastModified: d.lastModified, type: 'pdf' as const })), ...epubs.map((d) => ({ id: d.id, name: d.name, size: d.size, lastModified: d.lastModified, type: 'epub' as const })), ...htmls.map((d) => ({ id: d.id, name: d.name, size: d.size, lastModified: d.lastModified, type: 'html' as const })), ]; const pdfById = new Map(pdfs.map((d) => [d.id, d] as const)); const epubById = new Map(epubs.map((d) => [d.id, d] as const)); const htmlById = new Map(htmls.map((d) => [d.id, d] as const)); return { docs, pdfById, epubById, htmlById }; }, []); const checkAndMaybePrompt = useCallback(async () => { if (checkedRef.current) return; checkedRef.current = true; const cfg = await getAppConfig(); if (!cfg?.privacyAccepted) { // Wait for privacy acceptance before prompting. checkedRef.current = false; return; } if (cfg.documentsMigrationPrompted) return; const { docs } = await loadLocalDexieDocs(); const count = docs.length; setLocalCount(count); if (count === 0) return; const serverDocs = await listDocuments().catch(() => null); if (serverDocs) { const serverIds = new Set(serverDocs.map((d) => d.id)); const missing = docs.filter((d) => !serverIds.has(d.id)); setMissingCount(missing.length); if (missing.length === 0) return; } else { // If the server list fails, still prompt so the user can attempt upload. setMissingCount(count); } setIsOpen(true); }, [loadLocalDexieDocs]); useEffect(() => { checkAndMaybePrompt().catch((err) => { console.error('Dexie migration check failed:', err); }); }, [checkAndMaybePrompt]); useEffect(() => { const handler = () => { checkedRef.current = false; checkAndMaybePrompt().catch((err) => console.error('Dexie migration check failed:', err)); }; window.addEventListener('openreader:privacyAccepted', handler as EventListener); return () => window.removeEventListener('openreader:privacyAccepted', handler as EventListener); }, [checkAndMaybePrompt]); const title = 'Upload your local documents?'; const handleSkip = useCallback(async () => { await updateAppConfig({ documentsMigrationPrompted: true }); setIsOpen(false); }, []); const handleUpload = useCallback(async () => { setIsUploading(true); setProgress(0); setStatus('Preparing upload...'); try { const { docs, pdfById, epubById, htmlById } = await loadLocalDexieDocs(); const serverDocs = await listDocuments().catch(() => null); const serverIds = serverDocs ? new Set(serverDocs.map((d) => d.id)) : null; const toUpload = serverIds ? docs.filter((d) => !serverIds.has(d.id)) : docs; setMissingCount(toUpload.length); const encoder = new TextEncoder(); for (let i = 0; i < toUpload.length; i++) { const doc = toUpload[i]; setStatus(`Uploading ${i + 1}/${toUpload.length}: ${doc.name}`); setProgress((i / Math.max(1, toUpload.length)) * 100); // Pull raw data from Dexie for this doc if (doc.type === 'pdf') { const full = pdfById.get(doc.id) ?? null; if (!full) continue; const bytes = full.data.slice(0); const file = new File([full.data], full.name, { type: mimeTypeForDoc(doc), lastModified: full.lastModified, }); const uploaded = await uploadDocuments([file]); const stored = uploaded[0] ?? null; if (stored) { await cacheStoredDocumentFromBytes(stored, bytes).catch(() => { }); } } else if (doc.type === 'epub') { const full = epubById.get(doc.id) ?? null; if (!full) continue; const bytes = full.data.slice(0); const file = new File([full.data], full.name, { type: mimeTypeForDoc(doc), lastModified: full.lastModified, }); const uploaded = await uploadDocuments([file]); const stored = uploaded[0] ?? null; if (stored) { await cacheStoredDocumentFromBytes(stored, bytes).catch(() => { }); } } else { const full = htmlById.get(doc.id) ?? null; if (!full) continue; const bytes = encoder.encode(full.data).buffer; const file = new File([full.data], full.name, { type: mimeTypeForDoc(doc), lastModified: full.lastModified, }); const uploaded = await uploadDocuments([file]); const stored = uploaded[0] ?? null; if (stored) { await cacheStoredDocumentFromBytes(stored, bytes).catch(() => { }); } } } setProgress(100); setStatus('Refreshing...'); await refreshDocuments(); await updateAppConfig({ documentsMigrationPrompted: true }); setIsOpen(false); } catch (err) { console.error('Dexie migration upload failed:', err); setStatus('Upload failed. You can retry or skip.'); checkedRef.current = false; } finally { setIsUploading(false); } }, [loadLocalDexieDocs, refreshDocuments]); if (!isOpen) return null; return ( (closeDisabled ? null : setIsOpen(false))}>
{title}

Found {localCount} document{localCount === 1 ? '' : 's'} stored locally from an older version. {missingCount > 0 ? ( <> {missingCount} {missingCount === 1 ? 'is' : 'are'} not here yet. ) : null} {' '}This app now stores documents on the server and keeps a local cache for speed.

{isUploading && (

{status}

)} {!isUploading && status ?

{status}

: null}
); }