import { SystemSettings } from '@/types/settings'; import { AppPlatform, AppService, BaseDir, DeleteAction, DistChannel, FileItem, FileSystem, OsPlatform, ResolvedPath, SelectDirectoryMode, } from '@/types/system'; import { DatabaseOpts, DatabaseService } from '@/types/database'; import { SchemaType } from '@/services/database/migrate'; import { Book, BookConfig, BookContent, ImportBookOptions, ViewSettings } from '@/types/book'; import type { BookNav } from '@/services/nav'; import { getLibraryFilename, getLibraryBackupFilename } from '@/utils/book'; import { getOSPlatform } from '@/utils/misc'; import { ProgressHandler } from '@/utils/transfer'; import { CustomTextureInfo } from '@/styles/textures'; import { CustomFont, CustomFontInfo } from '@/styles/fonts'; import type { ImportedDictionary } from './dictionaries/types'; import type { SelectedFile } from '@/hooks/useFileSelector'; import * as BookSvc from './bookService'; import * as CloudSvc from './cloudService'; import * as DictSvc from './dictionaries/dictionaryService'; import * as FontSvc from './fontService'; import * as ImageSvc from './imageService'; import * as LibrarySvc from './libraryService'; import * as Settings from './settingsService'; export abstract class BaseAppService implements AppService { osPlatform: OsPlatform = getOSPlatform(); appPlatform: AppPlatform = 'tauri'; localBooksDir = ''; isMobile = false; isMacOSApp = false; isLinuxApp = false; isAppDataSandbox = false; isAndroidApp = false; isIOSApp = false; isWindowsApp = false; isMobileApp = false; isPortableApp = false; isDesktopApp = false; isAppImage = false; isEink = false; hasTrafficLight = false; hasWindow = false; hasWindowBar = false; hasContextMenu = false; hasRoundedWindow = false; hasSafeAreaInset = false; hasHaptics = false; hasUpdater = false; hasOrientationLock = false; hasScreenBrightness = false; hasIAP = false; canCustomizeRootDir = false; canReadExternalDir = false; supportsCanvasContext2DFilter = true; distChannel = 'readest' as DistChannel; storefrontRegionCode: string | null = null; isOnlineCatalogsAccessible = true; protected CURRENT_MIGRATION_VERSION = 20251124; protected abstract fs: FileSystem; protected abstract resolvePath(fp: string, base: BaseDir): ResolvedPath; abstract init(): Promise; abstract setCustomRootDir(customRootDir: string): Promise; abstract selectDirectory(mode: SelectDirectoryMode): Promise; abstract selectFiles(name: string, extensions: string[]): Promise; abstract saveFile( filename: string, content: string | ArrayBuffer | null, options?: { filePath?: string; mimeType?: string; share?: boolean; sharePosition?: { x: number; y: number; preferredEdge?: 'top' | 'bottom' | 'left' | 'right' }; }, ): Promise; abstract ask(message: string): Promise; abstract openDatabase( schema: SchemaType, path: string, base: BaseDir, opts?: DatabaseOpts, ): Promise; protected async runMigrations(lastMigrationVersion: number): Promise { if (lastMigrationVersion < 20251124) { try { await this.migrate20251124(); } catch (error) { console.error('Error migrating to version 20251124:', error); } } } private async migrate20251124(): Promise { console.log('Running migration for version 20251124 to rename the backup library file...'); const oldBackupFilename = getLibraryBackupFilename(); const newBackupFilename = `${getLibraryFilename()}.bak`; if (await this.fs.exists(oldBackupFilename, 'Books')) { try { const content = await this.fs.readFile(oldBackupFilename, 'Books', 'text'); await this.fs.writeFile(newBackupFilename, 'Books', content); await this.fs.removeFile(oldBackupFilename, 'Books'); console.log('Migration to rename backup library file completed successfully.'); } catch (error) { console.error('Error during migration to rename backup library file:', error); } } } async prepareBooksDir() { this.localBooksDir = await this.fs.getPrefix('Books'); } async openFile(path: string, base: BaseDir): Promise { return await this.fs.openFile(path, base); } async copyFile( srcPath: string, srcBase: BaseDir, dstPath: string, dstBase: BaseDir, ): Promise { return await this.fs.copyFile(srcPath, srcBase, dstPath, dstBase); } async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') { return await this.fs.readFile(path, base, mode); } async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File) { return await this.fs.writeFile(path, base, content); } async createDir(path: string, base: BaseDir, recursive: boolean = true): Promise { return await this.fs.createDir(path, base, recursive); } async deleteFile(path: string, base: BaseDir): Promise { return await this.fs.removeFile(path, base); } async deleteDir(path: string, base: BaseDir, recursive: boolean = true): Promise { return await this.fs.removeDir(path, base, recursive); } async resolveFilePath(path: string, base: BaseDir): Promise { const prefix = await this.fs.getPrefix(base); return path ? `${prefix}/${path}` : prefix; } async readDirectory(path: string, base: BaseDir): Promise { return await this.fs.readDir(path, base); } async exists(path: string, base: BaseDir): Promise { return await this.fs.exists(path, base); } async isDirectory(path: string, base: BaseDir): Promise { try { const info = await this.fs.stats(path, base); return info.isDirectory; } catch { return false; } } async getImageURL(path: string): Promise { return await this.fs.getImageURL(path); } private get settingsCtx(): Settings.Context { return { fs: this.fs, isMobile: this.isMobile, isEink: this.isEink, isAppDataSandbox: this.isAppDataSandbox, }; } private get coverCtx(): BookSvc.CoverContext { return { fs: this.fs, appPlatform: this.appPlatform, localBooksDir: this.localBooksDir }; } getDefaultViewSettings(): ViewSettings { return Settings.getDefaultViewSettings(this.settingsCtx); } async loadSettings(): Promise { const settings = await Settings.loadSettings(this.settingsCtx); this.localBooksDir = settings.localBooksDir; return settings; } async saveSettings(settings: SystemSettings): Promise { await Settings.saveSettings(this.fs, settings); } getCoverImageUrl = (book: Book): string => BookSvc.getCoverImageUrl(this.coverCtx, book); getCoverImageBlobUrl = async (book: Book): Promise => BookSvc.getCoverImageBlobUrl(this.coverCtx, book); async getCachedImageUrl(pathOrUrl: string): Promise { return BookSvc.getCachedImageUrl(this.coverCtx, pathOrUrl); } async generateCoverImageUrl(book: Book): Promise { return BookSvc.generateCoverImageUrl(this.coverCtx, book); } async updateCoverImage(book: Book, imageUrl?: string, imageFile?: string): Promise { return BookSvc.updateCoverImage(this.coverCtx, book, imageUrl, imageFile); } async importFont(file?: string | File): Promise { return FontSvc.importFont(this.fs, file); } async deleteFont(font: CustomFont): Promise { return FontSvc.deleteFont(this.fs, font); } async importImage(file?: string | File): Promise { return ImageSvc.importImage(this.fs, file); } async deleteImage(texture: CustomTextureInfo): Promise { return ImageSvc.deleteImage(this.fs, texture); } async importDictionaries( files: SelectedFile[], existingDictionaries: ImportedDictionary[] = [], ): Promise { return DictSvc.importDictionaries(this.fs, files, existingDictionaries); } async deleteDictionary(dict: ImportedDictionary): Promise { return DictSvc.deleteDictionary(this.fs, dict); } async importBook( file: string | File, books: Book[], options: ImportBookOptions = {}, ): Promise { return BookSvc.importBook(this.fs, file, books, { saveBookConfig: this.saveBookConfig.bind(this), generateCoverImageUrl: this.generateCoverImageUrl.bind(this), ...options, }); } async deleteBook(book: Book, deleteAction: DeleteAction): Promise { return CloudSvc.deleteBook(this.fs, book, deleteAction); } async uploadFileToCloud( lfp: string, cfp: string, base: BaseDir, handleProgress: ProgressHandler, hash: string, temp: boolean = false, ) { return CloudSvc.uploadFileToCloud( this.fs, this.resolveFilePath.bind(this), lfp, cfp, base, handleProgress, hash, temp, ); } async uploadReplicaFile( kind: string, replicaId: string, filename: string, lfp: string, base: BaseDir, onProgress: ProgressHandler, ) { return CloudSvc.uploadReplicaFileToCloud(this.fs, this.resolveFilePath.bind(this), { kind, replicaId, filename, lfp, base, onProgress, }); } async downloadReplicaFile( kind: string, replicaId: string, filename: string, lfp: string, base: BaseDir, onProgress?: ProgressHandler, ) { // Resolve the relative `/` lfp against the // replica's base dir before downloading. Mirrors how upload uses // `resolveFilePath(opts.lfp, opts.base)`. Without this, the writer // lands the bytes at the literal lfp (no base prefix) so subsequent // openFile(lfp, base) calls fail with "File not found". const dst = await this.resolveFilePath(lfp, base); return CloudSvc.downloadReplicaFileFromCloud(this, { kind, replicaId, filename, dst, onProgress, }); } async deleteReplicaBundle(kind: string, replicaId: string, filenames: string[]) { return CloudSvc.deleteReplicaBundleFromCloud(kind, replicaId, filenames); } async uploadBook(book: Book, onProgress?: ProgressHandler): Promise { return CloudSvc.uploadBook(this.fs, this.resolveFilePath.bind(this), book, onProgress); } async downloadCloudFile(lfp: string, cfp: string, onProgress: ProgressHandler) { return CloudSvc.downloadCloudFile(this, this.localBooksDir, lfp, cfp, onProgress); } async downloadBookCovers(books: Book[]): Promise { return CloudSvc.downloadBookCovers(this, this.fs, this.localBooksDir, books); } async downloadBook( book: Book, onlyCover = false, redownload = false, onProgress?: ProgressHandler, ): Promise { return CloudSvc.downloadBook( this, this.fs, this.localBooksDir, book, onlyCover, redownload, onProgress, ); } async exportBook(book: Book): Promise { return BookSvc.exportBook( this.fs, book, this.resolveFilePath.bind(this), this.copyFile.bind(this), this.saveFile.bind(this), ); } async refreshBookMetadata(book: Book): Promise { return BookSvc.refreshBookMetadata(this.fs, book); } async isBookAvailable(book: Book): Promise { return BookSvc.isBookAvailable(this.fs, book); } async getBookFileSize(book: Book): Promise { return BookSvc.getBookFileSize(this.fs, book); } async loadBookContent(book: Book): Promise { return BookSvc.loadBookContent(this.fs, book); } async loadBookConfig(book: Book, settings: SystemSettings): Promise { return BookSvc.loadBookConfig(this.fs, book, settings); } async fetchBookDetails(book: Book) { return BookSvc.fetchBookDetails(this.fs, book, this.downloadBook.bind(this)); } async saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings) { return BookSvc.saveBookConfig(this.fs, book, config, settings); } async loadBookNav(book: Book) { return BookSvc.loadBookNav(this.fs, book); } async saveBookNav(book: Book, nav: BookNav) { return BookSvc.saveBookNav(this.fs, book, nav); } async loadLibraryBooks(): Promise { return LibrarySvc.loadLibraryBooks(this.fs, this.generateCoverImageUrl.bind(this)); } async saveLibraryBooks(books: Book[]): Promise { return LibrarySvc.saveLibraryBooks(this.fs, books); } }