diff --git a/apps/readest-app/src-tauri/Info-ios.plist b/apps/readest-app/src-tauri/Info-ios.plist
new file mode 100644
index 00000000..d679effa
--- /dev/null
+++ b/apps/readest-app/src-tauri/Info-ios.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ LSSupportsOpeningDocumentsInPlace
+
+
+
diff --git a/apps/readest-app/src-tauri/Info.plist b/apps/readest-app/src-tauri/Info.plist
index 30554cb8..bc00a3e0 100644
--- a/apps/readest-app/src-tauri/Info.plist
+++ b/apps/readest-app/src-tauri/Info.plist
@@ -19,6 +19,8 @@
EPUB Document
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
org.idpf.epub-container
@@ -30,6 +32,8 @@
PDF Document
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
com.adobe.pdf
@@ -41,6 +45,8 @@
FB2 Document
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
com.readest.fb2
@@ -52,6 +58,8 @@
CBZ Archive
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
com.readest.cbz
@@ -63,6 +71,8 @@
MOBI Document
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
org.mobipocket.mobi
@@ -74,6 +84,8 @@
AZW Document
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
com.amazon.azw
@@ -86,6 +98,8 @@
Text File
LSHandlerRank
Alternate
+ CFBundleTypeRole
+ Viewer
LSItemContentTypes
public.plain-text
@@ -93,6 +107,153 @@
+ UTImportedTypeDeclarations
+
+
+ UTTypeIdentifier
+ org.idpf.epub-container
+ UTTypeDescription
+ EPUB Document
+ UTTypeConformsTo
+
+ public.data
+ public.composite-content
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ epub
+
+ public.mime-type
+ application/epub+zip
+
+
+
+
+ UTTypeIdentifier
+ com.adobe.pdf
+ UTTypeDescription
+ PDF Document
+ UTTypeConformsTo
+
+ public.data
+ public.composite-content
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ pdf
+
+ public.mime-type
+ application/pdf
+
+
+
+
+ UTTypeIdentifier
+ com.readest.fb2
+ UTTypeDescription
+ FB2 Document
+ UTTypeConformsTo
+
+ public.xml
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ fb2
+
+ public.mime-type
+ application/xml
+
+
+
+
+ UTTypeIdentifier
+ com.readest.cbz
+ UTTypeDescription
+ CBZ Archive
+ UTTypeConformsTo
+
+ public.archive
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ cbz
+
+ public.mime-type
+ application/x-cbz
+
+
+
+
+ UTTypeIdentifier
+ org.mobipocket.mobi
+ UTTypeDescription
+ MOBI Document
+ UTTypeConformsTo
+
+ public.data
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ mobi
+
+ public.mime-type
+ application/x-mobipocket-ebook
+
+
+
+
+ UTTypeIdentifier
+ com.amazon.azw
+ UTTypeDescription
+ AZW Document
+ UTTypeConformsTo
+
+ public.data
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ azw
+ azw3
+
+ public.mime-type
+ application/vnd.amazon.ebook
+
+
+
+
+ UTTypeIdentifier
+ public.plain-text
+ UTTypeDescription
+ Text File
+ UTTypeConformsTo
+
+ public.data
+ public.content
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ txt
+
+ public.mime-type
+ text/plain
+
+
+
+
UTExportedTypeDeclarations
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift
index 78457c23..ba4fc6d4 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/ios/Sources/NativeBridgePlugin.swift
@@ -53,6 +53,11 @@ class SetScreenBrightnessRequestArgs: Decodable {
let brightness: Float?
}
+class CopyUriToPathRequestArgs: Decodable {
+ let uri: String?
+ let dst: String?
+}
+
struct InitializeRequest: Decodable {
let publicKey: String?
}
@@ -816,6 +821,59 @@ class NativeBridgePlugin: Plugin {
}
invoke.resolve(["success": true])
}
+
+ @objc public func copy_uri_to_path(_ invoke: Invoke) {
+ guard let args = try? invoke.parseArgs(CopyUriToPathRequestArgs.self) else {
+ return invoke.reject("Failed to parse arguments")
+ }
+
+ guard let uriString = args.uri, let dstPath = args.dst else {
+ return invoke.reject("URI and destination path must be provided")
+ }
+
+ guard let uri = URL(string: uriString) else {
+ return invoke.reject("Invalid URI")
+ }
+
+ let fileManager = FileManager.default
+ let dstURL = URL(fileURLWithPath: dstPath)
+
+ do {
+ let didStartAccessing = uri.startAccessingSecurityScopedResource()
+ defer {
+ if didStartAccessing {
+ uri.stopAccessingSecurityScopedResource()
+ }
+ }
+
+ var shouldCopy = false
+
+ if fileManager.fileExists(atPath: dstURL.path) {
+ let srcAttributes = try fileManager.attributesOfItem(atPath: uri.path)
+ let dstAttributes = try fileManager.attributesOfItem(atPath: dstURL.path)
+
+ let srcModDate = srcAttributes[.modificationDate] as? Date ?? Date.distantPast
+ let dstModDate = dstAttributes[.modificationDate] as? Date ?? Date.distantPast
+
+ if srcModDate > dstModDate {
+ try fileManager.removeItem(at: dstURL)
+ shouldCopy = true
+ } else {
+ shouldCopy = false
+ }
+ } else {
+ shouldCopy = true
+ }
+
+ if shouldCopy {
+ try fileManager.copyItem(at: uri, to: dstURL)
+ }
+
+ invoke.resolve(["success": true])
+ } catch {
+ invoke.reject("Failed to copy file: \(error.localizedDescription)")
+ }
+ }
}
@_cdecl("init_plugin_native_bridge")
diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json
index 9403d353..31de56ca 100644
--- a/apps/readest-app/src-tauri/tauri.conf.json
+++ b/apps/readest-app/src-tauri/tauri.conf.json
@@ -72,6 +72,7 @@
},
"iOS": {
"developmentTeam": "J5W48D69VR",
+ "infoPlist": "./Info-ios.plist",
"minimumSystemVersion": "14.0"
},
"fileAssociations": [
diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx
index 54ece2a4..ae0b73cb 100644
--- a/apps/readest-app/src/app/library/page.tsx
+++ b/apps/readest-app/src/app/library/page.tsx
@@ -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);
diff --git a/apps/readest-app/src/app/reader/components/ReaderContent.tsx b/apps/readest-app/src/app/reader/components/ReaderContent.tsx
index 8772b2a4..b9aa4c3c 100644
--- a/apps/readest-app/src/app/reader/components/ReaderContent.tsx
+++ b/apps/readest-app/src/app/reader/components/ReaderContent.tsx
@@ -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);
diff --git a/apps/readest-app/src/helpers/openWith.ts b/apps/readest-app/src/helpers/openWith.ts
index c8319057..47bcc34c 100644
--- a/apps/readest-app/src/helpers/openWith.ts
+++ b/apps/readest-app/src/helpers/openWith.ts
@@ -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;
};
diff --git a/apps/readest-app/src/helpers/shortcuts.ts b/apps/readest-app/src/helpers/shortcuts.ts
index eb2b89bc..ac6a2d3b 100644
--- a/apps/readest-app/src/helpers/shortcuts.ts
+++ b/apps/readest-app/src/helpers/shortcuts.ts
@@ -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'],
diff --git a/apps/readest-app/src/hooks/useOpenWithBooks.ts b/apps/readest-app/src/hooks/useOpenWithBooks.ts
index 7086ff75..055e82a9 100644
--- a/apps/readest-app/src/hooks/useOpenWithBooks.ts
+++ b/apps/readest-app/src/hooks/useOpenWithBooks.ts
@@ -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(
+ '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 | 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);
diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts
index 0a642df2..471acadd 100644
--- a/apps/readest-app/src/services/nativeAppService.ts
+++ b/apps/readest-app/src/services/nativeAppService.ts
@@ -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);