Просмотр исходного кода

fix(rss): unify start pages with reader browser

HapeLee 4 месяцев назад
Родитель
Сommit
958dabca65

+ 50 - 2
app/src/main/java/io/legado/app/model/rss/Rss.kt

@@ -52,7 +52,31 @@ object Rss {
             coroutineContext = coroutineContext,
             hasLoginHeader = false
         )
-        val res = analyzeUrl.getStrResponseAwait()
+        val checkJs = rssSource.loginCheckJs
+        val res = kotlin.runCatching {
+            analyzeUrl.getStrResponseAwait().let {
+                if (!checkJs.isNullOrBlank()) {
+                    analyzeUrl.evalJS(checkJs, it) as StrResponse
+                } else {
+                    it
+                }
+            }
+        }.getOrElse { throwable ->
+            if (!checkJs.isNullOrBlank()) {
+                val errResponse = analyzeUrl.getErrStrResponse(throwable)
+                try {
+                    (analyzeUrl.evalJS(checkJs, errResponse) as StrResponse).also {
+                        if (it.code() == 500) {
+                            throw throwable
+                        }
+                    }
+                } catch (_: Throwable) {
+                    throw throwable
+                }
+            } else {
+                throw throwable
+            }
+        }
         checkRedirect(rssSource, res)
         return RssParserByRule.parseXML(sortName, sortUrl, res.url, res.body, rssSource, ruleData)
     }
@@ -82,7 +106,31 @@ object Rss {
             coroutineContext = coroutineContext,
             hasLoginHeader = false
         )
-        val res = analyzeUrl.getStrResponseAwait()
+        val checkJs = rssSource.loginCheckJs
+        val res = kotlin.runCatching {
+            analyzeUrl.getStrResponseAwait().let {
+                if (!checkJs.isNullOrBlank()) {
+                    analyzeUrl.evalJS(checkJs, it) as StrResponse
+                } else {
+                    it
+                }
+            }
+        }.getOrElse { throwable ->
+            if (!checkJs.isNullOrBlank()) {
+                val errResponse = analyzeUrl.getErrStrResponse(throwable)
+                try {
+                    (analyzeUrl.evalJS(checkJs, errResponse) as StrResponse).also {
+                        if (it.code() == 500) {
+                            throw throwable
+                        }
+                    }
+                } catch (_: Throwable) {
+                    throw throwable
+                }
+            } else {
+                throw throwable
+            }
+        }
         checkRedirect(rssSource, res)
         Debug.log(rssSource.sourceUrl, "≡获取成功:${rssSource.sourceUrl}")
         Debug.log(rssSource.sourceUrl, res.body ?: "", state = 20)

+ 13 - 3
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -151,13 +151,14 @@ fun MainActivity.mainEntryProvider(
                     )
                 )
             },
-            onNavigateToRssRead = { title, origin, link, openUrl ->
+            onNavigateToRssRead = { title, origin, link, openUrl, startPage ->
                 onNavigateToRoute(
                     MainRouteRssRead(
                         title = title,
                         origin = origin,
                         link = link,
-                        openUrl = openUrl
+                        openUrl = openUrl,
+                        startPage = startPage
                     )
                 )
             },
@@ -532,7 +533,16 @@ fun MainActivity.mainEntryProvider(
             origin = route.origin,
             link = route.link,
             openUrl = route.openUrl,
-            onBackClick = { onNavigateBack() }
+            startPage = route.startPage,
+            onBackClick = { onNavigateBack() },
+            onOpenArticles = { sortUrl ->
+                onNavigateToRoute(
+                    MainRouteRssSort(
+                        sourceUrl = route.origin,
+                        sortUrl = sortUrl
+                    )
+                )
+            }
         )
     }
 

+ 9 - 3
app/src/main/java/io/legado/app/ui/main/MainScreen.kt

@@ -127,7 +127,13 @@ fun MainScreen(
     onNavigateToBookInfo: (name: String, author: String, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit,
     onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
     onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
-    onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
+    onNavigateToRssRead: (
+        title: String?,
+        origin: String,
+        link: String?,
+        openUrl: String?,
+        startPage: Boolean
+    ) -> Unit,
     onNavigateToRssFavorites: () -> Unit,
     onNavigateToRuleSub: () -> Unit,
     onNavigateToReadRecord: () -> Unit,
@@ -469,8 +475,8 @@ fun MainScreen(
                                 onOpenSort = { sourceUrl, sortUrl, key ->
                                     onNavigateToRssSort(sourceUrl, sortUrl, key)
                                 },
-                                onOpenRead = { title, origin, link, openUrl ->
-                                    onNavigateToRssRead(title, origin, link, openUrl)
+                                onOpenRead = { title, origin, link, openUrl, startPage ->
+                                    onNavigateToRssRead(title, origin, link, openUrl, startPage)
                                 },
                                 onOpenFavorites = onNavigateToRssFavorites,
                                 onOpenRuleSub = onNavigateToRuleSub

+ 14 - 2
app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt

@@ -73,7 +73,13 @@ import org.koin.androidx.compose.koinViewModel
 fun RssScreen(
     viewModel: RssViewModel = koinViewModel(),
     onOpenSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
-    onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
+    onOpenRead: (
+        title: String?,
+        origin: String,
+        link: String?,
+        openUrl: String?,
+        startPage: Boolean
+    ) -> Unit,
     onOpenRuleSub: () -> Unit,
     onOpenFavorites: () -> Unit,
 ) {
@@ -97,7 +103,13 @@ fun RssScreen(
                 }
 
                 is RssEffect.OpenRead -> {
-                    currentOnOpenRead(effect.title, effect.origin, effect.link, effect.openUrl)
+                    currentOnOpenRead(
+                        effect.title,
+                        effect.origin,
+                        effect.link,
+                        effect.openUrl,
+                        effect.startPage
+                    )
                 }
 
                 is RssEffect.OpenExternalUrl -> {

+ 15 - 2
app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt

@@ -138,7 +138,19 @@ class RssViewModel(
 
     fun openSource(rssSource: RssSource) {
         if (!rssSource.singleUrl) {
-            _effects.tryEmit(RssEffect.OpenSort(rssSource.sourceUrl, null, null))
+            if (rssSource.startHtml.isNullOrBlank()) {
+                _effects.tryEmit(RssEffect.OpenSort(rssSource.sourceUrl, null, null))
+            } else {
+                _effects.tryEmit(
+                    RssEffect.OpenRead(
+                        title = rssSource.sourceName,
+                        origin = rssSource.sourceUrl,
+                        link = null,
+                        openUrl = null,
+                        startPage = true
+                    )
+                )
+            }
             return
         }
 
@@ -202,7 +214,8 @@ sealed interface RssEffect {
         val title: String?,
         val origin: String,
         val link: String?,
-        val openUrl: String?
+        val openUrl: String?,
+        val startPage: Boolean = false
     ) : RssEffect
 
     data class OpenExternalUrl(val url: String) : RssEffect

+ 0 - 274
app/src/main/java/io/legado/app/ui/rss/article/RssStartPage.kt

@@ -1,274 +0,0 @@
-package io.legado.app.ui.rss.article
-
-import android.annotation.SuppressLint
-import android.webkit.ConsoleMessage
-import android.webkit.WebChromeClient
-import android.webkit.WebResourceRequest
-import android.webkit.WebResourceResponse
-import android.webkit.WebSettings
-import android.webkit.WebView
-import android.webkit.WebViewClient
-import androidx.activity.compose.LocalActivity
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBars
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.runtime.Composable
-import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-import io.legado.app.data.entities.RssSource
-import io.legado.app.help.WebCacheManager
-import io.legado.app.help.config.AppConfig
-import io.legado.app.help.http.CookieManager
-import io.legado.app.help.webView.WebJsExtensions
-import java.io.ByteArrayInputStream
-
-@SuppressLint("SetJavaScriptEnabled")
-@Composable
-fun RssStartPage(
-    rssSource: RssSource?,
-    onNavigateToArticles: (sortUrl: String?) -> Unit,
-    modifier: Modifier = Modifier
-) {
-    val context = LocalContext.current
-    val activity = LocalActivity.current as? AppCompatActivity
-    var progress by remember { mutableIntStateOf(0) }
-    var isLoading by remember { mutableStateOf(true) }
-    var webViewRef by remember { mutableStateOf<WebView?>(null) }
-
-    val startHtml = rssSource?.startHtml
-    val startStyle = rssSource?.startStyle ?: rssSource?.style
-    val startJs = rssSource?.startJs
-    val preloadJs = rssSource?.preloadJs
-    val hasPreloadJs = !preloadJs.isNullOrBlank()
-
-    LaunchedEffect(startHtml) {
-        if (startHtml.isNullOrBlank()) {
-            onNavigateToArticles(null)
-        }
-    }
-
-    if (startHtml.isNullOrBlank()) {
-        return
-    }
-
-    DisposableEffect(Unit) {
-        onDispose {
-            webViewRef?.destroy()
-        }
-    }
-
-    Box(
-        modifier = modifier
-            .fillMaxSize()
-            .windowInsetsPadding(WindowInsets.statusBars)
-    ) {
-        if (isLoading && progress < 100) {
-            AppLinearProgressIndicator(
-                progress = progress / 100f,
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(top = 4.dp)
-            )
-        }
-
-        AndroidView(
-            factory = { ctx ->
-                WebView(ctx).apply {
-                    webViewRef = this
-
-                    settings.apply {
-                        javaScriptEnabled = true
-                        domStorageEnabled = true
-                        mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
-                        useWideViewPort = true
-                        loadWithOverviewMode = true
-                        userAgentString = AppConfig.userAgent
-                        cacheMode = if (rssSource?.cacheFirst == true) {
-                            WebSettings.LOAD_CACHE_ELSE_NETWORK
-                        } else {
-                            WebSettings.LOAD_DEFAULT
-                        }
-                    }
-
-                    CookieManager.applyToWebView(rssSource?.sourceUrl.orEmpty())
-
-                    if (hasPreloadJs && activity != null && rssSource != null) {
-                        val webJsExtensions = WebJsExtensions(
-                            rssSource,
-                            activity,
-                            this,
-                            callback = object : WebJsExtensions.Callback {
-                                override fun upConfig(config: String) {}
-                                override fun onNavigateToArticles(sortUrl: String?) {
-                                    activity.runOnUiThread {
-                                        onNavigateToArticles(sortUrl)
-                                    }
-                                }
-                            }
-                        )
-                        addJavascriptInterface(webJsExtensions, WebJsExtensions.nameJava)
-                        addJavascriptInterface(rssSource, WebJsExtensions.nameSource)
-                        addJavascriptInterface(WebCacheManager, WebJsExtensions.nameCache)
-                    }
-
-                    addJavascriptInterface(object {
-                        @android.webkit.JavascriptInterface
-                        fun navigateToArticles() {
-                            activity?.runOnUiThread {
-                                onNavigateToArticles(null)
-                            }
-                        }
-
-                        @android.webkit.JavascriptInterface
-                        fun getSourceUrl(): String {
-                            return rssSource?.sourceUrl.orEmpty()
-                        }
-
-                        @android.webkit.JavascriptInterface
-                        fun getSourceName(): String {
-                            return rssSource?.sourceName.orEmpty()
-                        }
-                    }, "AndroidBridge")
-
-                    var jsInjected = false
-
-                    webViewClient = object : WebViewClient() {
-                        override fun onPageFinished(view: WebView?, url: String?) {
-                            super.onPageFinished(view, url)
-                            isLoading = false
-                        }
-
-                        override fun shouldInterceptRequest(
-                            view: WebView?,
-                            request: WebResourceRequest
-                        ): WebResourceResponse? {
-                            val url = request.url.toString()
-                            if (!jsInjected && url == WebJsExtensions.nameUrl) {
-                                jsInjected = true
-                                val jsContent = buildString {
-                                    append("(() => {")
-                                    append(WebJsExtensions.JS_INJECTION)
-                                    if (!preloadJs.isNullOrBlank()) {
-                                        append("\n")
-                                        append(preloadJs)
-                                    }
-                                    append("\n})();")
-                                }
-                                return WebResourceResponse(
-                                    "text/javascript",
-                                    "utf-8",
-                                    ByteArrayInputStream(jsContent.toByteArray())
-                                )
-                            }
-                            return super.shouldInterceptRequest(view, request)
-                        }
-                    }
-
-                    webChromeClient = object : WebChromeClient() {
-                        override fun onProgressChanged(view: WebView?, newProgress: Int) {
-                            progress = newProgress
-                        }
-
-                        override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
-                            if (rssSource?.showWebLog == true) {
-                                consoleMessage?.message()?.let { message ->
-                                    io.legado.app.constant.AppLog.put("StartPage: $message")
-                                }
-                            }
-                            return true
-                        }
-                    }
-
-                    val htmlContent = buildStartPageHtml(startHtml, startStyle, startJs, hasPreloadJs)
-                    loadDataWithBaseURL(
-                        rssSource?.sourceUrl,
-                        htmlContent,
-                        "text/html",
-                        "utf-8",
-                        null
-                    )
-                }
-            },
-            modifier = Modifier.fillMaxSize()
-        )
-    }
-}
-
-private fun buildStartPageHtml(
-    html: String,
-    style: String?,
-    startJs: String?,
-    hasPreloadJs: Boolean
-): String {
-    val styleContent = style?.takeIf { it.isNotBlank() }?.let {
-        "<style>$it</style>"
-    } ?: "<style>img{max-width:100% !important; width:auto; height:auto;}body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}</style>"
-
-    val jsScript = startJs?.takeIf { it.isNotBlank() }?.let {
-        "<script>$it</script>"
-    } ?: ""
-
-    val preloadScript = if (hasPreloadJs) {
-        WebJsExtensions.JS_URL
-    } else {
-        ""
-    }
-
-    val htmlBuilder = StringBuilder()
-
-    val headIndex = html.indexOf("<head>")
-    if (headIndex >= 0) {
-        htmlBuilder.append(html, 0, headIndex + 6)
-        htmlBuilder.append(preloadScript)
-        htmlBuilder.append(html, headIndex + 6, html.length)
-    } else {
-        htmlBuilder.append("<head>")
-        htmlBuilder.append(preloadScript)
-        htmlBuilder.append("</head>")
-        htmlBuilder.append(html)
-    }
-
-    val result = htmlBuilder.toString()
-
-    val styleEndIndex = result.indexOf("</style>")
-    return if (styleEndIndex >= 0) {
-        val insertIndex = styleEndIndex + 8
-        buildString {
-            append(result, 0, insertIndex)
-            append(styleContent)
-            append(result, insertIndex, result.length)
-            append(jsScript)
-        }
-    } else {
-        val headEndIndex = result.indexOf("</head>")
-        if (headEndIndex >= 0) {
-            buildString {
-                append(result, 0, headEndIndex)
-                append(styleContent)
-                append(result, headEndIndex, result.length)
-                append(jsScript)
-            }
-        } else {
-            buildString {
-                append(styleContent)
-                append(result)
-                append(jsScript)
-            }
-        }
-    }
-}

+ 2 - 1
app/src/main/java/io/legado/app/ui/rss/read/MainRouteRssRead.kt

@@ -8,5 +8,6 @@ data class MainRouteRssRead(
     val title: String? = null,
     val origin: String,
     val link: String? = null,
-    val openUrl: String? = null
+    val openUrl: String? = null,
+    val startPage: Boolean = false
 ) : NavKey

+ 71 - 7
app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt

@@ -15,6 +15,7 @@ import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.TTS
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
+import io.legado.app.help.webView.WebJsExtensions
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.rss.Rss
 import io.legado.app.utils.ImageSaveUtils
@@ -30,7 +31,8 @@ data class ReadRssArgs(
     val title: String? = null,
     val origin: String,
     val link: String? = null,
-    val openUrl: String? = null
+    val openUrl: String? = null,
+    val startPage: Boolean = false
 )
 
 class ReadRssViewModel(application: Application) : BaseViewModel(application) {
@@ -38,6 +40,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
     var rssArticle: RssArticle? = null
     var tts: TTS? = null
     var headerMap: Map<String, String> = emptyMap()
+    private var isStartPage = false
 
     private val _contentState = MutableStateFlow<String?>(null)
     val contentState: StateFlow<String?> = _contentState.asStateFlow()
@@ -69,6 +72,11 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
             headerMap = runScriptWithContext {
                 rssSource?.getHeaderMap() ?: emptyMap()
             }
+            isStartPage = args.startPage
+            if (isStartPage) {
+                _contentState.value = resolveStartHtml()
+                return@execute
+            }
 
             val link = args.link
             if (!link.isNullOrBlank()) {
@@ -122,6 +130,24 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
         _urlState.value = analyzeUrl
     }
 
+    private suspend fun resolveStartHtml(): String {
+        val source = rssSource ?: return ""
+        val startHtml = source.startHtml ?: return ""
+        return when {
+            startHtml.startsWith("@js:") -> runScriptWithContext {
+                source.evalJS(startHtml.substring(4))?.toString().orEmpty()
+            }
+
+            startHtml.startsWith("<js>") -> runScriptWithContext {
+                source.evalJS(startHtml.substring(4, startHtml.lastIndexOf("<")))
+                    ?.toString()
+                    .orEmpty()
+            }
+
+            else -> startHtml
+        }
+    }
+
     private fun loadContent(rssArticle: RssArticle, ruleContent: String) {
         val source = rssSource ?: return
         Rss.getContent(viewModelScope, rssArticle, ruleContent, source)
@@ -217,18 +243,56 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
     }
 
     fun clHtml(content: String): String {
+        val contentWithPreloadJs = if (
+            isStartPage &&
+            !rssSource?.preloadJs.isNullOrBlank() &&
+            !content.contains(WebJsExtensions.JS_URL)
+        ) {
+            val headIndex = content.indexOf("<head>")
+            if (headIndex >= 0) {
+                buildString(content.length + WebJsExtensions.JS_URL.length) {
+                    append(content, 0, headIndex + 6)
+                    append(WebJsExtensions.JS_URL)
+                    append(content, headIndex + 6, content.length)
+                }
+            } else {
+                "<head>${WebJsExtensions.JS_URL}</head>$content"
+            }
+        } else {
+            content
+        }
+        val contentWithStartJs = if (isStartPage && !rssSource?.startJs.isNullOrBlank()) {
+            val startJs = rssSource?.startJs.orEmpty()
+            val bodyEndIndex = contentWithPreloadJs.indexOf("</body>")
+            if (bodyEndIndex >= 0) {
+                buildString(contentWithPreloadJs.length + startJs.length + 20) {
+                    append(contentWithPreloadJs, 0, bodyEndIndex)
+                    append("<script>$startJs</script>")
+                    append(contentWithPreloadJs, bodyEndIndex, contentWithPreloadJs.length)
+                }
+            } else {
+                "$contentWithPreloadJs<script>$startJs</script>"
+            }
+        } else {
+            contentWithPreloadJs
+        }
+        val style = if (isStartPage) {
+            rssSource?.startStyle ?: rssSource?.style
+        } else {
+            rssSource?.style
+        }
         return when {
-            !rssSource?.style.isNullOrEmpty() -> {
+            !style.isNullOrEmpty() -> {
                 """
                     <style>
-                        ${rssSource?.style}
+                        $style
                     </style>
-                    $content
+                    $contentWithStartJs
                 """.trimIndent()
             }
 
-            content.contains("<style>".toRegex()) -> {
-                content
+            contentWithStartJs.contains("<style>".toRegex()) -> {
+                contentWithStartJs
             }
 
             else -> {
@@ -238,7 +302,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
                         video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
                         body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
                     </style>
-                    $content
+                    $contentWithStartJs
                 """.trimIndent()
             }
         }

+ 57 - 46
app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt

@@ -93,7 +93,9 @@ fun RssReadRouteScreen(
     origin: String,
     link: String?,
     openUrl: String?,
+    startPage: Boolean,
     onBackClick: () -> Unit,
+    onOpenArticles: (sortUrl: String?) -> Unit,
     viewModel: ReadRssViewModel = koinViewModel()
 ) {
     val context = LocalContext.current
@@ -133,13 +135,14 @@ fun RssReadRouteScreen(
         activity?.toggleSystemBar(AppConfig.showStatusBar)
     }
 
-    LaunchedEffect(origin, link, openUrl, title) {
+    LaunchedEffect(origin, link, openUrl, title, startPage) {
         viewModel.initData(
             ReadRssArgs(
                 title = title,
                 origin = origin,
                 link = link,
-                openUrl = openUrl
+                openUrl = openUrl,
+                startPage = startPage
             )
         )
     }
@@ -152,8 +155,10 @@ fun RssReadRouteScreen(
         val body = content ?: return@LaunchedEffect
         val currentWebView = webView ?: return@LaunchedEffect
         currentWebView.settings.userAgentString = viewModel.headerMap[AppConst.UA_NAME] ?: fallbackUserAgent
-        val article = viewModel.rssArticle ?: return@LaunchedEffect
-        val url = NetworkUtils.getAbsoluteURL(article.origin, article.link)
+        val article = viewModel.rssArticle
+        val url = article?.let {
+            NetworkUtils.getAbsoluteURL(it.origin, it.link)
+        } ?: origin
         val html = viewModel.clHtml(body)
         if (currentWebView.url != url) {
             if (viewModel.rssSource?.loadWithBaseUrl == true) {
@@ -217,16 +222,18 @@ fun RssReadRouteScreen(
                             contentDescription = stringResource(R.string.refresh),
                             onClick = { viewModel.refresh { webView?.reload() } }
                         )
-                        TopBarActionButton(
-                            imageVector = Icons.Default.Star,
-                            contentDescription = stringResource(R.string.favorite),
-                            onClick = {
-                                viewModel.addFavorite()
-                                favoriteTitle = viewModel.rssArticle?.title.orEmpty()
-                                favoriteGroup = viewModel.rssArticle?.group.orEmpty()
-                                showFavoriteSheet = true
-                            }
-                        )
+                        if (!startPage) {
+                            TopBarActionButton(
+                                imageVector = Icons.Default.Star,
+                                contentDescription = stringResource(R.string.favorite),
+                                onClick = {
+                                    viewModel.addFavorite()
+                                    favoriteTitle = viewModel.rssArticle?.title.orEmpty()
+                                    favoriteGroup = viewModel.rssArticle?.group.orEmpty()
+                                    showFavoriteSheet = true
+                                }
+                            )
+                        }
                         Box {
                         TopBarActionButton(
                             imageVector = Icons.Default.MoreVert,
@@ -335,40 +342,44 @@ fun RssReadRouteScreen(
                     .fillMaxSize()
                     .padding(paddingValues)
             ) {
-                VisibleWebViewCompose(
-                    modifier = Modifier.fillMaxSize(),
-                    onCreated = { createdWebView ->
-                        webView = createdWebView
-                        configureRssReadWebView(
-                            webView = createdWebView,
-                            context = context,
-                            activity = activity,
-                            appCompatActivity = appCompatActivity,
-                            viewModel = viewModel,
-                            initialTitle = title,
-                            redirectPolicyProvider = { redirectPolicy },
-                            callbacks = RssReadWebControllerCallbacks(
-                                onProgressChanged = { webProgress = it },
-                                onPageTitleResolved = { resolved ->
-                                    pageTitle = resolved.ifBlank { defaultTopBarTitle }
-                                },
-                                onShowCustomView = { view, callback ->
-                                    if (view == null) {
-                                        callback?.onCustomViewHidden()
-                                    } else if (customView != null) {
-                                        callback?.onCustomViewHidden()
-                                    } else {
-                                        customView = view
-                                        customViewCallback = callback
-                                        activity?.keepScreenOn(true)
-                                        activity?.toggleSystemBar(false)
-                                    }
-                                },
-                                onHideCustomView = { hideCustomView() }
-                        )
+                if (!startPage || content != null) {
+                    VisibleWebViewCompose(
+                        modifier = Modifier.fillMaxSize(),
+                        onCreated = { createdWebView ->
+                            webView = createdWebView
+                            configureRssReadWebView(
+                                webView = createdWebView,
+                                context = context,
+                                activity = activity,
+                                appCompatActivity = appCompatActivity,
+                                viewModel = viewModel,
+                                initialTitle = title,
+                                isStartPage = startPage,
+                                redirectPolicyProvider = { redirectPolicy },
+                                callbacks = RssReadWebControllerCallbacks(
+                                    onProgressChanged = { webProgress = it },
+                                    onPageTitleResolved = { resolved ->
+                                        pageTitle = resolved.ifBlank { defaultTopBarTitle }
+                                    },
+                                    onShowCustomView = { view, callback ->
+                                        if (view == null) {
+                                            callback?.onCustomViewHidden()
+                                        } else if (customView != null) {
+                                            callback?.onCustomViewHidden()
+                                        } else {
+                                            customView = view
+                                            customViewCallback = callback
+                                            activity?.keepScreenOn(true)
+                                            activity?.toggleSystemBar(false)
+                                        }
+                                    },
+                                    onHideCustomView = { hideCustomView() },
+                                    navigateToArticles = onOpenArticles
+                                )
+                            )
+                        }
                     )
                 }
-                )
                 if (webProgress in 0..99) {
                     AppLinearProgressIndicator(
                         progress = webProgress / 100f,

+ 66 - 3
app/src/main/java/io/legado/app/ui/rss/read/RssReadWebController.kt

@@ -15,6 +15,7 @@ import android.webkit.SslErrorHandler
 import android.webkit.URLUtil
 import android.webkit.WebChromeClient
 import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
 import android.webkit.WebSettings
 import android.webkit.WebView
 import android.webkit.WebViewClient
@@ -27,7 +28,9 @@ import com.script.rhino.runScriptWithContext
 import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
+import io.legado.app.help.WebCacheManager
 import io.legado.app.help.config.AppConfig
+import io.legado.app.help.webView.WebJsExtensions
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.model.Download
@@ -38,14 +41,22 @@ import io.legado.app.utils.openUrl
 import io.legado.app.utils.setDarkeningAllowed
 import io.legado.app.utils.startActivity
 import io.legado.app.utils.toastOnUi
+import java.io.ByteArrayInputStream
 import java.net.URLDecoder
 
 internal data class RssReadWebControllerCallbacks(
     val onProgressChanged: (Int) -> Unit,
     val onPageTitleResolved: (String) -> Unit,
     val onShowCustomView: (View?, WebChromeClient.CustomViewCallback?) -> Unit,
-    val onHideCustomView: () -> Unit
-)
+    val onHideCustomView: () -> Unit,
+    val navigateToArticles: (String?) -> Unit
+) : WebJsExtensions.Callback {
+    override fun upConfig(config: String) = Unit
+
+    override fun onNavigateToArticles(sortUrl: String?) {
+        navigateToArticles(sortUrl)
+    }
+}
 
 @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
 internal fun configureRssReadWebView(
@@ -55,6 +66,7 @@ internal fun configureRssReadWebView(
     appCompatActivity: AppCompatActivity?,
     viewModel: ReadRssViewModel,
     initialTitle: String?,
+    isStartPage: Boolean,
     redirectPolicyProvider: () -> RedirectPolicy,
     callbacks: RssReadWebControllerCallbacks
 ) {
@@ -78,6 +90,7 @@ internal fun configureRssReadWebView(
 
     webView.webViewClient = object : WebViewClient() {
         private var lastUrl: String? = null
+        private var preloadJsInjected = false
 
         override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
             val targetUri = request.url
@@ -105,6 +118,35 @@ internal fun configureRssReadWebView(
             return handleCustomScheme(targetUri)
         }
 
+        override fun shouldInterceptRequest(
+            view: WebView?,
+            request: WebResourceRequest
+        ): WebResourceResponse? {
+            val source = viewModel.rssSource
+            val preloadJs = source?.preloadJs
+            if (
+                isStartPage &&
+                !preloadJsInjected &&
+                !preloadJs.isNullOrBlank() &&
+                request.url.toString() == WebJsExtensions.nameUrl
+            ) {
+                preloadJsInjected = true
+                val jsContent = buildString {
+                    append("(() => {")
+                    append(WebJsExtensions.JS_INJECTION)
+                    append("\n")
+                    append(preloadJs)
+                    append("\n})();")
+                }
+                return WebResourceResponse(
+                    "text/javascript",
+                    "utf-8",
+                    ByteArrayInputStream(jsContent.toByteArray())
+                )
+            }
+            return super.shouldInterceptRequest(view, request)
+        }
+
         private fun handleRedirect(view: WebView, fromUrl: String?, toUrl: String): Boolean {
             val fromHost = fromUrl?.toUri()?.host
             val toHost = toUrl.toUri().host
@@ -244,8 +286,29 @@ internal fun configureRssReadWebView(
         userAgentString = viewModel.headerMap[AppConst.UA_NAME] ?: AppConfig.userAgent
         viewModel.rssSource?.let { source ->
             javaScriptEnabled = source.enableJs
+            cacheMode = if (source.cacheFirst == true) {
+                WebSettings.LOAD_CACHE_ELSE_NETWORK
+            } else {
+                WebSettings.LOAD_DEFAULT
+            }
         }
-        cacheMode = WebSettings.LOAD_DEFAULT
+    }
+
+    val source = viewModel.rssSource
+    if (
+        isStartPage &&
+        !source?.preloadJs.isNullOrBlank() &&
+        appCompatActivity != null
+    ) {
+        val webJsExtensions = WebJsExtensions(
+            source,
+            appCompatActivity,
+            webView,
+            callback = callbacks
+        )
+        webView.addJavascriptInterface(webJsExtensions, WebJsExtensions.nameJava)
+        webView.addJavascriptInterface(source, WebJsExtensions.nameSource)
+        webView.addJavascriptInterface(WebCacheManager, WebJsExtensions.nameCache)
     }
 
     webView.addJavascriptInterface(object {