diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 585e6f4c..5c5509f8 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -164,9 +164,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP useEffect(() => { const libraryPage = document.querySelector('.library-page'); - libraryPage?.addEventListener('dragover', handleDragOver as unknown as EventListener); - libraryPage?.addEventListener('dragleave', handleDragLeave as unknown as EventListener); - libraryPage?.addEventListener('drop', handleDrop as unknown as EventListener); + if (!appService?.isMobile) { + libraryPage?.addEventListener('dragover', handleDragOver as unknown as EventListener); + libraryPage?.addEventListener('dragleave', handleDragLeave as unknown as EventListener); + libraryPage?.addEventListener('drop', handleDrop as unknown as EventListener); + } if (isTauriAppPlatform()) { const unlisten = getCurrentWebview().onDragDropEvent((event) => { @@ -185,9 +187,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP } return () => { - libraryPage?.removeEventListener('dragover', handleDragOver as unknown as EventListener); - libraryPage?.removeEventListener('dragleave', handleDragLeave as unknown as EventListener); - libraryPage?.removeEventListener('drop', handleDrop as unknown as EventListener); + if (!appService?.isMobile) { + libraryPage?.removeEventListener('dragover', handleDragOver as unknown as EventListener); + libraryPage?.removeEventListener('dragleave', handleDragLeave as unknown as EventListener); + libraryPage?.removeEventListener('drop', handleDrop as unknown as EventListener); + } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageRef.current]); @@ -337,7 +341,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP const selectFilesTauri = async () => { const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS; const files = (await appService?.selectFiles(_('Select Books'), exts)) || []; - return files.filter((file) => SUPPORTED_FILE_EXTS.some((ext) => file.endsWith(`.${ext}`))); + // Cannot filter out files on Android since some content providers may not return the file name + return files; }; const selectFilesWeb = () => { diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 4a5d5cf1..93715f8f 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -1,4 +1,4 @@ -import { AppPlatform, AppService, ToastType } from '@/types/system'; +import { AppPlatform, AppService } from '@/types/system'; import { SystemSettings } from '@/types/settings'; import { FileSystem, BaseDir } from '@/types/system'; @@ -62,13 +62,9 @@ export abstract class BaseAppService implements AppService { abstract getCoverImageUrl(book: Book): string; abstract getCoverImageBlobUrl(book: Book): Promise; abstract getInitBooksDir(): Promise; + abstract getCacheDir(): Promise; + abstract selectDirectory(): Promise; abstract selectFiles(name: string, extensions: string[]): Promise; - abstract showMessage( - msg: string, - kind?: ToastType, - title?: string, - okLabel?: string, - ): Promise; async loadSettings(): Promise { let settings: SystemSettings; @@ -118,9 +114,12 @@ export abstract class BaseAppService implements AppService { } this.localBooksDir = settings.localBooksDir; + const cacheDir = await this.getCacheDir(); this.fs.getPrefix = (baseDir: BaseDir) => { if (baseDir === 'Books') { return this.localBooksDir; + } else if (baseDir === 'Cache') { + return cacheDir; } return null; }; diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 1f3e0994..deafdd48 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -13,12 +13,12 @@ import { WriteFileOptions, } from '@tauri-apps/plugin-fs'; import { convertFileSrc } from '@tauri-apps/api/core'; -import { open as openDialog, message } from '@tauri-apps/plugin-dialog'; -import { join, appDataDir } from '@tauri-apps/api/path'; +import { open as openDialog } from '@tauri-apps/plugin-dialog'; +import { join, appDataDir, appCacheDir } from '@tauri-apps/api/path'; import { type as osType } from '@tauri-apps/plugin-os'; import { Book } from '@/types/book'; -import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system'; +import { FileSystem, BaseDir, AppPlatform } from '@/types/system'; import { isContentURI, isValidURL } from '@/utils/misc'; import { getCoverFilename, getFilename } from '@/utils/book'; import { copyURIToPath } from '@/utils/bridge'; @@ -80,7 +80,22 @@ export const nativeFileSystem: FileSystem = { if (isValidURL(path)) { return await new RemoteFile(path, name).open(); } else if (isContentURI(path)) { - return await new NativeFile(fp, fname, base ? baseDir : null).open(); + if (path.includes('com.android.externalstorage')) { + // If the URI is from shared internal storage (like /storage/emulated/0), + // we can access it directly using the path — no need to copy. + return await new NativeFile(fp, fname, base ? baseDir : null).open(); + } else { + // Otherwise, for content:// URIs (e.g. from MediaStore, Drive, or third-party apps), + // we cannot access the file directly — so we copy it to a temporary cache location. + const prefix = this.getPrefix('Cache'); + const dst = `${prefix}/${fname}`; + const res = await copyURIToPath({ uri: path, dst }); + if (!res.success) { + console.error('Failed to open file:', res); + throw new Error('Failed to open file'); + } + return await new NativeFile(dst, fname, base ? baseDir : null).open(); + } } else { const prefix = this.getPrefix(base); if (prefix && OS_TYPE !== 'android') { @@ -213,6 +228,18 @@ export class NativeAppService extends BaseAppService { return join(await appDataDir(), LOCAL_BOOKS_SUBDIR); } + async getCacheDir(): Promise { + return await appCacheDir(); + } + + async selectDirectory(): Promise { + const selected = await openDialog({ + directory: true, + multiple: false, + }); + return selected as string; + } + async selectFiles(name: string, extensions: string[]): Promise { const selected = await openDialog({ multiple: true, @@ -221,15 +248,6 @@ export class NativeAppService extends BaseAppService { return Array.isArray(selected) ? selected : selected ? [selected] : []; } - async showMessage( - msg: string, - kind: ToastType = 'info', - title?: string, - okLabel?: string, - ): Promise { - await message(msg, { kind, title, okLabel }); - } - getCoverImageUrl = (book: Book): string => { return this.fs.getURL(`${this.localBooksDir}/${getCoverFilename(book)}`); }; diff --git a/apps/readest-app/src/services/webAppService.ts b/apps/readest-app/src/services/webAppService.ts index 8cde13da..aee4f803 100644 --- a/apps/readest-app/src/services/webAppService.ts +++ b/apps/readest-app/src/services/webAppService.ts @@ -1,5 +1,5 @@ import { Book } from '@/types/book'; -import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system'; +import { FileSystem, BaseDir, AppPlatform } from '@/types/system'; import { getCoverFilename } from '@/utils/book'; import { getOSPlatform, isValidURL } from '@/utils/misc'; import { RemoteFile } from '@/utils/file'; @@ -212,6 +212,10 @@ export class WebAppService extends BaseAppService { return LOCAL_BOOKS_SUBDIR; } + async getCacheDir(): Promise { + return 'Cache'; + } + async selectDirectory(): Promise { throw new Error('selectDirectory is not supported in browser'); } @@ -220,10 +224,6 @@ export class WebAppService extends BaseAppService { 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)}`); }; diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index e3380d41..4b9863c9 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -5,7 +5,6 @@ import { ProgressHandler } from '@/utils/transfer'; 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; @@ -39,9 +38,8 @@ export interface AppService { isAndroidApp: boolean; isIOSApp: boolean; + selectDirectory(): Promise; selectFiles(name: string, extensions: string[]): Promise; - showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise; - loadSettings(): Promise; saveSettings(settings: SystemSettings): Promise; importBook(