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, ViewSettings } from '@/types/book'; 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 * as BookSvc from './bookService'; import * as CloudSvc from './cloudService'; 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; 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; 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, options?: { filePath?: string; mimeType?: string }, ): 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, dstPath: string, base: BaseDir): Promise { return await this.fs.copyFile(srcPath, dstPath, base); } 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 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 importBook( file: string | File, books: Book[], saveBook: boolean = true, saveCover: boolean = true, overwrite: boolean = false, transient: boolean = false, ): Promise { return BookSvc.importBook( this.fs, file, books, saveBook, saveCover, overwrite, transient, this.saveBookConfig.bind(this), this.generateCoverImageUrl.bind(this), ); } 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 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 loadLibraryBooks(): Promise { return LibrarySvc.loadLibraryBooks(this.fs, this.generateCoverImageUrl.bind(this)); } async saveLibraryBooks(books: Book[]): Promise { return LibrarySvc.saveLibraryBooks(this.fs, books); } }