refactor: use useFileSelector hook for all file selection (#1905)

This commit is contained in:
Huang Xin
2025-08-26 16:04:10 +08:00
committed by GitHub
parent 66fceb2313
commit 977c61b914
3 changed files with 37 additions and 79 deletions
+19 -38
View File
@@ -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<HTMLDivElement> | 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) => {
@@ -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<BookDetailEditProps> = ({
}) => {
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<BookDetailEditProps> = ({
},
];
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 (
@@ -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'),
},
};