diff --git a/apps/readest-app/.env.tauri b/apps/readest-app/.env.tauri new file mode 100644 index 00000000..a7af3d37 --- /dev/null +++ b/apps/readest-app/.env.tauri @@ -0,0 +1 @@ +NEXT_PUBLIC_APP_PLATFORM=tauri \ No newline at end of file diff --git a/apps/readest-app/.env.web b/apps/readest-app/.env.web new file mode 100644 index 00000000..fa1898b7 --- /dev/null +++ b/apps/readest-app/.env.web @@ -0,0 +1 @@ +NEXT_PUBLIC_APP_PLATFORM=web \ No newline at end of file diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 063a18b7..232b4a85 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -1,11 +1,14 @@ { "name": "@readest/readest-app", - "version": "0.8.0", + "version": "0.8.1", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", + "dev": "dotenv -e .env.tauri -- next dev", + "build": "dotenv -e .env.tauri -- next build", + "start": "dotenv -e .env.tauri -- next start", + "dev-web": "dotenv -e .env.web -- next dev", + "build-web": "dotenv -e .env.web -- next build", + "start-web": "dotenv -e .env.web -- next start", "lint": "next lint", "tauri": "tauri", "prepare-public-vendor": "mkdirp ./public/vendor/pdfjs", diff --git a/apps/readest-app/src/app/layout.tsx b/apps/readest-app/src/app/layout.tsx index 609cd571..d1dfbcad 100644 --- a/apps/readest-app/src/app/layout.tsx +++ b/apps/readest-app/src/app/layout.tsx @@ -6,6 +6,7 @@ import { AuthProvider } from '@/context/AuthContext'; import { EnvProvider } from '@/context/EnvContext'; import { CSPostHogProvider } from '@/context/PHContext'; import { useTheme } from '@/hooks/useTheme'; +import { isTauriAppPlatform } from '@/services/environment'; import { checkForAppUpdates } from '@/helpers/updater'; import '../styles/globals.css'; @@ -20,7 +21,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) React.useEffect(() => { const doAppUpdates = async () => { - await checkForAppUpdates(); + if (isTauriAppPlatform()) { + await checkForAppUpdates(); + } }; doAppUpdates(); }, []); diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index ad686219..17ffc497 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -4,6 +4,7 @@ import { FaSearch } from 'react-icons/fa'; import { PiPlus } from 'react-icons/pi'; import { PiSelectionAllDuotone } from 'react-icons/pi'; +import { useEnv } from '@/context/EnvContext'; import useTrafficLight from '@/hooks/useTrafficLight'; import WindowButtons from '@/components/WindowButtons'; @@ -18,6 +19,7 @@ const LibraryHeader: React.FC = ({ onImportBooks, onToggleSelectMode, }) => { + const { appService } = useEnv(); const { isTrafficLightVisible } = useTrafficLight(); const headerRef = useRef(null); @@ -85,9 +87,9 @@ const LibraryHeader: React.FC = ({ diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 5c2de108..e8c63824 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -4,16 +4,19 @@ import * as React from 'react'; import { useState, useRef } from 'react'; import { useRouter } from 'next/navigation'; +import { Book } from '@/types/book'; +import { AppService } from '@/types/system'; +import { parseOpenWithFiles } from '@/helpers/cli'; +import { isTauriAppPlatform } from '@/services/environment'; +import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants'; + import { useEnv } from '@/context/EnvContext'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; -import { Book } from '@/types/book'; -import { parseOpenWithFiles } from '@/helpers/cli'; import Spinner from '@/components/Spinner'; import LibraryHeader from '@/app/library/components/LibraryHeader'; import Bookshelf from '@/app/library/components/Bookshelf'; -import { AppService } from '@/types/system'; const LibraryPage = () => { const router = useRouter(); @@ -55,16 +58,17 @@ const LibraryPage = () => { if (isInitiating.current) return; isInitiating.current = true; - const loadingTimeout = setTimeout(() => setLoading(true), 200); + const loadingTimeout = setTimeout(() => setLoading(true), 300); const initLibrary = async () => { const appService = await envConfig.getAppService(); const settings = await appService.loadSettings(); setSettings(settings); const libraryBooks = await appService.loadLibraryBooks(); - if (checkOpenWithBooks) { + if (checkOpenWithBooks && isTauriAppPlatform()) { await handleOpenWithBooks(appService, libraryBooks); } else { + clearOpenWithBooks(); setLibrary(libraryBooks); } @@ -102,14 +106,14 @@ const LibraryPage = () => { }; const selectFilesTauri = async () => { - return appService?.selectFiles('Select Books', ['epub', 'pdf']); + return appService?.selectFiles('Select Books', SUPPORTED_FILE_EXTS); }; const selectFilesWeb = () => { return new Promise((resolve) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; - fileInput.accept = '.epub, .pdf'; + fileInput.accept = FILE_ACCEPT_FORMATS; fileInput.multiple = true; fileInput.click(); @@ -121,12 +125,17 @@ const LibraryPage = () => { const handleImportBooks = async () => { console.log('Importing books...'); - const { type } = await import('@tauri-apps/plugin-os'); let files; - if (['android', 'ios'].includes(type())) { - files = (await selectFilesWeb()) as [File]; + + if (isTauriAppPlatform()) { + const { type } = await import('@tauri-apps/plugin-os'); + if (['android', 'ios'].includes(type())) { + files = (await selectFilesWeb()) as [File]; + } else { + files = (await selectFilesTauri()) as [string]; + } } else { - files = (await selectFilesTauri()) as [string]; + files = (await selectFilesWeb()) as [File]; } importBooks(files); }; diff --git a/apps/readest-app/src/app/reader/components/HeaderBar.tsx b/apps/readest-app/src/app/reader/components/HeaderBar.tsx index 719f13d1..963aabc8 100644 --- a/apps/readest-app/src/app/reader/components/HeaderBar.tsx +++ b/apps/readest-app/src/app/reader/components/HeaderBar.tsx @@ -2,6 +2,7 @@ import clsx from 'clsx'; import React, { useRef, useState } from 'react'; import { PiDotsThreeVerticalBold } from 'react-icons/pi'; +import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import useTrafficLight from '@/hooks/useTrafficLight'; @@ -29,6 +30,7 @@ const HeaderBar: React.FC = ({ onCloseBook, onSetSettingsDialogOpen, }) => { + const { appService } = useEnv(); const headerRef = useRef(null); const { isTrafficLightVisible } = useTrafficLight(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -78,8 +80,8 @@ const HeaderBar: React.FC = ({ onCloseBook(bookKey)} /> diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx index cbb1ef19..c52c0a7e 100644 --- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx +++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx @@ -101,7 +101,7 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) => if (!bookKeys || bookKeys.length === 0) return null; const bookData = getBookData(bookKeys[0]!); if (!bookData || !bookData.book || !bookData.bookDoc) { - setTimeout(() => setLoading(true), 200); + setTimeout(() => setLoading(true), 300); return ( loading && (
diff --git a/apps/readest-app/src/app/reader/components/annotator/DeepLPopup.tsx b/apps/readest-app/src/app/reader/components/annotator/DeepLPopup.tsx index 26840997..64ead4f9 100644 --- a/apps/readest-app/src/app/reader/components/annotator/DeepLPopup.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/DeepLPopup.tsx @@ -61,6 +61,10 @@ const DeepLPopup: React.FC = ({ setError(null); setTranslation(null); + if (!process.env['NEXT_PUBLIC_DEEPL_API_KEY']) { + console.error('DeepL API key not found. Set NEXT_PUBLIC_DEEPL_API_KEY in .env.local'); + } + try { const response = await fetch('https://api-free.deepl.com/v2/translate', { method: 'POST', diff --git a/apps/readest-app/src/hooks/useTrafficLight.ts b/apps/readest-app/src/hooks/useTrafficLight.ts index d2bc78df..947dcb1c 100644 --- a/apps/readest-app/src/hooks/useTrafficLight.ts +++ b/apps/readest-app/src/hooks/useTrafficLight.ts @@ -30,7 +30,10 @@ const useTrafficLight = () => { }; useEffect(() => { + if (!appService?.hasTrafficLight) return; + handleSwitchFullScreen(); + return () => { unlistenEnterFullScreen?.(); unlistenExitFullScreen?.(); diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 24d11777..b3e2cacc 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -1,4 +1,4 @@ -import { AppService, ToastType } from '@/types/system'; +import { AppPlatform, AppService, ToastType } from '@/types/system'; import { SystemSettings } from '@/types/settings'; import { FileSystem, BaseDir } from '@/types/system'; @@ -26,14 +26,15 @@ import { export abstract class BaseAppService implements AppService { localBooksDir: string = ''; + abstract fs: FileSystem; + abstract appPlatform: AppPlatform; abstract isAppDataSandbox: boolean; abstract hasTrafficLight: boolean; - abstract fs: FileSystem; abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string }; abstract getCoverImageUrl(book: Book): string; + abstract getCoverImageBlobUrl(book: Book): Promise; abstract getInitBooksDir(): Promise; - abstract selectDirectory(title: string): Promise; abstract selectFiles(name: string, extensions: string[]): Promise; abstract showMessage( msg: string, @@ -133,8 +134,6 @@ export abstract class BaseAppService implements AppService { author: loadedBook.metadata.author, lastUpdated: Date.now(), }; - book.coverImageUrl = this.getCoverImageUrl(book); - if (!(await this.fs.exists(getDir(book), 'Books'))) { await this.fs.createDir(getDir(book), 'Books'); } @@ -156,6 +155,13 @@ export abstract class BaseAppService implements AppService { await this.saveBookConfig(book, INIT_BOOK_CONFIG); books.splice(0, 0, book); } + + if (this.appPlatform === 'web') { + book.coverImageUrl = await this.getCoverImageBlobUrl(book); + } else { + book.coverImageUrl = this.getCoverImageUrl(book); + } + return book; } catch (error) { throw error; @@ -173,7 +179,13 @@ export abstract class BaseAppService implements AppService { async loadBookContent(book: Book, settings: SystemSettings): Promise { const fp = getFilename(book); - const file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open(); + let file: File; + if (this.appPlatform === 'web') { + const content = await this.fs.readFile(fp, 'Books', 'binary'); + file = new File([content], fp); + } else { + file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open(); + } return { book, file, config: await this.loadBookConfig(book, settings) }; } @@ -222,9 +234,16 @@ export abstract class BaseAppService implements AppService { await this.fs.writeFile(libraryFilename, 'Books', '[]'); } - books.forEach((book) => { - book.coverImageUrl = this.getCoverImageUrl(book); - }); + await Promise.all( + books.map(async (book) => { + if (this.appPlatform === 'web') { + book.coverImageUrl = await this.getCoverImageBlobUrl(book); + } else { + book.coverImageUrl = this.getCoverImageUrl(book); + } + return book; + }), + ); return books; } diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index e5a0d94b..ca1567b8 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -4,6 +4,9 @@ import { ReadSettings } from '@/types/settings'; export const LOCAL_BOOKS_SUBDIR = 'Readest/Books'; export const CLOUD_BOOKS_SUBDIR = 'Readest/Books'; +export const SUPPORTED_FILE_EXTS = ['epub', 'mobi', 'azw', 'azw3', 'fb2', 'cbz', 'pdf']; +export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).join(', '); + export const DEFAULT_READSETTINGS: ReadSettings = { sideBarWidth: '25%', isSideBarPinned: true, diff --git a/apps/readest-app/src/services/environment.ts b/apps/readest-app/src/services/environment.ts index ec902c2c..c645989f 100644 --- a/apps/readest-app/src/services/environment.ts +++ b/apps/readest-app/src/services/environment.ts @@ -1,19 +1,39 @@ import { AppService } from '@/types/system'; -let appService: AppService | null = null; +export const isTauriAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri'; +export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web'; export interface EnvConfigType { getAppService: () => Promise; } +let nativeAppService: AppService | null = null; +const getNativeAppService = async () => { + if (!nativeAppService) { + const { NativeAppService } = await import('@/services/nativeAppService'); + nativeAppService = new NativeAppService(); + await nativeAppService.loadSettings(); + } + return nativeAppService; +}; + +let webAppService: AppService | null = null; +const getWebAppService = async () => { + if (!webAppService) { + const { WebAppService } = await import('@/services/webAppService'); + webAppService = new WebAppService(); + await webAppService.loadSettings(); + } + return webAppService; +}; + const environmentConfig: EnvConfigType = { getAppService: async () => { - if (!appService) { - const { NativeAppService } = await import('@/services/nativeAppService'); - appService = new NativeAppService(); - await appService.loadSettings(); + if (isTauriAppPlatform()) { + return getNativeAppService(); + } else { + return getWebAppService(); } - return appService; }, }; diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 266f3e72..f021a6af 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -16,7 +16,7 @@ import { join, appDataDir } from '@tauri-apps/api/path'; import { type as osType } from '@tauri-apps/plugin-os'; import { Book } from '@/types/book'; -import { ToastType, FileSystem, BaseDir } from '@/types/system'; +import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system'; import { getCoverFilename } from '@/utils/book'; import { BaseAppService } from './appService'; @@ -24,10 +24,7 @@ import { LOCAL_BOOKS_SUBDIR } from './constants'; export const isMobile = ['android', 'ios'].includes(osType()); -export const resolvePath = ( - fp: string, - base: BaseDir, -): { baseDir: number; base: BaseDir; fp: string } => { +const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => { switch (base) { case 'Settings': return { baseDir: BaseDirectory.AppConfig, fp, base }; @@ -56,6 +53,10 @@ export const nativeFileSystem: FileSystem = { getURL(path: string) { return convertFileSrc(path); }, + async getBlobURL(path: string, base: BaseDir) { + const content = await this.readFile(path, base, 'binary'); + return URL.createObjectURL(new Blob([content])); + }, async copyFile(srcPath: string, dstPath: string, base: BaseDir) { const { fp, baseDir } = resolvePath(dstPath, base); await copyFile(srcPath, fp, base && { toPathBaseDir: baseDir }); @@ -114,6 +115,7 @@ export const nativeFileSystem: FileSystem = { export class NativeAppService extends BaseAppService { fs = nativeFileSystem; + appPlatform = 'tauri' as AppPlatform; isAppDataSandbox = isMobile; hasTrafficLight = osType() === 'macos'; @@ -125,14 +127,6 @@ export class NativeAppService extends BaseAppService { return join(await appDataDir(), LOCAL_BOOKS_SUBDIR); } - async selectDirectory(title: string): Promise { - const selected = await open({ - title, - directory: true, - }); - return selected as string; - } - async selectFiles(name: string, extensions: string[]): Promise { const selected = await open({ multiple: true, @@ -153,4 +147,8 @@ export class NativeAppService extends BaseAppService { getCoverImageUrl = (book: Book): string => { return this.fs.getURL(`${this.localBooksDir}/${getCoverFilename(book)}`); }; + + getCoverImageBlobUrl = async (book: Book): Promise => { + return this.fs.getBlobURL(`${this.localBooksDir}/${getCoverFilename(book)}`, 'None'); + }; } diff --git a/apps/readest-app/src/services/webAppService.ts b/apps/readest-app/src/services/webAppService.ts new file mode 100644 index 00000000..bc5525f7 --- /dev/null +++ b/apps/readest-app/src/services/webAppService.ts @@ -0,0 +1,206 @@ +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 { + 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((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((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((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((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((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 { + return LOCAL_BOOKS_SUBDIR; + } + + async selectDirectory(): Promise { + throw new Error('selectDirectory is not supported in browser'); + } + + async selectFiles(): Promise { + throw new Error('selectFiles is not supported in browser'); + } + + async showMessage(msg: string, kind: ToastType = 'info'): Promise { + alert(`${kind.toUpperCase()}: ${msg}`); + } + + getCoverImageUrl = (book: Book): string => { + return this.fs.getURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`); + }; + + getCoverImageBlobUrl = async (book: Book): Promise => { + return this.fs.getBlobURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`, 'None'); + }; +} diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index 9e250a15..0796d613 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -1,11 +1,13 @@ import { SystemSettings } from './settings'; import { Book, BookConfig, BookContent } from './book'; +export type AppPlatform = 'web' | 'tauri'; export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Log' | 'Cache' | 'None'; export type ToastType = 'info' | 'warning' | 'error'; export interface FileSystem { getURL(path: string): string; + getBlobURL(path: string, base: BaseDir): Promise; copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise; readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise; writeFile(path: string, base: BaseDir, content: string | ArrayBuffer): Promise; @@ -18,10 +20,10 @@ export interface FileSystem { export interface AppService { fs: FileSystem; + appPlatform: AppPlatform; hasTrafficLight: boolean; isAppDataSandbox: boolean; - selectDirectory(title: string): Promise; selectFiles(name: string, extensions: string[]): Promise; showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise; @@ -35,4 +37,5 @@ export interface AppService { loadLibraryBooks(): Promise; saveLibraryBooks(books: Book[]): Promise; getCoverImageUrl(book: Book): string; + getCoverImageBlobUrl(book: Book): Promise; }