Procházet zdrojové kódy

feat: 登录SheetWebView问题

HapeLee před 2 měsíci
rodič
revize
fcab8713f4

+ 3 - 2
AGENTS.md

@@ -172,8 +172,9 @@ KMP 任务名在模块实际创建后才存在;不要假装运行尚未定义
 ## 重要项目约束
 ## 重要项目约束
 
 
 - 不将 jsoup 升级到 1.16.2 以上;新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。
 - 不将 jsoup 升级到 1.16.2 以上;新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。
-- 不重新引入 Hutool;加密、编码与日期使用现有 JCA、`java.time` 和 `help/crypto/CryptoUtils.kt` 路径,KMP
-  抽取时再通过能力契约替换 JVM API。
+- 应用代码不依赖 Hutool;Hutool 仅作为书源 JS 的运行时加密库保留在 classpath(书源会直接调用
+  `Packages.cn.hutool.*`),版本固定 5.8.22 勿升级。应用内部加密、编码与日期使用现有 JCA、
+  `java.time` 和 `help/crypto/CryptoUtils.kt` 路径,KMP 抽取时再通过能力契约替换 JVM API。
 - 代码 namespace 为 `io.legado.app`,Android `applicationId` 为 `io.legato.kazusa`,不要混用。
 - 代码 namespace 为 `io.legado.app`,Android `applicationId` 为 `io.legato.kazusa`,不要混用。
 - 当前 minSdk 26、target/compile SDK 37;Release 启用 R8 与资源压缩,`noR8` 变体用于排障。
 - 当前 minSdk 26、target/compile SDK 37;Release 启用 R8 与资源压缩,`noR8` 变体用于排障。
 - Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力,迁移前必须建立兼容测试或
 - Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力,迁移前必须建立兼容测试或

+ 2 - 0
app/build.gradle.kts

@@ -255,6 +255,8 @@ dependencies {
     implementation(libs.markwon.ext.tables)
     implementation(libs.markwon.ext.tables)
     implementation(libs.markwon.html)
     implementation(libs.markwon.html)
     implementation(libs.quick.chinese.transfer.core)
     implementation(libs.quick.chinese.transfer.core)
+    // 书源 JS 通过 Packages.cn.hutool.* 调用,保留在 classpath(应用代码不依赖,见 AGENTS.md)
+    implementation(libs.hutool.crypto)
     //noinspection GradleDependency
     //noinspection GradleDependency
     implementation(platform(libs.firebase.bom))
     implementation(platform(libs.firebase.bom))
     implementation(libs.firebase.analytics)
     implementation(libs.firebase.analytics)

+ 6 - 0
app/proguard-rules.pro

@@ -84,6 +84,12 @@
 # markwon
 # markwon
 -dontwarn org.commonmark.ext.gfm.**
 -dontwarn org.commonmark.ext.gfm.**
 
 
+# hutool-core hutool-crypto(书源 JS 通过 Packages.cn.hutool.* 反射调用,需整体保留)
+-keep class cn.hutool.crypto.**{*;}
+-keep class cn.hutool.core.codec.**{*;}
+-keep class cn.hutool.core.util.**{*;}
+-dontwarn cn.hutool.**
+
 -keep class okhttp3.*{*;}
 -keep class okhttp3.*{*;}
 -keep class okio.*{*;}
 -keep class okio.*{*;}
 -keep class com.jayway.jsonpath.*{*;}
 -keep class com.jayway.jsonpath.*{*;}

+ 6 - 1
app/src/main/java/io/legado/app/ui/login/SourceLoginJsExtensions.kt

@@ -7,6 +7,7 @@ import io.legado.app.constant.EventBus
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.data.entities.HttpTTS
 import io.legado.app.data.entities.HttpTTS
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadAloud
+import io.legado.app.ui.main.MainActivity
 import io.legado.app.ui.rss.read.RssJsExtensions
 import io.legado.app.ui.rss.read.RssJsExtensions
 import io.legado.app.ui.widget.dialog.BottomWebViewDialog
 import io.legado.app.ui.widget.dialog.BottomWebViewDialog
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
@@ -46,7 +47,11 @@ class SourceLoginJsExtensions(
 
 
     override fun open(name: String, url: String?, title: String?, origin: String?) {
     override fun open(name: String, url: String?, title: String?, origin: String?) {
         if (name == "login") {
         if (name == "login") {
-            activityRef.get()?.toastOnUi("已在登录界面")
+            if (activityRef.get() is MainActivity && MainActivity.hasActiveSourceLoginRoute) {
+                activityRef.get()?.toastOnUi("已在登录界面")
+            } else {
+                super.open(name, url, title, origin)
+            }
             return
             return
         }
         }
         super.open(name, url, title, origin)
         super.open(name, url, title, origin)

+ 23 - 18
app/src/main/java/io/legado/app/ui/login/SourceLoginSheets.kt

@@ -25,7 +25,6 @@ import androidx.compose.material.icons.filled.ContentCopy
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
@@ -39,11 +38,11 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.ui.about.MarkdownSheet
 import io.legado.app.ui.about.MarkdownSheet
+import io.legado.app.ui.rss.read.VisibleWebViewCompose
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.AppTextField
@@ -288,7 +287,17 @@ private fun SourceLoginWebSheet(
         contentPaddingEnabled = false,
         contentPaddingEnabled = false,
         sheetGesturesEnabled = false,
         sheetGesturesEnabled = false,
     ) {
     ) {
-        SourceLoginWebView(state, onIntent, onOpenExternalUrl)
+        Box(
+            Modifier
+                .fillMaxWidth()
+                .heightIn(min = 520.dp)
+        ) {
+            SourceLoginWebView(
+                state = state,
+                onIntent = onIntent,
+                onOpenExternalUrl = onOpenExternalUrl,
+            )
+        }
     }
     }
 }
 }
 
 
@@ -302,13 +311,11 @@ private fun SourceLoginWebView(
     val currentIntent by rememberUpdatedState(onIntent)
     val currentIntent by rememberUpdatedState(onIntent)
     val currentOpenExternalUrl by rememberUpdatedState(onOpenExternalUrl)
     val currentOpenExternalUrl by rememberUpdatedState(onOpenExternalUrl)
     var webView by remember { mutableStateOf<WebView?>(null) }
     var webView by remember { mutableStateOf<WebView?>(null) }
-    Box(Modifier
-        .fillMaxWidth()
-        .heightIn(min = 520.dp)) {
-        AndroidView(
+    Box(Modifier.fillMaxSize()) {
+        VisibleWebViewCompose(
             modifier = Modifier.fillMaxSize(),
             modifier = Modifier.fillMaxSize(),
-            factory = { context ->
-                WebView(context).apply {
+            onCreated = { createdWebView ->
+                createdWebView.apply {
                     settings.apply {
                     settings.apply {
                         mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                         mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                         domStorageEnabled = true
                         domStorageEnabled = true
@@ -319,7 +326,9 @@ private fun SourceLoginWebView(
                         displayZoomControls = false
                         displayZoomControls = false
                         state.headers[AppConst.UA_NAME]?.let { userAgentString = it }
                         state.headers[AppConst.UA_NAME]?.let { userAgentString = it }
                     }
                     }
-                    isNestedScrollingEnabled = true
+                    // 不透明 WebView:透明层在 Chromium 反复合成时会导致网页内 fixed 悬浮表单
+                    // (如登录弹层)显示异常,参考 WebViewRouteScreen 的处理
+                    setBackgroundColor(android.graphics.Color.WHITE)
                     webViewClient = object : WebViewClient() {
                     webViewClient = object : WebViewClient() {
                         override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                         override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                             url?.let { currentIntent(SourceLoginIntent.WebPageStarted(it)) }
                             url?.let { currentIntent(SourceLoginIntent.WebPageStarted(it)) }
@@ -360,7 +369,6 @@ private fun SourceLoginWebView(
                     webView = this
                     webView = this
                 }
                 }
             },
             },
-            update = {},
         )
         )
         if (state.webProgress in 0..99) {
         if (state.webProgress in 0..99) {
             LinearProgressIndicator(
             LinearProgressIndicator(
@@ -370,13 +378,10 @@ private fun SourceLoginWebView(
                     .align(Alignment.TopCenter),
                     .align(Alignment.TopCenter),
             )
             )
         }
         }
-    }
-    DisposableEffect(Unit) {
-        onDispose { webView?.destroy() }
-    }
-    LaunchedEffect(state.checkingCookie) {
-        if (state.checkingCookie) {
-            state.webUrl?.let { webView?.loadUrl(it, state.headers) }
+        LaunchedEffect(state.checkingCookie) {
+            if (state.checkingCookie) {
+                state.webUrl?.let { webView?.loadUrl(it, state.headers) }
+            }
         }
         }
     }
     }
 }
 }

+ 3 - 0
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -100,6 +100,9 @@ open class MainActivity : BaseComposeActivity(), AudioPlay.CallBack,
         @Volatile
         @Volatile
         var hasActiveAudioPlayRoute: Boolean = false
         var hasActiveAudioPlayRoute: Boolean = false
 
 
+        @Volatile
+        var hasActiveSourceLoginRoute: Boolean = false
+
         fun createLauncherIntent(context: Context): Intent =
         fun createLauncherIntent(context: Context): Intent =
             MainIntent.createLauncherIntent(context)
             MainIntent.createLauncherIntent(context)
 
 

+ 6 - 0
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -194,6 +194,12 @@ fun MainActivity.mainEntryProvider(
     entry<MainRouteSourceLogin>(
     entry<MainRouteSourceLogin>(
         metadata = ModalOverlaySceneStrategy.modalOverlay(),
         metadata = ModalOverlaySceneStrategy.modalOverlay(),
     ) { route ->
     ) { route ->
+        DisposableEffect(route) {
+            MainActivity.hasActiveSourceLoginRoute = true
+            onDispose {
+                MainActivity.hasActiveSourceLoginRoute = false
+            }
+        }
         val viewModel = koinViewModel<SourceLoginViewModel>(
         val viewModel = koinViewModel<SourceLoginViewModel>(
             key = "SourceLogin:${route.type}:${route.sourceKey}:${route.bookUrl}",
             key = "SourceLogin:${route.type}:${route.sourceKey}:${route.bookUrl}",
         )
         )

+ 12 - 0
app/src/test/java/io/legado/app/JsTest.kt

@@ -132,4 +132,16 @@ class JsTest {
         Assert.assertEquals(result, "string")
         Assert.assertEquals(result, "string")
     }
     }
 
 
+    @Test
+    fun hutoolDigestUtilMd5Hex() {
+        // 书源 JS 通过 Packages.cn.hutool.crypto.digest.DigestUtil 直接调用 Hutool
+        // (如七猫书源的 qmMd5),Hutool 必须保留在运行时 classpath 且能被 Rhino 反射访问。
+        @Language("js")
+        val js = """
+            String(Packages.cn.hutool.crypto.digest.DigestUtil.md5Hex("abc"))
+        """.trimIndent()
+        val result = RhinoScriptEngine.eval(js, ScriptBindings())
+        Assert.assertEquals("900150983cd24fb0d6963f7d28e17f72", result)
+    }
+
 }
 }

+ 4 - 0
gradle/libs.versions.toml

@@ -62,6 +62,9 @@ palette = "1.0.0"
 preference = "1.2.1"
 preference = "1.2.1"
 protobufJavalite = "4.26.1"
 protobufJavalite = "4.26.1"
 quickChineseTransfer = "0.2.17"
 quickChineseTransfer = "0.2.17"
+# 书源 JS 通过 Packages.cn.hutool.* 直接调用,Hutool 作为书源运行时加密库保留在 classpath。
+# 应用代码不直接依赖它;加密走 JCA / help/crypto/CryptoUtils.kt。版本固定,勿升级(参考上游基线)。
+hutool = "5.8.22"
 room = "2.8.4"
 room = "2.8.4"
 robolectric = "4.16.1"
 robolectric = "4.16.1"
 splitties = "3.0.0"
 splitties = "3.0.0"
@@ -136,6 +139,7 @@ androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayo
 
 
 haze-core = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
 haze-core = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
 haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
 haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
+hutool-crypto = { module = "cn.hutool:hutool-crypto", version.ref = "hutool" }
 koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
 koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
 koin-core = { module = "io.insert-koin:koin-core" }
 koin-core = { module = "io.insert-koin:koin-core" }
 koin-android = { group = "io.insert-koin", name = "koin-android" }
 koin-android = { group = "io.insert-koin", name = "koin-android" }