feat(iOS): support open file with Readest in Files App, closes #2334 (#2705)

This commit is contained in:
Huang Xin
2025-12-13 20:28:03 +08:00
committed by GitHub
parent 730fadb834
commit c1530cc5c4
10 changed files with 261 additions and 16 deletions
+1 -1
View File
@@ -323,7 +323,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
};
const handleOpenWithBooks = async (appService: AppService, library: Book[]) => {
const openWithFiles = (await parseOpenWithFiles()) || [];
const openWithFiles = (await parseOpenWithFiles(appService)) || [];
if (openWithFiles.length > 0) {
return await processOpenWithFiles(appService, openWithFiles, library);
@@ -178,7 +178,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
}
dismissBook(bookKey);
if (bookKeys.filter((key) => key !== bookKey).length == 0) {
const openWithFiles = (await parseOpenWithFiles()) || [];
const openWithFiles = (await parseOpenWithFiles(appService)) || [];
if (appService?.hasWindow) {
if (openWithFiles.length > 0) {
tauriHandleOnCloseWindow(handleCloseBooks);
+9 -4
View File
@@ -1,4 +1,5 @@
import { isWebAppPlatform, hasCli } from '@/services/environment';
import { AppService } from '@/types/system';
import { getCurrent } from '@tauri-apps/plugin-deep-link';
declare global {
@@ -35,14 +36,18 @@ const parseCLIOpenWithFiles = async () => {
return files;
};
const parseIntentOpenWithFiles = async () => {
const parseIntentOpenWithFiles = async (appService: AppService | null) => {
const urls = await getCurrent();
if (urls && urls.length > 0) {
console.log('Intent Open with URL:', urls);
return urls
.map((url) => {
if (url.startsWith('file://')) {
return decodeURI(url.replace('file://', ''));
if (appService?.isIOSApp) {
return decodeURI(url);
} else {
return decodeURI(url.replace('file://', ''));
}
} else if (url.startsWith('content://')) {
return url;
} else {
@@ -55,7 +60,7 @@ const parseIntentOpenWithFiles = async () => {
return null;
};
export const parseOpenWithFiles = async () => {
export const parseOpenWithFiles = async (appService: AppService | null) => {
if (isWebAppPlatform()) return [];
let files = parseWindowOpenWithFiles();
@@ -63,7 +68,7 @@ export const parseOpenWithFiles = async () => {
files = await parseCLIOpenWithFiles();
}
if (!files || files.length === 0) {
files = await parseIntentOpenWithFiles();
files = await parseIntentOpenWithFiles(appService);
}
return files;
};
+1 -1
View File
@@ -1,6 +1,6 @@
const DEFAULT_SHORTCUTS = {
onSwitchSideBar: ['ctrl+Tab', 'opt+Tab', 'alt+Tab'],
onToggleSideBar: ['s', 'F9'],
onToggleSideBar: ['s'],
onToggleNotebook: ['n'],
onShowSearchBar: ['ctrl+f', 'cmd+f'],
onToggleScrollMode: ['shift+j'],
+19 -8
View File
@@ -36,7 +36,11 @@ export function useOpenWithBooks() {
const filePaths = [];
for (let url of urls) {
if (url.startsWith('file://')) {
url = decodeURI(url.replace('file://', ''));
if (appService?.isIOSApp) {
url = decodeURI(url);
} else {
url = decodeURI(url.replace('file://', ''));
}
}
if (!/^(https?:|data:|blob:)/i.test(url)) {
filePaths.push(url);
@@ -73,20 +77,27 @@ export function useOpenWithBooks() {
if (listenedOpenWithBooks.current) return;
listenedOpenWithBooks.current = true;
const unlistenDeeplink = getCurrentWindow().listen('single-instance', ({ event, payload }) => {
console.log('Received deep link:', event, payload);
const { args } = payload as SingleInstancePayload;
if (args?.[1]) {
handleOpenWithFileUrl([args[1]]);
}
});
// For Windows/Linux deep link and macOS open-file event
const unlistenDeeplink = getCurrentWindow().listen<SingleInstancePayload>(
'single-instance',
({ event, payload }) => {
console.log('Received deep link:', event, payload);
const { args } = payload;
if (args?.[1]) {
handleOpenWithFileUrl([args[1]]);
}
},
);
// For Android "Share to Readest" intent
let unlistenSharedIntent: Promise<PluginListener> | null = null;
// FIXME: register/unregister plugin listeniner on iOS might cause app freeze for unknown reason
// so we only register it on Android for now to support "Shared to Readest" feature
if (appService?.isAndroidApp) {
unlistenSharedIntent = initializeListeners();
}
// iOS Open with URL event
const listenOpenWithFiles = async () => {
return await onOpenUrl((urls) => {
handleOpenWithFileUrl(urls);
@@ -194,7 +194,7 @@ export const nativeFileSystem: FileSystem = {
let fname = name || getFilename(fp);
if (isValidURL(path)) {
return await new RemoteFile(path, fname).open();
} else if (isContentURI(path)) {
} else if (isContentURI(path) || (isFileURI(path) && OS_TYPE === 'ios')) {
fname = await basename(path);
if (path.includes('com.android.externalstorage')) {
// If the URI is from shared internal storage (like /storage/emulated/0),
@@ -202,6 +202,7 @@ export const nativeFileSystem: FileSystem = {
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
} else {
// Otherwise, for content:// URIs (e.g. from MediaStore, Drive, or third-party apps),
// or file:// URIs is security scoped resource in iOS (e.g. from Files app),
// we cannot access the file directly — so we copy it to a temporary cache location.
const prefix = await this.getPrefix('Cache');
const dst = await join(prefix, fname);