diff --git a/README.md b/README.md
index cb88c26..dafbdd3 100644
--- a/README.md
+++ b/README.md
@@ -7,28 +7,63 @@
[](../../discussions)
-# OpenReader WebUI 📄🔊
+# 📄🔊 OpenReader WebUI
-OpenReader WebUI is a document reader with Text-to-Speech capabilities, offering a TTS read along experience with narration for EPUB, PDF, TXT, MD, and DOCX documents. It supports multiple TTS providers including OpenAI, Deepinfra, and custom OpenAI-compatible endpoints like [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) and [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI)
+OpenReader WebUI is an open source text to speech document reader web app built using Next.js, offering a TTS read along experience with narration for EPUB, PDF, TXT, MD, and DOCX documents. It supports multiple TTS providers including OpenAI, Deepinfra, and custom OpenAI-compatible endpoints like [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) and [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI)
-- 🎯 **Multi-Provider TTS Support**:
- - **OpenAI**: tts-1, tts-1-hd, gpt-4o-mini-tts models with voices (alloy, echo, fable, onyx, nova, shimmer)
+- 🧠 **(New) Smart Sentence-Aware Narration**: EPUB and PDF playback use shared NLP (compromise) and smart sentence continuation to merge sentences that span pages/chapters for smoother TTS trying to prevent hard cuts at page breaks
+- 🎧 **(New) Reliable Audiobook Export**: Create and export audiobooks from PDF and EPUB files **(in m4b or mp3 format using ffmpeg)** with resumable, chapter/page-based export and per-chapter regeneration
+- 🎯 **(New) Multi-Provider TTS Support**:
- **Deepinfra**: Kokoro-82M, Orpheus-3B, Sesame-1B models with extensive voice libraries
- - **Custom OpenAI-Compatible**: Any OpenAI-compatible endpoint with custom voice sets
-- 💾 **Local-First Architecture**: Uses IndexedDB browser storage for documents
-- 🛜 **Optional Server-side documents**: Manually upload documents to the next backend for all users to download
-- 📖 **Read Along Experience**: Follow along with highlighted text as the TTS narrates
-- 📄 **Document formats**: EPUB, PDF, TXT, MD, DOCX (with libreoffice installed)
-- 🎧 **Audiobook Creation**: Create and export audiobooks from PDF and ePub files **(in m4b format with ffmpeg and aac TTS output)**
+ - **OpenAI API ($$)**: tts-1, tts-1-hd, gpt-4o-mini-tts models
+ - **Kokoro-FastAPI**: Self-hosted OpenAI-compatible TTS API server supporting Kokoro-82M and multi-voice combinations (like `af_heart+bf_emma`)
+ - **Orpheus-FastAPI**: Self-hosted OpenAI-compatible TTS API server supporting Orpheus-3B
+ - And other Custom OpenAI-compatible endpoints with a `/v1/audio/voices` endpoint
+- 🚀 **(New) Optimized TTS Pipeline**: Next.js TTS backend with in-memory LRU audio cache, ETag-aware responses, and in-flight request de-duplication for faster repeat playback
+- 💾 **Local-First Architecture**: IndexedDB browser storage for documents and settings (now using Dexie.js)
+- 🛜 **Optional Server-side documents**: Manually upload documents to the Next.js backend (and Docker `docstore`) for all users to download
+- 📖 **Read Along Experience**: Follow along with highlighted text as the TTS narrates PDF files, with per-sentence navigation and skip controls
+- 📄 **Document formats**: EPUB, PDF, TXT, MD, DOCX (with libreoffice installed, plus hardened DOCX→PDF conversion for better reliability)
- 🎨 **Customizable Experience**:
- 🔑 Select TTS provider (OpenAI, Deepinfra, or Custom OpenAI-compatible)
- 🔐 Set TTS API base URL and optional API key
- 🎨 Multiple app theme options
- And more...
-### 🛠️ Work in progress
-- [ ] **Native .docx support** (currently requires libreoffice)
-- [ ] **Accessibility Improvements**
+
+
+
+### 🆕 What's New in v1.0.0
+
+
+
+- 🧠 **Smart sentence continuation**
+ - EPUB and PDF playback now use smarter sentence splitting and continuation metadata so sentences that cross page/chapter boundaries are merged before hitting the TTS API.
+ - This yields more natural narration and fewer awkward pauses when a sentence spans multiple pages or EPUB spine items
+- 🎧 **Chapter/page-based audiobook export with resume & regeneration**
+ - Per-chapter/per-page generation to disk with persistent `bookId`
+ - Resumable generation (can cancel and continue later)
+ - Per-chapter regeneration & deletion
+ - Final combined **M4B** or **MP3** download with embedded chapter metadata.
+- 💾 **Dexie-backed local storage & sync**
+ - All document types (PDF, EPUB, TXT/MD-as-HTML) and config are stored via a unified Dexie layer on top of IndexedDB.
+ - Document lists use live Dexie queries (no manual refresh needed), and server sync now correctly includes text/markdown documents as part of the library backup.
+- 🗣️ **Kokoro multi-voice selection & utilities**
+ - Kokoro models now support multi-voice combination, with provider-aware limits and helpers (not supported on OpenAI or Deepinfra)
+- ⚡ **Faster, more efficient TTS backend proxy**
+ - In-memory **LRU caching** for audio responses with configurable size/TTL
+ - **ETag** support (`304` on cache hits) + `X-Cache` headers (`HIT` / `MISS` / `INFLIGHT`)
+- 📄 **More robust DOCX → PDF conversion**
+ - DOCX conversion now uses isolated per-job LibreOffice profiles and temp directories, polls for a stable output file size, and aggressively cleans up temp files.
+ - This reduces cross-job interference and flakiness when converting multiple DOCX files in parallel.
+- ♿ **Accessibility & layout improvements**
+ - Dialogs and folder toggles expose proper roles and ARIA attributes.
+ - PDF/EPUB/HTML readers use a full-height app shell with a sticky bottom TTS bar, improved scrollbars, and refined focus styles.
+- ✅ **End-to-end Playwright test suite with TTS mocks**
+ - Deterministic TTS responses in tests via a reusable Playwright route mock.
+ - Coverage for accessibility, upload, navigation, folder management, deletion flows, and playback across all document types.
+
+
## 🐳 Docker Quick Start
@@ -78,12 +113,18 @@ docker pull ghcr.io/richardr1126/openreader-webui:latest
### 🗣️ Local Kokoro-FastAPI Quick-start (CPU or GPU)
-You can run the Kokoro TTS API server directly with Docker. **We are not responsible for issues with Kokoro-FastAPI.** For best performance, use an NVIDIA GPU (for GPU version) or Apple Silicon (for CPU version).
+You can run the Kokoro TTS API server directly with Docker. **We are not responsible for issues with [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI).** For best performance, use an NVIDIA GPU (for GPU version) or Apple Silicon (for CPU version).
> **Note:** When using these, set the `API_BASE` env var to `http://host.docker.internal:8880/v1` or `http://kokoro-tts:8880/v1`.
> You can also use the example `docker-compose.yml` in `examples/docker-compose.yml` if you prefer Docker Compose.
-**CPU Version:**
+
+
+
+**Docker CPU**
+
+
+
```bash
docker run -d \
--name kokoro-tts \
@@ -99,7 +140,15 @@ docker run -d \
ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4
```
-**GPU Version:**
+
+
+
+
+
+**Docker GPU**
+
+
+
```bash
docker run -d \
--name kokoro-tts \
@@ -113,23 +162,31 @@ docker run -d \
ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4
```
+
+
> **Note:**
> - These commands are for running the Kokoro TTS API server only. For issues or support, see the [Kokoro-FastAPI repository](https://github.com/remsky/Kokoro-FastAPI).
> - The GPU version requires NVIDIA Docker support and works best with NVIDIA GPUs. The CPU version works best on Apple Silicon or modern x86 CPUs.
> - Adjust environment variables as needed for your hardware and use case.
-## Dev Installation
+## Local Development Installation
### Prerequisites
-- Node.js & npm or pnpm (recommended: use [nvm](https://github.com/nvm-sh/nvm) for Node.js)
+- Node.js (recommended: use [nvm](https://github.com/nvm-sh/nvm))
+- pnpm (recommended) or npm
+ ```bash
+ npm install -g pnpm
+ ```
+- A TTS API server (Kokoro-FastAPI, Orpheus-FastAPI, Deepinfra, OpenAI, etc.) running and accessible
Optionally required for different features:
- [FFmpeg](https://ffmpeg.org) (required for audiobook m4b creation only)
- - On Linux: `sudo apt install ffmpeg`
- - On MacOS: `brew install ffmpeg`
+ ```bash
+ brew install ffmpeg
+ ```
- [libreoffice](https://www.libreoffice.org) (required for DOCX files)
- - On Linux: `sudo apt install libreoffice`
- - On MacOS: `brew install libreoffice`
-
+ ```bash
+ brew install libreoffice
+ ```
### Steps
1. Clone the repository:
@@ -142,12 +199,7 @@ Optionally required for different features:
With pnpm (recommended):
```bash
- pnpm install
- ```
-
- Or with npm:
- ```bash
- npm install
+ pnpm i # or npm i
```
3. Configure the environment:
@@ -161,26 +213,15 @@ Optionally required for different features:
With pnpm (recommended):
```bash
- pnpm dev
- ```
-
- Or with npm:
- ```bash
- npm run dev
+ pnpm dev # or npm run dev
```
or build and run the production server:
With pnpm:
```bash
- pnpm build
- pnpm start
- ```
-
- Or with npm:
- ```bash
- npm run build
- npm start
+ pnpm build # or npm run build
+ pnpm start # or npm start
```
Visit [http://localhost:3003](http://localhost:3003) to run the app.
@@ -217,7 +258,7 @@ This project would not be possible without standing on the shoulders of these gi
- **Framework:** Next.js (React)
- **Containerization:** Docker
-- **Storage:** IndexedDB (in browser db store)
+- **Storage:** Dexie + IndexedDB (in-browser local database)
- **PDF:**
- [react-pdf](https://github.com/wojtekmaj/react-pdf)
- [pdf.js](https://mozilla.github.io/pdf.js/)
diff --git a/package.json b/package.json
index 026ce35..7fc2a3b 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"cmpstr": "^3.0.4",
"compromise": "^14.14.4",
"core-js": "^3.46.0",
+ "dexie": "^4.2.1",
+ "dexie-react-hooks": "^4.2.0",
"epubjs": "^0.3.93",
"howler": "^2.2.4",
"lru-cache": "^11.2.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1bbecc8..a7a9ec6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,12 @@ importers:
core-js:
specifier: ^3.46.0
version: 3.46.0
+ dexie:
+ specifier: ^4.2.1
+ version: 4.2.1
+ dexie-react-hooks:
+ specifier: ^4.2.0
+ version: 4.2.0(@types/react@19.2.2)(dexie@4.2.1)(react@19.2.0)
epubjs:
specifier: ^0.3.93
version: 0.3.93
@@ -1093,6 +1099,16 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+ dexie-react-hooks@4.2.0:
+ resolution: {integrity: sha512-u7KqTX9JpBQK8+tEyA9X0yMGXlSCsbm5AU64N6gjvGk/IutYDpLBInMYEAEC83s3qhIvryFS+W+sqLZUBEvePQ==}
+ peerDependencies:
+ '@types/react': '>=16'
+ dexie: '>=4.2.0-alpha.1 <5.0.0'
+ react: '>=16'
+
+ dexie@4.2.1:
+ resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==}
+
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -3710,6 +3726,14 @@ snapshots:
dependencies:
dequal: 2.0.3
+ dexie-react-hooks@4.2.0(@types/react@19.2.2)(dexie@4.2.1)(react@19.2.0):
+ dependencies:
+ '@types/react': 19.2.2
+ dexie: 4.2.1
+ react: 19.2.0
+
+ dexie@4.2.1: {}
+
didyoumean@1.2.2: {}
dlv@1.1.3: {}
diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts
index 679b0ea..f926e2f 100644
--- a/src/app/api/documents/route.ts
+++ b/src/app/api/documents/route.ts
@@ -1,6 +1,7 @@
import { writeFile, readFile, readdir, mkdir, unlink } from 'fs/promises';
import { NextRequest, NextResponse } from 'next/server';
import path from 'path';
+import type { BaseDocument, SyncedDocument } from '@/types/documents';
const DOCS_DIR = path.join(process.cwd(), 'docstore');
@@ -17,9 +18,10 @@ export async function POST(req: NextRequest) {
try {
await ensureDocsDir();
const data = await req.json();
+ const documents = data.documents as SyncedDocument[];
// Save document metadata and content
- for (const doc of data.documents) {
+ for (const doc of documents) {
const docPath = path.join(DOCS_DIR, `${doc.id}.json`);
const contentPath = path.join(DOCS_DIR, `${doc.id}.${doc.type}`);
@@ -49,7 +51,7 @@ export async function POST(req: NextRequest) {
export async function GET() {
try {
await ensureDocsDir();
- const documents = [];
+ const documents: SyncedDocument[] = [];
const files = await readdir(DOCS_DIR);
const jsonFiles = files.filter(file => file.endsWith('.json'));
@@ -58,7 +60,7 @@ export async function GET() {
const docPath = path.join(DOCS_DIR, file);
try {
- const metadata = JSON.parse(await readFile(docPath, 'utf8'));
+ const metadata = JSON.parse(await readFile(docPath, 'utf8')) as BaseDocument;
const contentPath = path.join(DOCS_DIR, `${metadata.id}.${metadata.type}`);
const content = await readFile(contentPath);
diff --git a/src/app/api/tts/route.ts b/src/app/api/tts/route.ts
index a6a2ec1..2e0dade 100644
--- a/src/app/api/tts/route.ts
+++ b/src/app/api/tts/route.ts
@@ -23,36 +23,6 @@ const ttsAudioCache = new LRUCache({
ttl: TTS_CACHE_TTL_MS,
});
-// Concurrency controls and in-flight de-duplication
-const TTS_MAX_CONCURRENCY = Number(process.env.TTS_MAX_CONCURRENCY || 4);
-
-class Semaphore {
- private permits: number;
- private queue: Array<() => void> = [];
- constructor(max: number) {
- this.permits = Math.max(1, max);
- }
- async acquire(): Promise<() => void> {
- if (this.permits > 0) {
- this.permits -= 1;
- return this.release.bind(this);
- }
- return new Promise<() => void>((resolve) => {
- this.queue.push(() => {
- this.permits -= 1;
- resolve(this.release.bind(this));
- });
- });
- }
- private release() {
- this.permits += 1;
- const next = this.queue.shift();
- if (next) next();
- }
-}
-
-const ttsSemaphore = new Semaphore(TTS_MAX_CONCURRENCY);
-
type InflightEntry = {
promise: Promise;
controller: AbortController;
@@ -211,7 +181,7 @@ export async function POST(req: NextRequest) {
});
}
- // De-duplicate identical in-flight requests and bound upstream concurrency
+ // De-duplicate identical in-flight requests
const existing = inflightRequests.get(cacheKey);
if (existing) {
console.log('TTS in-flight JOIN for key:', cacheKey.slice(0, 8));
@@ -247,14 +217,12 @@ export async function POST(req: NextRequest) {
controller,
consumers: 1,
promise: (async () => {
- const release = await ttsSemaphore.acquire();
try {
const buffer = await fetchTTSBufferWithRetry(openai, createParams, controller.signal);
// Save to cache
ttsAudioCache.set(cacheKey, buffer);
return buffer;
} finally {
- release();
inflightRequests.delete(cacheKey);
}
})()
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index bc32181..fa1f78f 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -22,9 +22,8 @@ import {
import { useTheme } from '@/contexts/ThemeContext';
import { useConfig } from '@/contexts/ConfigContext';
import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
-import { indexedDBService } from '@/utils/indexedDB';
+import { syncDocumentsToServer, loadDocumentsFromServer, setItem, getItem } from '@/utils/dexie';
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';
@@ -42,7 +41,7 @@ export function SettingsModal() {
const { theme, setTheme } = useTheme();
const { apiKey, baseUrl, ttsProvider, ttsModel, ttsInstructions, updateConfig, updateConfigKey } = useConfig();
- const { refreshPDFs, refreshEPUBs, clearPDFs, clearEPUBs } = useDocuments();
+ const { clearPDFs, clearEPUBs, clearHTML } = useDocuments();
const [localApiKey, setLocalApiKey] = useState(apiKey);
const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl);
const [localTTSProvider, setLocalTTSProvider] = useState(ttsProvider);
@@ -158,7 +157,7 @@ export function SettingsModal() {
setProgress(0);
setOperationType('sync');
setStatusMessage('Preparing documents...');
- await indexedDBService.syncToServer((progress, status) => {
+ await syncDocumentsToServer((progress, status) => {
if (controller.signal.aborted) return;
setProgress(progress);
if (status) setStatusMessage(status);
@@ -190,14 +189,13 @@ export function SettingsModal() {
setProgress(0);
setOperationType('load');
setStatusMessage('Downloading documents from server...');
- await indexedDBService.loadFromServer((progress, status) => {
+ await loadDocumentsFromServer((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()]);
+ setStatusMessage('Documents loaded from server');
} catch (error) {
if (controller.signal.aborted) {
console.log('Load operation cancelled');
@@ -218,6 +216,7 @@ export function SettingsModal() {
const handleClearLocal = async () => {
await clearPDFs();
await clearEPUBs();
+ await clearHTML();
setShowClearLocalConfirm(false);
};
diff --git a/src/components/doclist/DocumentList.tsx b/src/components/doclist/DocumentList.tsx
index c28f433..0878620 100644
--- a/src/components/doclist/DocumentList.tsx
+++ b/src/components/doclist/DocumentList.tsx
@@ -5,7 +5,7 @@ import { useDocuments } from '@/contexts/DocumentContext';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DocumentType, DocumentListDocument, Folder, DocumentListState, SortBy, SortDirection } from '@/types/documents';
-import { getDocumentListState, saveDocumentListState } from '@/utils/indexedDB';
+import { getDocumentListState, saveDocumentListState } from '@/utils/dexie';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { DocumentListItem } from '@/components/doclist/DocumentListItem';
import { DocumentFolder } from '@/components/doclist/DocumentFolder';
diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx
index f99441d..8b6f3c3 100644
--- a/src/contexts/ConfigContext.tsx
+++ b/src/contexts/ConfigContext.tsx
@@ -1,7 +1,7 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
-import { getItem, indexedDBService, setItem, removeItem } from '@/utils/indexedDB';
+import { getItem, setItem, removeItem, initDB } from '@/utils/dexie';
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
@@ -95,7 +95,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
const initializeDB = async () => {
try {
setIsLoading(true);
- await indexedDBService.init();
+ await initDB();
setIsDBReady(true);
// Load config from IndexedDB
diff --git a/src/contexts/DocumentContext.tsx b/src/contexts/DocumentContext.tsx
index adc6b77..597b235 100644
--- a/src/contexts/DocumentContext.tsx
+++ b/src/contexts/DocumentContext.tsx
@@ -25,10 +25,6 @@ interface DocumentContextType {
removeHTMLDocument: (id: string) => Promise;
isHTMLLoading: boolean;
- refreshPDFs: () => Promise;
- refreshEPUBs: () => Promise;
- refreshHTML: () => Promise;
-
clearPDFs: () => Promise;
clearEPUBs: () => Promise;
clearHTML: () => Promise;
@@ -42,7 +38,6 @@ export function DocumentProvider({ children }: { children: ReactNode }) {
addDocument: addPDFDocument,
removeDocument: removePDFDocument,
isLoading: isPDFLoading,
- refresh: refreshPDFs,
clearDocuments: clearPDFs
} = usePDFDocuments();
@@ -51,7 +46,6 @@ export function DocumentProvider({ children }: { children: ReactNode }) {
addDocument: addEPUBDocument,
removeDocument: removeEPUBDocument,
isLoading: isEPUBLoading,
- refresh: refreshEPUBs,
clearDocuments: clearEPUBs
} = useEPUBDocuments();
@@ -60,7 +54,6 @@ export function DocumentProvider({ children }: { children: ReactNode }) {
addDocument: addHTMLDocument,
removeDocument: removeHTMLDocument,
isLoading: isHTMLLoading,
- refresh: refreshHTML,
clearDocuments: clearHTML
} = useHTMLDocuments();
@@ -78,9 +71,6 @@ export function DocumentProvider({ children }: { children: ReactNode }) {
addHTMLDocument,
removeHTMLDocument,
isHTMLLoading,
- refreshPDFs,
- refreshEPUBs,
- refreshHTML,
clearPDFs,
clearEPUBs,
clearHTML
diff --git a/src/contexts/EPUBContext.tsx b/src/contexts/EPUBContext.tsx
index ebdd451..25d849f 100644
--- a/src/contexts/EPUBContext.tsx
+++ b/src/contexts/EPUBContext.tsx
@@ -10,12 +10,11 @@ import {
useRef,
RefObject
} from 'react';
-import { indexedDBService } from '@/utils/indexedDB';
+import { getEpubDocument, setLastDocumentLocation } from '@/utils/dexie';
import { useTTS } from '@/contexts/TTSContext';
import { Book, Rendition } from 'epubjs';
import { createRangeCfi } from '@/utils/epub';
import type { NavItem } from 'epubjs';
-import { setLastDocumentLocation } from '@/utils/indexedDB';
import { SpineItem } from 'epubjs/types/section';
import { useParams } from 'next/navigation';
import { useConfig } from './ConfigContext';
@@ -176,7 +175,7 @@ export function EPUBProvider({ children }: { children: ReactNode }) {
*/
const setCurrentDocument = useCallback(async (id: string): Promise => {
try {
- const doc = await indexedDBService.getEPUBDocument(id);
+ const doc = await getEpubDocument(id);
if (doc) {
console.log('Retrieved document size:', doc.size);
console.log('Retrieved ArrayBuffer size:', doc.data.byteLength);
diff --git a/src/contexts/HTMLContext.tsx b/src/contexts/HTMLContext.tsx
index e2752bd..81d83d6 100644
--- a/src/contexts/HTMLContext.tsx
+++ b/src/contexts/HTMLContext.tsx
@@ -8,7 +8,7 @@ import {
useCallback,
useMemo,
} from 'react';
-import { indexedDBService } from '@/utils/indexedDB';
+import { getHtmlDocument } from '@/utils/dexie';
import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import { combineAudioChunks, withRetry } from '@/utils/audio';
@@ -58,7 +58,7 @@ export function HTMLProvider({ children }: { children: ReactNode }) {
*/
const setCurrentDocument = useCallback(async (id: string): Promise => {
try {
- const doc = await indexedDBService.getHTMLDocument(id);
+ const doc = await getHtmlDocument(id);
if (doc) {
setCurrDocName(doc.name);
setCurrDocData(doc.data);
diff --git a/src/contexts/PDFContext.tsx b/src/contexts/PDFContext.tsx
index 4128816..b54bd43 100644
--- a/src/contexts/PDFContext.tsx
+++ b/src/contexts/PDFContext.tsx
@@ -25,7 +25,7 @@ import {
useRef,
} from 'react';
-import { indexedDBService } from '@/utils/indexedDB';
+import { getPdfDocument } from '@/utils/dexie';
import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import {
@@ -215,7 +215,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
*/
const setCurrentDocument = useCallback(async (id: string): Promise => {
try {
- const doc = await indexedDBService.getDocument(id);
+ const doc = await getPdfDocument(id);
if (doc) {
setCurrDocName(doc.name);
setCurrDocData(doc.data);
diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx
index 8644d88..477d81f 100644
--- a/src/contexts/TTSContext.tsx
+++ b/src/contexts/TTSContext.tsx
@@ -34,7 +34,7 @@ import { useAudioCache } from '@/hooks/audio/useAudioCache';
import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
import { useMediaSession } from '@/hooks/audio/useMediaSession';
import { useAudioContext } from '@/hooks/audio/useAudioContext';
-import { getLastDocumentLocation, setLastDocumentLocation } from '@/utils/indexedDB';
+import { getLastDocumentLocation, setLastDocumentLocation } from '@/utils/dexie';
import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
import { withRetry } from '@/utils/audio';
import { preprocessSentenceForAudio, processTextToSentences } from '@/utils/nlp';
diff --git a/src/hooks/epub/useEPUBDocuments.ts b/src/hooks/epub/useEPUBDocuments.ts
index c0cc993..1098383 100644
--- a/src/hooks/epub/useEPUBDocuments.ts
+++ b/src/hooks/epub/useEPUBDocuments.ts
@@ -1,40 +1,27 @@
'use client';
-import { useState, useCallback, useEffect } from 'react';
+import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { indexedDBService } from '@/utils/indexedDB';
+import { useLiveQuery } from 'dexie-react-hooks';
+import { db } from '@/utils/dexie';
import type { EPUBDocument } from '@/types/documents';
-import { useConfig } from '@/contexts/ConfigContext';
export function useEPUBDocuments() {
- const { isDBReady } = useConfig();
- const [documents, setDocuments] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
+ const documents = useLiveQuery(
+ () => db['epub-documents'].toArray(),
+ [],
+ undefined,
+ );
- const loadDocuments = useCallback(async () => {
- if (isDBReady) {
- try {
- const docs = await indexedDBService.getAllEPUBDocuments();
- setDocuments(docs);
- } catch (error) {
- console.error('Failed to load EPUB documents:', error);
- } finally {
- setIsLoading(false);
- }
- }
- }, [isDBReady]);
-
- useEffect(() => {
- loadDocuments();
- }, [loadDocuments]);
+ const isLoading = documents === undefined;
const addDocument = useCallback(async (file: File): Promise => {
const id = uuidv4();
const arrayBuffer = await file.arrayBuffer();
-
+
console.log('Original file size:', file.size);
console.log('ArrayBuffer size:', arrayBuffer.byteLength);
-
+
const newDoc: EPUBDocument = {
id,
type: 'epub',
@@ -44,56 +31,23 @@ export function useEPUBDocuments() {
data: arrayBuffer,
};
- try {
- await indexedDBService.addEPUBDocument(newDoc);
- setDocuments((prev) => [...prev, newDoc]);
- return id;
- } catch (error) {
- console.error('Failed to add EPUB document:', error);
- throw error;
- }
+ await db['epub-documents'].add(newDoc);
+ return id;
}, []);
const removeDocument = useCallback(async (id: string): Promise => {
- try {
- await indexedDBService.removeEPUBDocument(id);
- setDocuments((prev) => prev.filter((doc) => doc.id !== id));
- } catch (error) {
- console.error('Failed to remove EPUB document:', error);
- throw error;
- }
+ await db['epub-documents'].delete(id);
}, []);
- const refresh = useCallback(async () => {
- if (isDBReady) {
- setIsLoading(true);
- try {
- const docs = await indexedDBService.getAllEPUBDocuments();
- setDocuments(docs);
- } catch (error) {
- console.error('Failed to refresh documents:', error);
- } finally {
- setIsLoading(false);
- }
- }
- }, [isDBReady]);
-
const clearDocuments = useCallback(async (): Promise => {
- try {
- await indexedDBService.clearEPUBDocuments();
- setDocuments([]);
- } catch (error) {
- console.error('Failed to clear EPUB documents:', error);
- throw error;
- }
+ await db['epub-documents'].clear();
}, []);
return {
- documents,
+ documents: documents ?? [],
isLoading,
addDocument,
removeDocument,
- refresh,
- clearDocuments, // Add clearDocuments to return value
+ clearDocuments,
};
}
diff --git a/src/hooks/html/useHTMLDocuments.ts b/src/hooks/html/useHTMLDocuments.ts
index 11b8ed5..a51ef9c 100644
--- a/src/hooks/html/useHTMLDocuments.ts
+++ b/src/hooks/html/useHTMLDocuments.ts
@@ -1,32 +1,19 @@
'use client';
-import { useState, useCallback, useEffect } from 'react';
+import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { indexedDBService } from '@/utils/indexedDB';
+import { useLiveQuery } from 'dexie-react-hooks';
+import { db } from '@/utils/dexie';
import type { HTMLDocument } from '@/types/documents';
-import { useConfig } from '@/contexts/ConfigContext';
export function useHTMLDocuments() {
- const { isDBReady } = useConfig();
- const [documents, setDocuments] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
+ const documents = useLiveQuery(
+ () => db['html-documents'].toArray(),
+ [],
+ undefined,
+ );
- const loadDocuments = useCallback(async () => {
- if (isDBReady) {
- try {
- const docs = await indexedDBService.getAllHTMLDocuments();
- setDocuments(docs);
- } catch (error) {
- console.error('Failed to load HTML documents:', error);
- } finally {
- setIsLoading(false);
- }
- }
- }, [isDBReady]);
-
- useEffect(() => {
- loadDocuments();
- }, [loadDocuments]);
+ const isLoading = documents === undefined;
const addDocument = useCallback(async (file: File): Promise => {
const id = uuidv4();
@@ -41,56 +28,23 @@ export function useHTMLDocuments() {
data: content,
};
- try {
- await indexedDBService.addHTMLDocument(newDoc);
- setDocuments((prev) => [...prev, newDoc]);
- return id;
- } catch (error) {
- console.error('Failed to add HTML document:', error);
- throw error;
- }
+ await db['html-documents'].add(newDoc);
+ return id;
}, []);
const removeDocument = useCallback(async (id: string): Promise => {
- try {
- await indexedDBService.removeHTMLDocument(id);
- setDocuments((prev) => prev.filter((doc) => doc.id !== id));
- } catch (error) {
- console.error('Failed to remove HTML document:', error);
- throw error;
- }
+ await db['html-documents'].delete(id);
}, []);
- const refresh = useCallback(async () => {
- if (isDBReady) {
- setIsLoading(true);
- try {
- const docs = await indexedDBService.getAllHTMLDocuments();
- setDocuments(docs);
- } catch (error) {
- console.error('Failed to refresh documents:', error);
- } finally {
- setIsLoading(false);
- }
- }
- }, [isDBReady]);
-
const clearDocuments = useCallback(async (): Promise => {
- try {
- await indexedDBService.clearHTMLDocuments();
- setDocuments([]);
- } catch (error) {
- console.error('Failed to clear HTML documents:', error);
- throw error;
- }
+ await db['html-documents'].clear();
}, []);
return {
- documents,
+ documents: documents ?? [],
isLoading,
addDocument,
removeDocument,
- refresh,
clearDocuments,
};
}
diff --git a/src/hooks/pdf/usePDFDocuments.ts b/src/hooks/pdf/usePDFDocuments.ts
index e2fa4ea..57bb6c5 100644
--- a/src/hooks/pdf/usePDFDocuments.ts
+++ b/src/hooks/pdf/usePDFDocuments.ts
@@ -1,46 +1,24 @@
'use client';
-import { useState, useCallback, useEffect } from 'react';
+import { useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { indexedDBService } from '@/utils/indexedDB';
+import { useLiveQuery } from 'dexie-react-hooks';
+import { db } from '@/utils/dexie';
import type { PDFDocument } from '@/types/documents';
-import { useConfig } from '@/contexts/ConfigContext';
export function usePDFDocuments() {
- const { isDBReady } = useConfig();
- const [documents, setDocuments] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
+ const documents = useLiveQuery(
+ () => db['pdf-documents'].toArray(),
+ [],
+ undefined,
+ );
- /**
- * Load documents from IndexedDB when the database is ready
- */
- const loadDocuments = useCallback(async () => {
- if (isDBReady) {
- try {
- const docs = await indexedDBService.getAllDocuments();
- setDocuments(docs);
- } catch (error) {
- console.error('Failed to load documents:', error);
- } finally {
- setIsLoading(false);
- }
- }
- }, [isDBReady]);
+ const isLoading = documents === undefined;
- useEffect(() => {
- loadDocuments();
- }, [loadDocuments]);
-
- /**
- * Adds a new document to IndexedDB
- *
- * @param {File} file - The PDF file to add
- * @returns {Promise} The ID of the added document
- */
const addDocument = useCallback(async (file: File): Promise => {
const id = uuidv4();
const arrayBuffer = await file.arrayBuffer();
-
+
const newDoc: PDFDocument = {
id,
type: 'pdf',
@@ -50,61 +28,23 @@ export function usePDFDocuments() {
data: arrayBuffer,
};
- try {
- await indexedDBService.addDocument(newDoc);
- setDocuments((prev) => [...prev, newDoc]);
- return id;
- } catch (error) {
- console.error('Failed to add document:', error);
- throw error;
- }
+ await db['pdf-documents'].add(newDoc);
+ return id;
}, []);
- /**
- * Removes a document from IndexedDB
- *
- * @param {string} id - The ID of the document to remove
- */
const removeDocument = useCallback(async (id: string): Promise => {
- try {
- await indexedDBService.removeDocument(id);
- setDocuments((prev) => prev.filter((doc) => doc.id !== id));
- } catch (error) {
- console.error('Failed to remove document:', error);
- throw error;
- }
+ await db['pdf-documents'].delete(id);
}, []);
- const refresh = useCallback(async () => {
- if (isDBReady) {
- setIsLoading(true);
- try {
- const docs = await indexedDBService.getAllDocuments();
- setDocuments(docs);
- } catch (error) {
- console.error('Failed to refresh documents:', error);
- } finally {
- setIsLoading(false);
- }
- }
- }, [isDBReady]);
-
const clearDocuments = useCallback(async (): Promise => {
- try {
- await indexedDBService.clearPDFDocuments();
- setDocuments([]);
- } catch (error) {
- console.error('Failed to clear PDF documents:', error);
- throw error;
- }
+ await db['pdf-documents'].clear();
}, []);
return {
- documents,
+ documents: documents ?? [],
isLoading,
addDocument,
removeDocument,
- refresh,
- clearDocuments, // Add clearDocuments to return value
+ clearDocuments,
};
}
diff --git a/src/types/documents.ts b/src/types/documents.ts
index 397e1c2..d21b1af 100644
--- a/src/types/documents.ts
+++ b/src/types/documents.ts
@@ -30,6 +30,20 @@ export interface DOCXDocument extends BaseDocument {
data: ArrayBuffer;
}
+export type AnyDocument =
+ | PDFDocument
+ | EPUBDocument
+ | HTMLDocument
+ | DOCXDocument;
+
+export type BinaryDocument = PDFDocument | EPUBDocument | DOCXDocument;
+
+// Representation used when syncing binary documents to/from the server.
+// Data is converted from ArrayBuffer to a numeric array for JSON transport.
+export interface SyncedDocument extends BaseDocument {
+ data: number[];
+}
+
export interface DocumentListDocument extends BaseDocument {
type: DocumentType;
}
diff --git a/src/utils/dexie.ts b/src/utils/dexie.ts
new file mode 100644
index 0000000..ab97169
--- /dev/null
+++ b/src/utils/dexie.ts
@@ -0,0 +1,413 @@
+import Dexie, { type EntityTable } from 'dexie';
+import {
+ PDFDocument,
+ EPUBDocument,
+ HTMLDocument,
+ DocumentListState,
+ SyncedDocument,
+} from '@/types/documents';
+
+const DB_NAME = 'openreader-db';
+// Managed via Dexie (version bumped from the original manual IndexedDB)
+const DB_VERSION = 4;
+
+const PDF_TABLE = 'pdf-documents' as const;
+const EPUB_TABLE = 'epub-documents' as const;
+const HTML_TABLE = 'html-documents' as const;
+const CONFIG_TABLE = 'config' as const;
+
+export interface ConfigRow {
+ key: string;
+ value: string;
+}
+
+type OpenReaderDB = Dexie & {
+ [PDF_TABLE]: EntityTable;
+ [EPUB_TABLE]: EntityTable;
+ [HTML_TABLE]: EntityTable;
+ [CONFIG_TABLE]: EntityTable;
+};
+
+export const db = new Dexie(DB_NAME) as OpenReaderDB;
+
+db.version(DB_VERSION).stores({
+ [PDF_TABLE]: 'id, type, name, lastModified, size, folderId',
+ [EPUB_TABLE]: 'id, type, name, lastModified, size, folderId',
+ [HTML_TABLE]: 'id, type, name, lastModified, size, folderId',
+ [CONFIG_TABLE]: 'key',
+});
+
+let dbOpenPromise: Promise | null = null;
+
+export async function initDB(): Promise {
+ if (dbOpenPromise) {
+ return dbOpenPromise;
+ }
+
+ dbOpenPromise = (async () => {
+ try {
+ console.log('Opening Dexie database...');
+ await db.open();
+ console.log('Dexie database opened successfully');
+ } catch (error) {
+ console.error('Dexie initialization error:', error);
+ dbOpenPromise = null;
+ throw error;
+ }
+ })();
+
+ return dbOpenPromise;
+}
+
+async function withDB(operation: () => Promise): Promise {
+ await initDB();
+ return operation();
+}
+
+// PDF document helpers
+
+export async function addPdfDocument(document: PDFDocument): Promise {
+ await withDB(async () => {
+ console.log('Adding PDF document via Dexie:', document.name);
+ await db[PDF_TABLE].put(document);
+ });
+}
+
+export async function getPdfDocument(id: string): Promise {
+ return withDB(async () => {
+ console.log('Fetching PDF document via Dexie:', id);
+ return db[PDF_TABLE].get(id);
+ });
+}
+
+export async function getAllPdfDocuments(): Promise {
+ return withDB(async () => {
+ console.log('Fetching all PDF documents via Dexie');
+ return db[PDF_TABLE].toArray();
+ });
+}
+
+export async function removePdfDocument(id: string): Promise {
+ await withDB(async () => {
+ console.log('Removing PDF document via Dexie:', id);
+ const locationKey = `lastLocation_${id}`;
+ await db.transaction('readwrite', db[PDF_TABLE], db[CONFIG_TABLE], async () => {
+ await db[PDF_TABLE].delete(id);
+ await db[CONFIG_TABLE].delete(locationKey);
+ });
+ });
+}
+
+export async function clearPdfDocuments(): Promise {
+ await withDB(async () => {
+ console.log('Clearing all PDF documents via Dexie');
+ await db[PDF_TABLE].clear();
+ });
+}
+
+// EPUB document helpers
+
+export async function addEpubDocument(document: EPUBDocument): Promise {
+ await withDB(async () => {
+ if (document.data.byteLength === 0) {
+ throw new Error('Cannot store empty ArrayBuffer');
+ }
+
+ console.log('Adding EPUB document via Dexie:', {
+ name: document.name,
+ size: document.size,
+ actualSize: document.data.byteLength,
+ });
+
+ await db[EPUB_TABLE].put(document);
+ });
+}
+
+export async function getEpubDocument(id: string): Promise {
+ return withDB(async () => {
+ console.log('Fetching EPUB document via Dexie:', id);
+ return db[EPUB_TABLE].get(id);
+ });
+}
+
+export async function getAllEpubDocuments(): Promise {
+ return withDB(async () => {
+ console.log('Fetching all EPUB documents via Dexie');
+ return db[EPUB_TABLE].toArray();
+ });
+}
+
+export async function removeEpubDocument(id: string): Promise {
+ await withDB(async () => {
+ console.log('Removing EPUB document via Dexie:', id);
+ const locationKey = `lastLocation_${id}`;
+ await db.transaction('readwrite', db[EPUB_TABLE], db[CONFIG_TABLE], async () => {
+ await db[EPUB_TABLE].delete(id);
+ await db[CONFIG_TABLE].delete(locationKey);
+ });
+ });
+}
+
+export async function clearEpubDocuments(): Promise {
+ await withDB(async () => {
+ console.log('Clearing all EPUB documents via Dexie');
+ await db[EPUB_TABLE].clear();
+ });
+}
+
+// HTML / text document helpers
+
+export async function addHtmlDocument(document: HTMLDocument): Promise {
+ await withDB(async () => {
+ console.log('Adding HTML document via Dexie:', document.name);
+ await db[HTML_TABLE].put(document);
+ });
+}
+
+export async function getHtmlDocument(id: string): Promise {
+ return withDB(async () => {
+ console.log('Fetching HTML document via Dexie:', id);
+ return db[HTML_TABLE].get(id);
+ });
+}
+
+export async function getAllHtmlDocuments(): Promise {
+ return withDB(async () => {
+ console.log('Fetching all HTML documents via Dexie');
+ return db[HTML_TABLE].toArray();
+ });
+}
+
+export async function removeHtmlDocument(id: string): Promise {
+ await withDB(async () => {
+ console.log('Removing HTML document via Dexie:', id);
+ await db[HTML_TABLE].delete(id);
+ });
+}
+
+export async function clearHtmlDocuments(): Promise {
+ await withDB(async () => {
+ console.log('Clearing all HTML documents via Dexie');
+ await db[HTML_TABLE].clear();
+ });
+}
+
+// Config helpers
+
+export async function setConfigItem(key: string, value: string): Promise {
+ await withDB(async () => {
+ console.log('Setting config item via Dexie:', key);
+ await db[CONFIG_TABLE].put({ key, value });
+ });
+}
+
+export async function getConfigItem(key: string): Promise {
+ const row = await withDB(() => db[CONFIG_TABLE].get(key));
+ console.log('Fetching config item via Dexie:', key, row ? 'found' : 'not found');
+ return row ? row.value : null;
+}
+
+export async function getAllConfigItems(): Promise> {
+ const rows = await withDB(() => db[CONFIG_TABLE].toArray());
+ const config: Record = {};
+ rows.forEach((row) => {
+ config[row.key] = row.value;
+ });
+ console.log('Fetched config items via Dexie:', rows.length);
+ return config;
+}
+
+export async function removeConfigItem(key: string): Promise {
+ await withDB(async () => {
+ console.log('Removing config item via Dexie:', key);
+ await db[CONFIG_TABLE].delete(key);
+ });
+}
+
+// Legacy-style config accessors retained for convenience
+
+export const getItem = getConfigItem;
+export const setItem = setConfigItem;
+export const removeItem = removeConfigItem;
+
+// Document list state helpers
+
+export async function saveDocumentListState(state: DocumentListState): Promise {
+ await setConfigItem('documentListState', JSON.stringify(state));
+}
+
+export async function getDocumentListState(): Promise {
+ const stateStr = await getConfigItem('documentListState');
+ if (!stateStr) return null;
+ try {
+ return JSON.parse(stateStr);
+ } catch (error) {
+ console.error('Error parsing document list state:', error);
+ return null;
+ }
+}
+
+// Last-location helpers (used by TTS and readers)
+
+export async function getLastDocumentLocation(docId: string): Promise {
+ const key = `lastLocation_${docId}`;
+ return getConfigItem(key);
+}
+
+export async function setLastDocumentLocation(docId: string, location: string): Promise {
+ const key = `lastLocation_${docId}`;
+ await setConfigItem(key, location);
+}
+
+// Sync helpers (server round-trip)
+
+export async function syncDocumentsToServer(
+ onProgress?: (progress: number, status?: string) => void,
+ signal?: AbortSignal,
+): Promise<{ lastSync: number }> {
+ const pdfDocs = await getAllPdfDocuments();
+ const epubDocs = await getAllEpubDocuments();
+ const htmlDocs = await getAllHtmlDocuments();
+
+ const documents: SyncedDocument[] = [];
+ const totalDocs = pdfDocs.length + epubDocs.length + htmlDocs.length;
+ let processedDocs = 0;
+
+ for (const doc of pdfDocs) {
+ documents.push({
+ ...doc,
+ type: 'pdf',
+ data: Array.from(new Uint8Array(doc.data)),
+ });
+ processedDocs++;
+ if (onProgress) {
+ onProgress((processedDocs / totalDocs) * 50, `Processing ${processedDocs}/${totalDocs} documents...`);
+ }
+ }
+
+ for (const doc of epubDocs) {
+ documents.push({
+ ...doc,
+ type: 'epub',
+ data: Array.from(new Uint8Array(doc.data)),
+ });
+ processedDocs++;
+ if (onProgress) {
+ onProgress((processedDocs / totalDocs) * 50, `Processing ${processedDocs}/${totalDocs} documents...`);
+ }
+ }
+
+ const encoder = new TextEncoder();
+ for (const doc of htmlDocs) {
+ const encoded = encoder.encode(doc.data);
+ documents.push({
+ ...doc,
+ type: 'html',
+ data: Array.from(encoded),
+ });
+ 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 }),
+ signal,
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to sync documents to server');
+ }
+
+ if (onProgress) {
+ onProgress(100, 'Upload complete!');
+ }
+
+ return { lastSync: Date.now() };
+}
+
+export async function loadDocumentsFromServer(
+ 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()) as { documents: SyncedDocument[] };
+
+ if (onProgress) {
+ onProgress(40, 'Parsing documents...');
+ }
+
+ const textDecoder = new TextDecoder();
+
+ for (let i = 0; i < documents.length; i++) {
+ const doc = documents[i];
+
+ if (doc.type === 'pdf') {
+ const uint8Array = new Uint8Array(doc.data);
+ const documentData: PDFDocument = {
+ id: doc.id,
+ type: 'pdf',
+ name: doc.name,
+ size: doc.size,
+ lastModified: doc.lastModified,
+ data: uint8Array.buffer,
+ };
+ await addPdfDocument(documentData);
+ } else if (doc.type === 'epub') {
+ const uint8Array = new Uint8Array(doc.data);
+ const documentData: EPUBDocument = {
+ id: doc.id,
+ type: 'epub',
+ name: doc.name,
+ size: doc.size,
+ lastModified: doc.lastModified,
+ data: uint8Array.buffer,
+ };
+ await addEpubDocument(documentData);
+ } else if (doc.type === 'html') {
+ const uint8Array = new Uint8Array(doc.data);
+ const decoded = textDecoder.decode(uint8Array);
+ const documentData: HTMLDocument = {
+ id: doc.id,
+ type: 'html',
+ name: doc.name,
+ size: doc.size,
+ lastModified: doc.lastModified,
+ data: decoded,
+ };
+ await addHtmlDocument(documentData);
+ } else {
+ console.warn(`Unknown document type: ${doc.type}`);
+ }
+
+ if (onProgress) {
+ onProgress(40 + ((i + 1) / documents.length) * 50, `Processing document ${i + 1}/${documents.length}...`);
+ }
+ }
+
+ if (onProgress) {
+ onProgress(100, 'Load complete!');
+ }
+
+ return { lastSync: Date.now() };
+}
+
diff --git a/src/utils/indexedDB.ts b/src/utils/indexedDB.ts
deleted file mode 100644
index 973df19..0000000
--- a/src/utils/indexedDB.ts
+++ /dev/null
@@ -1,857 +0,0 @@
-import { PDFDocument, EPUBDocument, HTMLDocument, DocumentListState } from '@/types/documents';
-
-const DB_NAME = 'openreader-db';
-const DB_VERSION = 3; // Increased version for new store
-const PDF_STORE_NAME = 'pdf-documents';
-const EPUB_STORE_NAME = 'epub-documents';
-const HTML_STORE_NAME = 'html-documents';
-const CONFIG_STORE_NAME = 'config';
-
-export interface Config {
- key: string;
- value: string;
-}
-
-class IndexedDBService {
- private db: IDBDatabase | null = null;
- private initPromise: Promise | null = null;
-
- async init(): Promise {
- if (this.initPromise) {
- return this.initPromise;
- }
-
- if (this.db) {
- return Promise.resolve();
- }
-
- this.initPromise = new Promise((resolve, reject) => {
- console.log('Initializing IndexedDB...');
- const request = indexedDB.open(DB_NAME, DB_VERSION);
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('IndexedDB initialization error:', error);
- reject(error);
- };
-
- request.onsuccess = (event) => {
- console.log('IndexedDB initialized successfully');
- this.db = (event.target as IDBOpenDBRequest).result;
- resolve();
- };
-
- request.onupgradeneeded = (event) => {
- console.log('Upgrading IndexedDB schema...');
- const db = (event.target as IDBOpenDBRequest).result;
-
- if (!db.objectStoreNames.contains(PDF_STORE_NAME)) {
- console.log('Creating PDF documents store...');
- db.createObjectStore(PDF_STORE_NAME, { keyPath: 'id' });
- }
-
- if (!db.objectStoreNames.contains(HTML_STORE_NAME)) {
- console.log('Creating HTML documents store...');
- db.createObjectStore(HTML_STORE_NAME, { keyPath: 'id' });
- }
-
- if (!db.objectStoreNames.contains(EPUB_STORE_NAME)) {
- console.log('Creating EPUB documents store...');
- db.createObjectStore(EPUB_STORE_NAME, { keyPath: 'id' });
- }
-
- if (!db.objectStoreNames.contains(CONFIG_STORE_NAME)) {
- console.log('Creating config store...');
- db.createObjectStore(CONFIG_STORE_NAME, { keyPath: 'key' });
- }
- };
- });
-
- return this.initPromise;
- }
-
- // PDF Document Methods
- async addDocument(document: PDFDocument): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Adding document to IndexedDB:', document.name);
- const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(PDF_STORE_NAME);
-
- // Create a structured clone of the document to ensure proper storage
- const request = store.put({
- ...document,
- data: document.data // Store ArrayBuffer directly
- });
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error adding document:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('Document added successfully:', document.name);
- resolve();
- };
- } catch (error) {
- console.error('Error in addDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- async getDocument(id: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Fetching document:', id);
- const transaction = this.db!.transaction([PDF_STORE_NAME], 'readonly');
- const store = transaction.objectStore(PDF_STORE_NAME);
- const request = store.get(id);
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error fetching document:', error);
- reject(error);
- };
-
- request.onsuccess = () => {
- console.log('Document fetch result:', request.result ? 'found' : 'not found');
- resolve(request.result);
- };
- } catch (error) {
- console.error('Error in getDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- async getAllDocuments(): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Fetching all documents');
- const transaction = this.db!.transaction([PDF_STORE_NAME], 'readonly');
- const store = transaction.objectStore(PDF_STORE_NAME);
- const request = store.getAll();
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error fetching all documents:', error);
- reject(error);
- };
-
- request.onsuccess = () => {
- console.log('Retrieved documents count:', request.result?.length || 0);
- resolve(request.result || []);
- };
- } catch (error) {
- console.error('Error in getAllDocuments transaction:', error);
- reject(error);
- }
- });
- }
-
- async removeDocument(id: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Removing document:', id);
- const locationKey = `lastLocation_${id}`;
-
- // Create two transactions - one for document deletion, one for location cleanup
- const docTransaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
- const configTransaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
-
- const docStore = docTransaction.objectStore(PDF_STORE_NAME);
- const configStore = configTransaction.objectStore(CONFIG_STORE_NAME);
-
- // Remove the document
- const docRequest = docStore.delete(id);
- // Remove any saved location
- const locationRequest = configStore.delete(locationKey);
-
- docRequest.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error removing document:', error);
- reject(error);
- };
-
- locationRequest.onerror = (event) => {
- console.warn('Error cleaning up location:', (event.target as IDBRequest).error);
- // Don't reject here, as document deletion is the primary operation
- };
-
- // Wait for both transactions to complete
- Promise.all([
- new Promise((res) => { docTransaction.oncomplete = () => res(); }),
- new Promise((res) => { configTransaction.oncomplete = () => res(); })
- ]).then(() => {
- console.log('Document and location data removed successfully:', id);
- resolve();
- }).catch(reject);
-
- } catch (error) {
- console.error('Error in removeDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- // Add HTML Document Methods
- async addHTMLDocument(document: HTMLDocument): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Adding HTML document to IndexedDB:', document.name);
- const transaction = this.db!.transaction([HTML_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(HTML_STORE_NAME);
-
- const request = store.put({
- ...document,
- data: document.data
- });
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error adding HTML document:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('HTML document added successfully:', document.name);
- resolve();
- };
- } catch (error) {
- console.error('Error in addHTMLDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- async getHTMLDocument(id: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Fetching HTML document:', id);
- const transaction = this.db!.transaction([HTML_STORE_NAME], 'readonly');
- const store = transaction.objectStore(HTML_STORE_NAME);
- const request = store.get(id);
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error fetching HTML document:', error);
- reject(error);
- };
-
- request.onsuccess = () => {
- console.log('HTML Document fetch result:', request.result ? 'found' : 'not found');
- resolve(request.result);
- };
- } catch (error) {
- console.error('Error in getHTMLDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- async getAllHTMLDocuments(): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Fetching all HTML documents');
- const transaction = this.db!.transaction([HTML_STORE_NAME], 'readonly');
- const store = transaction.objectStore(HTML_STORE_NAME);
- const request = store.getAll();
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error fetching all HTML documents:', error);
- reject(error);
- };
-
- request.onsuccess = () => {
- console.log('Retrieved HTML documents count:', request.result?.length || 0);
- resolve(request.result || []);
- };
- } catch (error) {
- console.error('Error in getAllHTMLDocuments transaction:', error);
- reject(error);
- }
- });
- }
-
- async removeHTMLDocument(id: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Removing HTML document:', id);
- const transaction = this.db!.transaction([HTML_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(HTML_STORE_NAME);
- const request = store.delete(id);
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error removing HTML document:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('HTML document removed successfully:', id);
- resolve();
- };
- } catch (error) {
- console.error('Error in removeHTMLDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- async clearHTMLDocuments(): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Clearing all HTML documents');
- const transaction = this.db!.transaction([HTML_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(HTML_STORE_NAME);
- const request = store.clear();
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error clearing HTML documents:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('All HTML documents cleared successfully');
- resolve();
- };
- } catch (error) {
- console.error('Error in clearHTMLDocuments transaction:', error);
- reject(error);
- }
- });
- }
-
- // Add EPUB Document Methods
- async addEPUBDocument(document: EPUBDocument): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- if (document.data.byteLength === 0) {
- throw new Error('Cannot store empty ArrayBuffer');
- }
-
- console.log('Storing document:', {
- name: document.name,
- size: document.size,
- actualSize: document.data.byteLength
- });
-
- const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(EPUB_STORE_NAME);
-
- // Create a structured clone of the document to ensure proper storage
- const request = store.put({
- ...document,
- data: document.data.slice(0) // Create a copy of the ArrayBuffer
- });
-
- request.onerror = (event) => {
- console.error('Error storing document:', (event.target as IDBRequest).error);
- reject((event.target as IDBRequest).error);
- };
-
- transaction.oncomplete = () => {
- console.log('Document stored successfully');
- resolve();
- };
- } catch (error) {
- console.error('Error in addEPUBDocument:', error);
- reject(error);
- }
- });
- }
-
- async getEPUBDocument(id: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readonly');
- const store = transaction.objectStore(EPUB_STORE_NAME);
- const request = store.get(id);
-
- request.onerror = (event) => {
- console.error('Error fetching document:', (event.target as IDBRequest).error);
- reject((event.target as IDBRequest).error);
- };
-
- request.onsuccess = () => {
- const doc = request.result;
- if (doc) {
- console.log('Retrieved document from DB:', {
- name: doc.name,
- size: doc.size,
- actualSize: doc.data.byteLength
- });
- }
- resolve(doc);
- };
- } catch (error) {
- console.error('Error in getEPUBDocument:', error);
- reject(error);
- }
- });
- }
-
- async getAllEPUBDocuments(): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readonly');
- const store = transaction.objectStore(EPUB_STORE_NAME);
- const request = store.getAll();
-
- request.onerror = (event) => {
- reject((event.target as IDBRequest).error);
- };
-
- request.onsuccess = () => {
- resolve(request.result || []);
- };
- } catch (error) {
- reject(error);
- }
- });
- }
-
- async removeEPUBDocument(id: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Removing EPUB document:', id);
- const locationKey = `lastLocation_${id}`;
-
- // Create two transactions - one for document deletion, one for location cleanup
- const docTransaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
- const configTransaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
-
- const docStore = docTransaction.objectStore(EPUB_STORE_NAME);
- const configStore = configTransaction.objectStore(CONFIG_STORE_NAME);
-
- // Remove the document
- const docRequest = docStore.delete(id);
- // Remove any saved location
- const locationRequest = configStore.delete(locationKey);
-
- docRequest.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error removing EPUB document:', error);
- reject(error);
- };
-
- locationRequest.onerror = (event) => {
- console.warn('Error cleaning up location:', (event.target as IDBRequest).error);
- // Don't reject here, as document deletion is the primary operation
- };
-
- // Wait for both transactions to complete
- Promise.all([
- new Promise((res) => { docTransaction.oncomplete = () => res(); }),
- new Promise((res) => { configTransaction.oncomplete = () => res(); })
- ]).then(() => {
- console.log('EPUB document and location data removed successfully:', id);
- resolve();
- }).catch(reject);
-
- } catch (error) {
- console.error('Error in removeEPUBDocument transaction:', error);
- reject(error);
- }
- });
- }
-
- // Config Methods
- async setConfigItem(key: string, value: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Setting config item:', key);
- const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(CONFIG_STORE_NAME);
- const request = store.put({ key, value });
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error setting config item:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('Config item set successfully:', key);
- resolve();
- };
- } catch (error) {
- console.error('Error in setConfigItem transaction:', error);
- reject(error);
- }
- });
- }
-
- async getConfigItem(key: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Fetching config item:', key);
- const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readonly');
- const store = transaction.objectStore(CONFIG_STORE_NAME);
- const request = store.get(key);
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error fetching config item:', error);
- reject(error);
- };
-
- request.onsuccess = () => {
- const result = request.result as Config | undefined;
- console.log('Config item fetch result:', result ? 'found' : 'not found');
- resolve(result ? result.value : null);
- };
- } catch (error) {
- console.error('Error in getConfigItem transaction:', error);
- reject(error);
- }
- });
- }
-
- async getAllConfig(): Promise> {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Fetching all config items');
- const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readonly');
- const store = transaction.objectStore(CONFIG_STORE_NAME);
- const request = store.getAll();
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error fetching all config items:', error);
- reject(error);
- };
-
- request.onsuccess = () => {
- const result = request.result as Config[];
- const config: Record = {};
- result.forEach((item) => {
- config[item.key] = item.value;
- });
- console.log('Retrieved config items count:', result.length);
- resolve(config);
- };
- } catch (error) {
- console.error('Error in getAllConfig transaction:', error);
- reject(error);
- }
- });
- }
-
- async removeConfigItem(key: string): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Removing config item:', key);
- const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(CONFIG_STORE_NAME);
- const request = store.delete(key);
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error removing config item:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('Config item removed successfully:', key);
- resolve();
- };
- } catch (error) {
- console.error('Error in removeConfigItem transaction:', error);
- reject(error);
- }
- });
- }
-
- 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) {
- documents.push({
- ...doc,
- type: 'pdf',
- data: Array.from(new Uint8Array(doc.data))
- });
- processedDocs++;
- if (onProgress) {
- onProgress((processedDocs / totalDocs) * 50, `Processing ${processedDocs}/${totalDocs} documents...`);
- }
- }
-
- // Process EPUB documents
- for (const doc of epubDocs) {
- documents.push({
- ...doc,
- 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 }),
- 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(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 (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 = {
- id: doc.id,
- type: doc.type,
- name: doc.name,
- size: doc.size,
- lastModified: doc.lastModified,
- data: uint8Array.buffer
- };
-
- if (doc.type === 'pdf') {
- await this.addDocument(documentData);
- } else if (doc.type === 'epub') {
- await this.addEPUBDocument(documentData);
- } 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() };
- }
-
- async clearPDFDocuments(): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Clearing all PDF documents');
- const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(PDF_STORE_NAME);
- const request = store.clear();
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error clearing PDF documents:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('All PDF documents cleared successfully');
- resolve();
- };
- } catch (error) {
- console.error('Error in clearPDFDocuments transaction:', error);
- reject(error);
- }
- });
- }
-
- async clearEPUBDocuments(): Promise {
- if (!this.db) {
- await this.init();
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('Clearing all EPUB documents');
- const transaction = this.db!.transaction([EPUB_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(EPUB_STORE_NAME);
- const request = store.clear();
-
- request.onerror = (event) => {
- const error = (event.target as IDBRequest).error;
- console.error('Error clearing EPUB documents:', error);
- reject(error);
- };
-
- transaction.oncomplete = () => {
- console.log('All EPUB documents cleared successfully');
- resolve();
- };
- } catch (error) {
- console.error('Error in clearEPUBDocuments transaction:', error);
- reject(error);
- }
- });
- }
-
- async saveDocumentListState(state: DocumentListState): Promise {
- return this.setConfigItem('documentListState', JSON.stringify(state));
- }
-
- async getDocumentListState(): Promise {
- const stateStr = await this.getConfigItem('documentListState');
- if (!stateStr) return null;
- try {
- return JSON.parse(stateStr);
- } catch (error) {
- console.error('Error parsing document list state:', error);
- return null;
- }
- }
-}
-
-// Make sure we export a singleton instance
-const indexedDBServiceInstance = new IndexedDBService();
-export const indexedDBService = indexedDBServiceInstance;
-
-// Helper functions for the ConfigContext
-export async function getItem(key: string): Promise {
- return indexedDBService.getConfigItem(key);
-}
-
-export async function setItem(key: string, value: string): Promise {
- return indexedDBService.setConfigItem(key, value);
-}
-
-export async function removeItem(key: string): Promise {
- return indexedDBService.removeConfigItem(key);
-}
-
-// Add these helper functions before the final export
-export async function getLastDocumentLocation(docId: string): Promise {
- const key = `lastLocation_${docId}`;
- return indexedDBService.getConfigItem(key);
-}
-
-export async function setLastDocumentLocation(docId: string, location: string): Promise {
- const key = `lastLocation_${docId}`;
- return indexedDBService.setConfigItem(key, location);
-}
-
-export async function getDocumentListState(): Promise {
- return indexedDBService.getDocumentListState();
-}
-
-export async function saveDocumentListState(state: DocumentListState): Promise {
- return indexedDBService.saveDocumentListState(state);
-}
\ No newline at end of file