From a8430a7be115f5a5b32870f5eb89de8fbe5d99d2 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 16 Apr 2025 13:04:30 +0800 Subject: [PATCH] feat: open files with readest from file manager on Android, closes #865 (#895) --- .../android/app/src/main/AndroidManifest.xml | 43 ++++++++++++++++--- .../com/bilingify/readest/MainActivity.kt | 18 -------- .../src/main/java/NativeBridgePlugin.kt | 35 +++++++++++++++ .../app/library/components/LibraryHeader.tsx | 8 ++-- apps/readest-app/src/app/library/page.tsx | 4 +- .../app/reader/components/ReaderContent.tsx | 4 +- .../reader/components/annotator/Annotator.tsx | 6 ++- .../app/reader/components/tts/TTSControl.tsx | 5 ++- .../src/helpers/{cli.ts => openWith.ts} | 21 ++++++++- 9 files changed, 107 insertions(+), 37 deletions(-) rename apps/readest-app/src/helpers/{cli.ts => openWith.ts} (63%) diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 49626f08..630a8da6 100644 --- a/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -20,12 +20,43 @@ android:label="@string/main_activity_title" android:name=".MainActivity" android:exported="true"> - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 57b00b97..97272de0 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 @@ -2,14 +2,10 @@ package com.bilingify.readest import android.os.Build import android.os.Bundle -import android.content.Intent -import android.net.Uri import android.view.View import android.view.WindowInsets import android.view.WindowInsetsController import androidx.activity.enableEdgeToEdge -import app.tauri.plugin.JSObject -import com.readest.native_bridge.NativeBridgePlugin class MainActivity : TauriActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -19,20 +15,6 @@ class MainActivity : TauriActivity() { hideSystemUI() } - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - - val uri = intent.data ?: return - if (uri.scheme == "readest" && uri.host == "auth-callback") { - val result = JSObject().apply { - put("redirectUrl", uri.toString()) - } - - NativeBridgePlugin.pendingInvoke?.resolve(result) - NativeBridgePlugin.pendingInvoke = null - } - } - private fun hideSystemUI() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.decorView.windowInsetsController?.let { controller -> 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 62d3d1df..5fe13240 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 @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.util.Log +import android.webkit.WebView import androidx.core.content.FileProvider import androidx.browser.customtabs.CustomTabsIntent import app.tauri.annotation.Command @@ -40,6 +41,40 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { var pendingInvoke: Invoke? = null } + override fun load(webView: WebView) { + super.load(webView) + handleIntent(activity.intent) + } + + override fun onNewIntent(intent: Intent) { + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + val uri = intent?.data ?: return + Log.e("NativeBridgePlugin", "Received intent: $uri") + when { + uri.scheme == "readest" && uri.host == "auth-callback" -> { + val result = JSObject().apply { + put("redirectUrl", uri.toString()) + } + pendingInvoke?.resolve(result) + pendingInvoke = null + } + + intent.action == Intent.ACTION_VIEW -> { + try { + activity.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (e: SecurityException) { + Log.e("NativeBridgePlugin", "Failed to take persistable URI permission: ${e.message}") + } + } + } + } + @Command fun auth_with_custom_tab(invoke: Invoke) { val args = invoke.parseArgs(AuthRequestArgs::class.java) diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index 3a295910..9b6daadd 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -175,16 +175,16 @@ const LibraryHeader: React.FC = ({ } + toggleButton={} > - + } + toggleButton={} > - + {appService?.hasWindowBar && ( = ({ i dismissBook(bookKey); if (bookKeys.filter((key) => key !== bookKey).length == 0) { const openWithFiles = (await parseOpenWithFiles()) || []; - if (openWithFiles.length > 0) { + if (openWithFiles.length > 0 && !appService?.isMobile) { tauriHandleClose(); } else { saveSettingsAndGoToLibrary(); diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index 679424c7..5c177f29 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -200,8 +200,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const lineHeightValue = parseFloat(lineHeight) || viewSettings.lineHeight * viewSettings.defaultFontSize; const fontSizeValue = parseFloat(fontSize) || viewSettings.defaultFontSize; - const strokeWidth = style === 'underline' ? 2 : 4; - const padding = (lineHeightValue - fontSizeValue - strokeWidth) / 2; + const strokeWidth = 2; + const padding = viewSettings.vertical + ? (lineHeightValue - fontSizeValue - strokeWidth) / 2 + : strokeWidth; draw(Overlayer[style as keyof typeof Overlayer], { writingMode, color: hexColor, padding }); } }; diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index 4712630c..220c4985 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -38,9 +38,10 @@ const TTSControl = () => { const [timeoutTimestamp, setTimeoutTimestamp] = useState(0); const [timeoutFunc, setTimeoutFunc] = useState | null>(null); - const popupWidth = useResponsiveSize(POPUP_WIDTH); - const popupHeight = useResponsiveSize(POPUP_HEIGHT); const popupPadding = useResponsiveSize(POPUP_PADDING); + const maxWidth = window.innerWidth - 2 * popupPadding; + const popupWidth = Math.min(maxWidth, useResponsiveSize(POPUP_WIDTH)); + const popupHeight = useResponsiveSize(POPUP_HEIGHT); const iconRef = useRef(null); const ttsControllerRef = useRef(null); diff --git a/apps/readest-app/src/helpers/cli.ts b/apps/readest-app/src/helpers/openWith.ts similarity index 63% rename from apps/readest-app/src/helpers/cli.ts rename to apps/readest-app/src/helpers/openWith.ts index 7680f351..269d33bf 100644 --- a/apps/readest-app/src/helpers/cli.ts +++ b/apps/readest-app/src/helpers/openWith.ts @@ -1,8 +1,9 @@ import { isWebAppPlatform, hasCli } from '@/services/environment'; +import { getCurrent } from '@tauri-apps/plugin-deep-link'; declare global { interface Window { - OPEN_WITH_FILES?: string[]; + OPEN_WITH_FILES?: string[] | null; } } @@ -32,10 +33,28 @@ const parseCLIOpenWithFiles = async () => { return files; }; +const parseIntentOpenWithFiles = async () => { + const urls = await getCurrent(); + if (urls && urls.length > 0) { + console.log('Intent Open with URL:', urls); + const files = urls.map((url) => { + if (url.startsWith('file://')) { + return decodeURI(url.replace('file://', '')); + } + return url; + }); + return files; + } + return null; +}; + export const parseOpenWithFiles = async () => { if (isWebAppPlatform()) return []; let files = parseWindowOpenWithFiles(); + if (!files) { + files = await parseIntentOpenWithFiles(); + } if (!files && hasCli()) { files = await parseCLIOpenWithFiles(); }