diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt b/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt index d8e902b6..3275a7af 100644 --- a/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt +++ b/apps/readest-app/src-tauri/gen/android/app/src/main/java/com/bilingify/readest/MainActivity.kt @@ -4,6 +4,7 @@ import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.webkit.WebView +import android.net.Uri import android.util.Log import android.content.Intent import android.graphics.Color @@ -16,6 +17,8 @@ import androidx.activity.OnBackPressedCallback import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject import com.readest.native_bridge.KeyDownInterceptor import com.readest.native_bridge.NativeBridgePlugin @@ -113,6 +116,8 @@ class MainActivity : TauriActivity(), KeyDownInterceptor { enableEdgeToEdge() super.onCreate(savedInstanceState) + handleIncomingIntent(intent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setTaskDescription( ActivityManager.TaskDescription( @@ -164,4 +169,48 @@ class MainActivity : TauriActivity(), KeyDownInterceptor { NativeBridgePlugin.getInstance()?.handleActivityResult(requestCode, resultCode, data) } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + intent?.let { handleIncomingIntent(it) } + } + + private fun handleIncomingIntent(intent: Intent) { + when (intent.action) { + Intent.ACTION_SEND -> { + if (intent.type != null) { + handleSingleFile(intent) + } + } + Intent.ACTION_SEND_MULTIPLE -> { + if (intent.type != null) { + handleMultipleFiles(intent) + } + } + } + } + + private fun handleSingleFile(intent: Intent) { + val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) + uri?.let { fileUri -> + val payload = JSObject().apply { + var urls = JSArray() + urls.put(fileUri.toString()) + put("urls", urls) + } + NativeBridgePlugin.getInstance()?.triggerEvent("shared-intent", payload) + } + } + + private fun handleMultipleFiles(intent: Intent) { + val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + uris?.let { fileUris -> + val payload = JSObject().apply { + var urls = JSArray() + fileUris.forEach { urls.put(it.toString()) } + put("urls", urls) + } + NativeBridgePlugin.getInstance()?.triggerEvent("shared-intent", payload) + } + } } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt index 1acdb24a..f10354bb 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt @@ -773,4 +773,10 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { path } } + + fun triggerEvent(eventName: String, payload: JSObject) { + activity.runOnUiThread { + trigger(eventName, payload) + } + } } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs index 10653122..29ea7714 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs @@ -20,6 +20,8 @@ const COMMANDS: &[&str] = &[ "get_external_sdcard_path", "open_external_url", "select_directory", + "register_listener", + "remove_listener", "request_manage_storage_permission", "check_permissions", "request_permissions", diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/register_listener.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/register_listener.toml new file mode 100644 index 00000000..48363c0d --- /dev/null +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/register_listener.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-register-listener" +description = "Enables the register_listener command without any pre-configured scope." +commands.allow = ["register_listener"] + +[[permission]] +identifier = "deny-register-listener" +description = "Denies the register_listener command without any pre-configured scope." +commands.deny = ["register_listener"] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/remove_listener.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/remove_listener.toml new file mode 100644 index 00000000..9f315e51 --- /dev/null +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/remove_listener.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-remove-listener" +description = "Enables the remove_listener command without any pre-configured scope." +commands.allow = ["remove_listener"] + +[[permission]] +identifier = "deny-remove-listener" +description = "Denies the remove_listener command without any pre-configured scope." +commands.deny = ["remove_listener"] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md index 2fecfb0a..fbed275b 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md @@ -26,6 +26,8 @@ Default permissions for the plugin - `allow-open-external-url` - `allow-select-directory` - `allow-request-manage-storage-permission` +- `allow-register-listener` +- `allow-remove-listener` - `allow-check-permissions` - `allow-request-permissions` - `allow-checkPermissions` @@ -563,6 +565,58 @@ Denies the open_external_url command without any pre-configured scope. +`native-bridge:allow-register-listener` + + + + +Enables the register_listener command without any pre-configured scope. + + + + + + + +`native-bridge:deny-register-listener` + + + + +Denies the register_listener command without any pre-configured scope. + + + + + + + +`native-bridge:allow-remove-listener` + + + + +Enables the remove_listener command without any pre-configured scope. + + + + + + + +`native-bridge:deny-remove-listener` + + + + +Denies the remove_listener command without any pre-configured scope. + + + + + + + `native-bridge:allow-request-permissions` diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml index 8eac4050..c4229af8 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml @@ -23,6 +23,8 @@ permissions = [ "allow-open-external-url", "allow-select-directory", "allow-request-manage-storage-permission", + "allow-register-listener", + "allow-remove-listener", "allow-check-permissions", "allow-request-permissions", "allow-checkPermissions", diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json index 732e79e5..f04cf37b 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json @@ -534,6 +534,30 @@ "const": "deny-open-external-url", "markdownDescription": "Denies the open_external_url command without any pre-configured scope." }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, { "description": "Enables the request-permissions command without any pre-configured scope.", "type": "string", @@ -631,10 +655,10 @@ "markdownDescription": "Denies the use_background_audio command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-select-directory`\n- `allow-request-manage-storage-permission`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`" } ] } diff --git a/apps/readest-app/src/hooks/useOpenWithBooks.ts b/apps/readest-app/src/hooks/useOpenWithBooks.ts index 6530e28a..4f063016 100644 --- a/apps/readest-app/src/hooks/useOpenWithBooks.ts +++ b/apps/readest-app/src/hooks/useOpenWithBooks.ts @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { useEnv } from '@/context/EnvContext'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; +import { addPluginListener } from '@tauri-apps/api/core'; import { onOpenUrl } from '@tauri-apps/plugin-deep-link'; import { getCurrentWindow, getAllWindows } from '@tauri-apps/api/window'; import { isTauriAppPlatform } from '@/services/environment'; @@ -13,6 +14,10 @@ interface SingleInstancePayload { cwd: string; } +interface SharedIntentPayload { + urls: string[]; +} + export function useOpenWithBooks() { const router = useRouter(); const { appService } = useEnv(); @@ -26,20 +31,25 @@ export function useOpenWithBooks() { return sortedWindows[0]?.label === currentWindow.label; }; - const handleOpenWithFileUrl = async (url: string) => { - console.log('Handle Open with URL:', url); - let filePath = url; - if (filePath.startsWith('file://')) { - filePath = decodeURI(filePath.replace('file://', '')); + const handleOpenWithFileUrl = async (urls: string[]) => { + console.log('Handle Open with URL:', urls); + const filePaths = []; + for (let url of urls) { + if (url.startsWith('file://')) { + url = decodeURI(url.replace('file://', '')); + } + if (!/^(https?:|data:|blob:)/i.test(url)) { + filePaths.push(url); + } } - if (!/^(https?:|data:|blob:)/i.test(filePath)) { + if (filePaths.length > 0) { const settings = useSettingsStore.getState().settings; if (appService?.hasWindow && settings.openBookInNewWindow) { if (await isFirstWindow()) { - showLibraryWindow(appService, [filePath]); + showLibraryWindow(appService, filePaths); } } else { - window.OPEN_WITH_FILES = [filePath]; + window.OPEN_WITH_FILES = filePaths; setCheckOpenWithBooks(true); navigateToLibrary(router, `reload=${Date.now()}`); } @@ -55,19 +65,27 @@ export function useOpenWithBooks() { console.log('Received deep link:', event, payload); const { args } = payload as SingleInstancePayload; if (args?.[1]) { - handleOpenWithFileUrl(args[1]); + handleOpenWithFileUrl([args[1]]); } }); + const unlistenSharedIntent = addPluginListener( + 'native-bridge', + 'shared-intent', + (payload) => { + console.log('Received shared intent:', payload); + const { urls } = payload; + handleOpenWithFileUrl(urls); + }, + ); const listenOpenWithFiles = async () => { return await onOpenUrl((urls) => { - urls.forEach((url) => { - handleOpenWithFileUrl(url); - }); + handleOpenWithFileUrl(urls); }); }; const unlistenOpenUrl = listenOpenWithFiles(); return () => { unlistenDeeplink.then((f) => f()); + unlistenSharedIntent.then((f) => f.unregister()); unlistenOpenUrl.then((f) => f()); }; // eslint-disable-next-line react-hooks/exhaustive-deps