forked from akai/readest
@@ -30,27 +30,55 @@
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
<data android:mimeType="application/fb2" />
|
||||
<data android:mimeType="application/x-fb2" />
|
||||
<data android:mimeType="application/mobi"/>
|
||||
<data android:mimeType="application/azw"/>
|
||||
<data android:mimeType="application/azw3"/>
|
||||
<data android:mimeType="application/x-mobipocket-ebook" />
|
||||
<data android:mimeType="application/vnd.amazon.ebook" />
|
||||
<data android:mimeType="application/vnd.amazon.mobi8-ebook" />
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
<data android:mimeType="application/x-cbz" />
|
||||
<data android:mimeType="application/x-mobipocket-ebook" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="text/plain" />
|
||||
<data android:mimeType="application/octet-stream"/>
|
||||
<data android:mimeType="application/x-font-ttf"/>
|
||||
<data android:mimeType="application/x-font-otf"/>
|
||||
</intent-filter>
|
||||
|
||||
<data android:pathPattern="/.*\\.epub" />
|
||||
<data android:pathPattern="/.*\\.pdf" />
|
||||
<data android:pathPattern="/.*\\.fb2" />
|
||||
<data android:pathPattern="/.*\\.fb2.zip" />
|
||||
<data android:pathPattern="/.*\\.mobi" />
|
||||
<data android:pathPattern="/.*\\.azw" />
|
||||
<data android:pathPattern="/.*\\.cbz" />
|
||||
<data android:pathPattern="/.*\\.zip" />
|
||||
<data android:pathPattern="/.*\\.txt" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.epub" />
|
||||
<data android:pathPattern=".*\\.pdf" />
|
||||
<data android:pathPattern=".*\\.fb2" />
|
||||
<data android:pathPattern=".*\\.fb2.zip" />
|
||||
<data android:pathPattern=".*\\.mobi" />
|
||||
<data android:pathPattern=".*\\.azw" />
|
||||
<data android:pathPattern=".*\\.azw3" />
|
||||
<data android:pathPattern=".*\\.cbz" />
|
||||
<data android:pathPattern=".*\\.zip" />
|
||||
<data android:pathPattern=".*\\.txt" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="oauth">
|
||||
@@ -59,6 +87,7 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="readest" android:host="auth-callback" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -86,11 +86,18 @@
|
||||
},
|
||||
{
|
||||
"name": "azw",
|
||||
"ext": ["azw", "azw3"],
|
||||
"ext": ["azw"],
|
||||
"description": "AZW file",
|
||||
"mimeType": "application/vnd.amazon.ebook",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "azw3",
|
||||
"ext": ["azw3"],
|
||||
"description": "AZW3 file",
|
||||
"mimeType": "application/vnd.amazon.mobi8-ebook",
|
||||
"role": "Viewer"
|
||||
},
|
||||
{
|
||||
"name": "fb2",
|
||||
"ext": ["fb2"],
|
||||
|
||||
@@ -35,7 +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 { SelectedFile, useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { lockScreenOrientation } from '@/utils/bridge';
|
||||
import {
|
||||
tauriHandleSetAlwaysOnTop,
|
||||
@@ -589,7 +589,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
const handleImportBooks = async () => {
|
||||
setIsSelectMode(false);
|
||||
console.log('Importing books...');
|
||||
selectFiles({ ...FILE_SELECTION_PRESETS.books, multiple: true }).then((result) => {
|
||||
selectFiles({ type: 'books', multiple: true }).then((result) => {
|
||||
if (result.files.length === 0 || result.error) return;
|
||||
importBooks(result.files);
|
||||
});
|
||||
@@ -662,6 +662,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
ref={osRef}
|
||||
className='flex-grow'
|
||||
options={{ scrollbars: { autoHide: 'scroll' } }}
|
||||
events={{
|
||||
initialized: (instance) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCustomFontStore } from '@/store/customFontStore';
|
||||
import { FILE_SELECTION_PRESETS, useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { mountCustomFont } from '@/styles/fonts';
|
||||
import { parseFontName } from '@/utils/font';
|
||||
import { getFilename } from '@/utils/path';
|
||||
@@ -41,7 +41,7 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
currentDefaultFont === 'serif' ? viewSettings.serifFont : viewSettings.sansSerifFont;
|
||||
|
||||
const handleImportFont = () => {
|
||||
selectFiles({ ...FILE_SELECTION_PRESETS.fonts, multiple: true }).then(async (result) => {
|
||||
selectFiles({ type: 'fonts', multiple: true }).then(async (result) => {
|
||||
if (result.error || result.files.length === 0) return;
|
||||
if (!(await appService!.fs.exists('', 'Fonts'))) {
|
||||
await appService!.fs.createDir('', 'Fonts');
|
||||
@@ -164,9 +164,9 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
'card h-12 border shadow-sm',
|
||||
currentFontFamily === font.name
|
||||
? 'border-primary/50 bg-primary/50'
|
||||
: 'border-base-200 bg-base-200 cursor-pointer',
|
||||
: `border-base-200 bg-base-200 ${isDeleteMode ? '' : 'cursor-pointer'}`,
|
||||
)}
|
||||
onClick={() => handleSelectFont(font.id)}
|
||||
onClick={!isDeleteMode ? () => handleSelectFont(font.id) : undefined}
|
||||
title={font.name}
|
||||
>
|
||||
<div className='card-body flex items-center justify-center p-2'>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { BookMetadata } from '@/libs/document';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { flattenContributors, formatAuthors, formatPublisher, formatTitle } from '@/utils/book';
|
||||
import { useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { FormField } from './FormField';
|
||||
import { FILE_SELECTION_PRESETS, useFileSelector } from '@/hooks/useFileSelector';
|
||||
import BookCover from '@/components/BookCover';
|
||||
|
||||
interface BookDetailEditProps {
|
||||
@@ -149,7 +149,7 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
];
|
||||
|
||||
const handleSelectLocalImage = async () => {
|
||||
selectFiles({ ...FILE_SELECTION_PRESETS.covers, multiple: false }).then(async (result) => {
|
||||
selectFiles({ type: 'covers', multiple: false }).then(async (result) => {
|
||||
if (result.error || result.files.length === 0) return;
|
||||
const selectedFile = result.files[0]!;
|
||||
if (selectedFile.path && appService) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AppService } from '@/types/system';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { basename } from '@tauri-apps/api/path';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
import { BOOK_ACCEPT_FORMATS, SUPPORTED_BOOK_EXTS } from '@/services/constants';
|
||||
|
||||
export interface FileSelectorOptions {
|
||||
type: SelectionType;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
extensions?: string[];
|
||||
@@ -42,15 +44,24 @@ const selectFileTauri = async (
|
||||
appService: AppService,
|
||||
_: (key: string) => string,
|
||||
): Promise<string[]> => {
|
||||
const exts = appService?.isIOSApp ? [] : options.extensions || [];
|
||||
const noFilter = appService?.isIOSApp || (appService?.isAndroidApp && options.type === 'books');
|
||||
const exts = noFilter ? [] : options.extensions || [];
|
||||
const title = options.dialogTitle || _('Select Files');
|
||||
const files = (await appService?.selectFiles(_(title), exts)) || [];
|
||||
let files = (await appService?.selectFiles(_(title), exts)) || [];
|
||||
|
||||
if (appService?.isIOSApp && options.extensions) {
|
||||
return files.filter((file: string) => {
|
||||
const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown';
|
||||
return options.extensions!.includes(fileExt);
|
||||
});
|
||||
if (noFilter && options.extensions) {
|
||||
files = await Promise.all(
|
||||
files.map(async (file: string) => {
|
||||
let processedFile = file;
|
||||
if (appService?.isAndroidApp && file.startsWith('content://')) {
|
||||
processedFile = await basename(file);
|
||||
}
|
||||
const fileExt = processedFile.split('.').pop()?.toLowerCase() || 'unknown';
|
||||
const extensions = options.extensions!;
|
||||
const shouldInclude = extensions.includes(fileExt) || extensions.includes('*');
|
||||
return shouldInclude ? file : null;
|
||||
}),
|
||||
).then((results) => results.filter((file) => file !== null));
|
||||
}
|
||||
|
||||
return files;
|
||||
@@ -69,7 +80,8 @@ const processTauriFiles = (files: string[]): SelectedFile[] => {
|
||||
};
|
||||
|
||||
export const useFileSelector = (appService: AppService | null, _: (key: string) => string) => {
|
||||
const selectFiles = async (options: FileSelectorOptions = {}) => {
|
||||
const selectFiles = async (options: FileSelectorOptions = { type: 'generic' }) => {
|
||||
options = { ...FILE_SELECTION_PRESETS[options.type], ...options };
|
||||
if (!appService) {
|
||||
return { files: [] as SelectedFile[], error: 'App service is not available' };
|
||||
}
|
||||
@@ -96,6 +108,11 @@ export const useFileSelector = (appService: AppService | null, _: (key: string)
|
||||
};
|
||||
|
||||
export const FILE_SELECTION_PRESETS = {
|
||||
generic: {
|
||||
accept: '*/*',
|
||||
extensions: ['*'],
|
||||
dialogTitle: _('Select Files'),
|
||||
},
|
||||
images: {
|
||||
accept: 'image/*',
|
||||
extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
|
||||
@@ -127,3 +144,5 @@ export const FILE_SELECTION_PRESETS = {
|
||||
dialogTitle: _('Select Image'),
|
||||
},
|
||||
};
|
||||
|
||||
export type SelectionType = keyof typeof FILE_SELECTION_PRESETS;
|
||||
|
||||
@@ -176,7 +176,7 @@ export function useSync(bookKey?: string) {
|
||||
|
||||
const syncConfigs = useCallback(
|
||||
async (bookConfigs?: BookConfig[], bookId?: string, op: SyncOp = 'both') => {
|
||||
if (!lastSyncedAtInited) return;
|
||||
if (!bookId && !lastSyncedAtInited) return;
|
||||
if ((op === 'push' || op === 'both') && bookConfigs?.length) {
|
||||
await pushChanges({ configs: bookConfigs });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user