Files
readest/apps/readest-app/src/services/webAppService.ts
T
chrox aa16bc09f1 Support running Readest in modern browsers
Now we support Web platform
2024-12-05 18:57:16 +01:00

207 lines
6.5 KiB
TypeScript

import { Book } from '@/types/book';
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { getCoverFilename } from '@/utils/book';
import { BaseAppService } from './appService';
import { LOCAL_BOOKS_SUBDIR } from './constants';
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
switch (base) {
case 'Books':
return { baseDir: 0, fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`, base };
case 'None':
return { baseDir: 0, fp, base };
default:
return { baseDir: 0, fp: `${base}/${fp}`, base };
}
};
const dbName = 'AppFileSystem';
const dbVersion = 1;
async function openIndexedDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, dbVersion);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains('files')) {
db.createObjectStore('files', { keyPath: 'path' });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
const indexedDBFileSystem: FileSystem = {
getURL(path: string) {
return URL.createObjectURL(new Blob([path]));
},
async getBlobURL(path: string, base: BaseDir) {
try {
const content = await this.readFile(path, base, 'binary');
return URL.createObjectURL(new Blob([content]));
} catch {
return path;
}
},
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
const { fp } = resolvePath(dstPath, base);
const db = await openIndexedDB();
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction('files', 'readwrite');
const store = transaction.objectStore('files');
const getRequest = store.get(srcPath);
getRequest.onsuccess = () => {
const data = getRequest.result;
if (data) {
store.put({ path: fp, content: data.content });
resolve();
} else {
reject(new Error(`File not found: ${srcPath}`));
}
};
getRequest.onerror = () => reject(getRequest.error);
});
},
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
const { fp } = resolvePath(path, base);
const db = await openIndexedDB();
return new Promise<string | ArrayBuffer>((resolve, reject) => {
const transaction = db.transaction('files', 'readonly');
const store = transaction.objectStore('files');
const request = store.get(fp);
request.onsuccess = async () => {
if (request.result) {
const content = request.result.content;
if (mode === 'text') resolve(content);
else {
if (content instanceof Blob) {
const arrayBuffer = await content.arrayBuffer();
resolve(arrayBuffer);
} else if (content instanceof ArrayBuffer) {
resolve(content);
} else if (typeof content === 'string') {
resolve(new TextEncoder().encode(content).buffer as ArrayBuffer);
} else {
reject(new Error('Unsupported content type in IndexedDB'));
}
}
} else {
reject(new Error(`File not found: ${fp}`));
}
};
request.onerror = () => reject(request.error);
});
},
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer) {
const { fp } = resolvePath(path, base);
const db = await openIndexedDB();
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction('files', 'readwrite');
const store = transaction.objectStore('files');
store.put({ path: fp, content });
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
},
async removeFile(path: string, base: BaseDir) {
const { fp } = resolvePath(path, base);
const db = await openIndexedDB();
return new Promise<void>((resolve, reject) => {
const transaction = db.transaction('files', 'readwrite');
const store = transaction.objectStore('files');
store.delete(fp);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
},
async createDir() {
// Directories are virtual in IndexedDB; no-op
},
async removeDir() {
// Directories are virtual in IndexedDB; no-op
},
async readDir(path: string) {
const db = await openIndexedDB();
return new Promise<{ path: string; isDir: boolean }[]>((resolve, reject) => {
const transaction = db.transaction('files', 'readonly');
const store = transaction.objectStore('files');
const request = store.getAll();
request.onsuccess = () => {
const files = request.result as { path: string }[];
resolve(
files
.filter((file) => file.path.startsWith(path))
.map((file) => ({ path: file.path, isDir: false })),
);
};
request.onerror = () => reject(request.error);
});
},
async exists(path: string, base: BaseDir) {
const { fp } = resolvePath(path, base);
const db = await openIndexedDB();
return new Promise<boolean>((resolve, reject) => {
const transaction = db.transaction('files', 'readonly');
const store = transaction.objectStore('files');
const request = store.get(fp);
request.onsuccess = () => resolve(!!request.result);
request.onerror = () => reject(request.error);
});
},
};
export class WebAppService extends BaseAppService {
fs = indexedDBFileSystem;
appPlatform = 'web' as AppPlatform;
isAppDataSandbox = false;
hasTrafficLight = false;
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
}
async getInitBooksDir(): Promise<string> {
return LOCAL_BOOKS_SUBDIR;
}
async selectDirectory(): Promise<string> {
throw new Error('selectDirectory is not supported in browser');
}
async selectFiles(): Promise<string[]> {
throw new Error('selectFiles is not supported in browser');
}
async showMessage(msg: string, kind: ToastType = 'info'): Promise<void> {
alert(`${kind.toUpperCase()}: ${msg}`);
}
getCoverImageUrl = (book: Book): string => {
return this.fs.getURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`);
};
getCoverImageBlobUrl = async (book: Book): Promise<string> => {
return this.fs.getBlobURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`, 'None');
};
}