fix: support importing azw3 files on Android, closes #1920 (#1921)

This commit is contained in:
Huang Xin
2025-08-28 22:11:30 +08:00
committed by GitHub
parent cb126613b1
commit 1b00d8c41c
7 changed files with 85 additions and 29 deletions
@@ -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" />
+8 -1
View File
@@ -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"],
+3 -2
View File
@@ -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) {
+27 -8
View File
@@ -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;
+1 -1
View File
@@ -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 });
}