diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 6d399fc9..3633a2ba 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -18,7 +18,7 @@ import { getFilename } from '@/utils/path'; import { parseOpenWithFiles } from '@/helpers/openWith'; import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment'; import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater'; -import { BOOK_ACCEPT_FORMATS, SUPPORTED_BOOK_EXTS } from '@/services/constants'; +import { BOOK_ACCEPT_FORMATS } from '@/services/constants'; import { impactFeedback } from '@tauri-apps/plugin-haptics'; import { getCurrentWebview } from '@tauri-apps/api/webview'; @@ -35,6 +35,7 @@ import { useDemoBooks } from './hooks/useDemoBooks'; import { useBooksSync } from './hooks/useBooksSync'; import { useScreenWakeLock } from '@/hooks/useScreenWakeLock'; import { useOpenWithBooks } from '@/hooks/useOpenWithBooks'; +import { FILE_SELECTION_PRESETS, SelectedFile, useFileSelector } from '@/hooks/useFileSelector'; import { lockScreenOrientation } from '@/utils/bridge'; import { tauriHandleSetAlwaysOnTop, @@ -73,6 +74,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP setCheckLastOpenBooks, } = useLibraryStore(); const _ = useTranslation(); + const { selectFiles } = useFileSelector(appService, _); const { safeAreaInsets: insets } = useThemeStore(); const { settings, setSettings, saveSettings } = useSettingsStore(); const [loading, setLoading] = useState(false); @@ -164,7 +166,14 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP impactFeedback('medium'); } - await importBooks(supportedFiles); + const selectedFiles = supportedFiles.map( + (file) => + ({ + file: typeof file === 'string' ? undefined : file, + path: typeof file === 'string' ? file : undefined, + }) as SelectedFile, + ); + await importBooks(selectedFiles); }; const handleDragOver = (event: React.DragEvent | DragEvent) => { @@ -392,7 +401,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP // eslint-disable-next-line react-hooks/exhaustive-deps }, [demoBooks, libraryLoaded]); - const importBooks = async (files: (string | File)[]) => { + const importBooks = async (files: SelectedFile[]) => { setLoading(true); const failedFiles = []; const errorMap: [string, string][] = [ @@ -401,7 +410,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP ['Unsupported format.', _('This book format is not supported.')], ]; const { library } = useLibraryStore.getState(); - for (const file of files) { + for (const selectedFile of files) { + const file = selectedFile.file || selectedFile.path; + if (!file) continue; try { const book = await appService?.importBook(file, library); setLibrary([...library]); @@ -431,32 +442,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP setLoading(false); }; - const selectFilesTauri = async () => { - const exts = appService?.isIOSApp ? [] : SUPPORTED_BOOK_EXTS; - const files = (await appService?.selectFiles(_('Select Books'), exts)) || []; - if (appService?.isIOSApp) { - return files.filter((file) => { - const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown'; - return SUPPORTED_BOOK_EXTS.includes(fileExt); - }); - } - return files; - }; - - const selectFilesWeb = () => { - return new Promise((resolve) => { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = BOOK_ACCEPT_FORMATS; - fileInput.multiple = true; - fileInput.click(); - - fileInput.onchange = () => { - resolve(fileInput.files); - }; - }); - }; - const updateBookTransferProgress = throttle((bookHash: string, progress: ProgressPayload) => { if (progress.total === 0) return; const progressPct = (progress.progress / progress.total) * 100; @@ -605,14 +590,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP const handleImportBooks = async () => { setIsSelectMode(false); console.log('Importing books...'); - let files; - - if (isTauriAppPlatform()) { - files = (await selectFilesTauri()) as string[]; - } else { - files = (await selectFilesWeb()) as File[]; - } - importBooks(files); + selectFiles({ ...FILE_SELECTION_PRESETS.books, multiple: true }).then((result) => { + if (result.files.length === 0 || result.error) return; + importBooks(result.files); + }); }; const handleSetSelectMode = (selectMode: boolean) => { diff --git a/apps/readest-app/src/components/metadata/BookDetailEdit.tsx b/apps/readest-app/src/components/metadata/BookDetailEdit.tsx index de1653b4..c10dcdef 100644 --- a/apps/readest-app/src/components/metadata/BookDetailEdit.tsx +++ b/apps/readest-app/src/components/metadata/BookDetailEdit.tsx @@ -8,8 +8,7 @@ import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { flattenContributors, formatAuthors, formatPublisher, formatTitle } from '@/utils/book'; import { FormField } from './FormField'; -import { IMAGE_ACCEPT_FORMATS, SUPPORTED_IMAGE_EXTS } from '@/services/constants'; -import { isTauriAppPlatform } from '@/services/environment'; +import { FILE_SELECTION_PRESETS, useFileSelector } from '@/hooks/useFileSelector'; import BookCover from '@/components/BookCover'; interface BookDetailEditProps { @@ -47,6 +46,7 @@ const BookDetailEdit: React.FC = ({ }) => { const _ = useTranslation(); const { appService } = useEnv(); + const { selectFiles } = useFileSelector(appService, _); const hasLockedFields = Object.values(lockedFields).some((locked) => locked); const allFieldsLocked = Object.values(lockedFields).every((locked) => locked); @@ -148,51 +148,23 @@ const BookDetailEdit: React.FC = ({ }, ]; - const selectImageFileWeb = () => { - return new Promise((resolve) => { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = IMAGE_ACCEPT_FORMATS; - fileInput.multiple = false; - fileInput.click(); - - fileInput.onchange = () => { - resolve(fileInput.files); - }; - }); - }; - - const selectImageFileTauri = async () => { - const exts = appService?.isMobileApp ? [] : SUPPORTED_IMAGE_EXTS; - const files = (await appService?.selectFiles(_('Select Cover Image'), exts)) || []; - if (appService?.isIOSApp) { - return files.filter((file) => { - const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown'; - return SUPPORTED_IMAGE_EXTS.includes(fileExt); - }); - } - return files; - }; - const handleSelectLocalImage = async () => { - let files; - if (isTauriAppPlatform()) { - files = (await selectImageFileTauri()) as string[]; - if (appService && files.length > 0) { - metadata.coverImageFile = files[0]!; + selectFiles({ ...FILE_SELECTION_PRESETS.covers, multiple: false }).then(async (result) => { + if (result.error || result.files.length === 0) return; + const selectedFile = result.files[0]!; + if (selectedFile.path && appService) { + const filePath = selectedFile.path; + metadata.coverImageFile = filePath; const tempName = `cover-${Date.now()}.png`; const cachePrefix = await appService.fs.getPrefix('Cache'); - await appService.fs.copyFile(files[0]!, tempName, 'Cache'); + await appService.fs.copyFile(filePath, tempName, 'Cache'); metadata.coverImageUrl = await appService.fs.getURL(`${cachePrefix}/${tempName}`); setNewCoverImageUrl(metadata.coverImageUrl!); - } - } else { - files = (await selectImageFileWeb()) as File[]; - if (files.length > 0) { - metadata.coverImageBlobUrl = URL.createObjectURL(files[0]!); + } else if (selectedFile.file) { + metadata.coverImageBlobUrl = URL.createObjectURL(selectedFile.file); setNewCoverImageUrl(metadata.coverImageBlobUrl!); } - } + }); }; return ( diff --git a/apps/readest-app/src/hooks/useFileSelector.ts b/apps/readest-app/src/hooks/useFileSelector.ts index 824b3a17..40294dcd 100644 --- a/apps/readest-app/src/hooks/useFileSelector.ts +++ b/apps/readest-app/src/hooks/useFileSelector.ts @@ -71,7 +71,7 @@ const processTauriFiles = (files: string[]): SelectedFile[] => { export const useFileSelector = (appService: AppService | null, _: (key: string) => string) => { const selectFiles = async (options: FileSelectorOptions = {}) => { if (!appService) { - return { files: [], error: 'App service is not available' }; + return { files: [] as SelectedFile[], error: 'App service is not available' }; } try { if (isTauriAppPlatform()) { @@ -121,4 +121,9 @@ export const FILE_SELECTION_PRESETS = { extensions: ['ttf', 'otf', 'woff', 'woff2'], dialogTitle: _('Select Fonts'), }, + covers: { + accept: '.png, .jpg, .jpeg, .gif', + extensions: ['png', 'jpg', 'jpeg', 'gif'], + dialogTitle: _('Select Image'), + }, };