Răsfoiți Sursa

[优化] 修复订阅源视频播放问题和新增源编辑

HapeLee 6 luni în urmă
părinte
comite
a191d80e02

+ 12 - 1
app/src/main/java/io/legado/app/base/BaseRuleViewModel.kt

@@ -301,4 +301,15 @@ abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiS
         _importState.value = currentState.copy(items = newItems)
     }
 
-}
+    fun updateImportItem(index: Int, data: Entity) {
+        val currentState = _importState.value as? BaseImportUiState.Success<Entity> ?: return
+        if (index !in currentState.items.indices) return
+        val newItems = currentState.items.toMutableList()
+        newItems[index] = newItems[index].copy(data = data)
+        _importState.value = currentState.copy(
+            items = newItems,
+            version = currentState.version + 1
+        )
+    }
+
+}

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt

@@ -191,6 +191,7 @@ fun TxtRuleScreen(
         onDismissRequest = { viewModel.cancelImport() },
         onToggleItem = { viewModel.toggleImportSelection(it) },
         onToggleAll = { viewModel.toggleImportAll(it) },
+        onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
         onConfirm = { viewModel.saveImportedRules() },
         itemTitle = { rule -> rule.name },
         itemSubtitle = { rule ->

+ 1 - 0
app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt

@@ -182,6 +182,7 @@ fun DictRuleScreen(
         onDismissRequest = { viewModel.cancelImport() },
         onToggleItem = { viewModel.toggleImportSelection(it) },
         onToggleAll = { viewModel.toggleImportAll(it) },
+        onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
         onConfirm = { viewModel.saveImportedRules() },
         itemTitle = { rule -> rule.name },
         itemSubtitle = { rule ->

+ 1 - 0
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt

@@ -188,6 +188,7 @@ fun ReplaceRuleScreen(
         onDismissRequest = { viewModel.cancelImport() },
         onToggleItem = { viewModel.toggleImportSelection(it) },
         onToggleAll = { viewModel.toggleImportAll(it) },
+        onUpdateItem = { index, rule -> viewModel.updateImportItem(index, rule) },
         onConfirm = { viewModel.saveImportedRules() },
         topBarActions = {},
         itemTitle = { rule -> rule.name },

+ 9 - 3
app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt

@@ -59,6 +59,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.RssArticle
 import io.legado.app.data.entities.RssSource
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
@@ -92,6 +93,7 @@ fun RssArticlesPage(
     rssSource: RssSource?,
     viewModel: RssArticlesViewModel,
     onRead: (RssArticle) -> Unit,
+    paddingValues: PaddingValues = PaddingValues(0.dp),
     modifier: Modifier = Modifier
 ) {
     val context = LocalContext.current
@@ -123,6 +125,10 @@ fun RssArticlesPage(
     }
 
     val refreshState = rememberPullToRefreshState()
+    val contentPadding = adaptiveContentPadding(
+        top = paddingValues.calculateTopPadding(),
+        bottom = 120.dp
+    )
 
     PullToRefreshBox(
         isRefreshing = loadState.isRefreshing,
@@ -141,7 +147,7 @@ fun RssArticlesPage(
                 LazyColumn(
                     state = listState,
                     modifier = Modifier.fillMaxSize(),
-                    contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 72.dp),
+                    contentPadding = contentPadding,
                     verticalArrangement = Arrangement.spacedBy(8.dp)
                 ) {
                     items(
@@ -174,7 +180,7 @@ fun RssArticlesPage(
                     columns = GridCells.Fixed(2),
                     state = gridState,
                     modifier = Modifier.fillMaxSize(),
-                    contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 72.dp),
+                    contentPadding = contentPadding,
                     horizontalArrangement = Arrangement.spacedBy(8.dp),
                     verticalArrangement = Arrangement.spacedBy(8.dp)
                 ) {
@@ -208,7 +214,7 @@ fun RssArticlesPage(
                     columns = StaggeredGridCells.Fixed(2),
                     state = staggeredState,
                     modifier = Modifier.fillMaxSize(),
-                    contentPadding = PaddingValues(start = 8.dp, end = 8.dp, bottom = 72.dp),
+                    contentPadding = contentPadding,
                     horizontalArrangement = Arrangement.spacedBy(8.dp),
                     verticalItemSpacing = 8.dp
                 ) {

+ 2 - 1
app/src/main/java/io/legado/app/ui/rss/article/RssSortRouteScreen.kt

@@ -156,7 +156,7 @@ fun RssSortRouteScreen(
             }
             context.toastOnUi("重定向策略已更新")
         },
-        pagerContent = { _, sort ->
+        pagerContent = { _, sort, paddingValues ->
             val pageViewModel: RssArticlesViewModel = koinViewModel(
                 key = "rss_${viewModel.url}_${sort.first}_${sort.second}"
             )
@@ -167,6 +167,7 @@ fun RssSortRouteScreen(
                 rssUrl = viewModel.url,
                 rssSource = viewModel.rssSource,
                 viewModel = pageViewModel,
+                paddingValues = paddingValues,
                 onRead = { article ->
                     viewModel.read(article)
                     onOpenRead(article.title, article.origin, article.link, null)

+ 3 - 6
app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt

@@ -89,7 +89,7 @@ fun RssSortScreen(
     onOpenReadRecord: (RssReadRecord) -> Unit,
     onClearArticles: () -> Unit,
     onRedirectPolicyChanged: (RedirectPolicy) -> Unit,
-    pagerContent: @Composable (index: Int, item: Pair<String, String>) -> Unit
+    pagerContent: @Composable (index: Int, item: Pair<String, String>, paddingValues: PaddingValues) -> Unit
 ) {
     val scope = rememberCoroutineScope()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
@@ -291,12 +291,10 @@ fun RssSortScreen(
             HorizontalPager(
                 state = pagerState,
                 contentPadding = PaddingValues(bottom = 0.dp),
-                modifier = Modifier
-                    .fillMaxSize()
-                    .padding(paddingValues)
+                modifier = Modifier.fillMaxSize()
             ) { page ->
                 val item = sortList.getOrNull(page) ?: return@HorizontalPager
-                pagerContent(page, item)
+                pagerContent(page, item, paddingValues)
             }
         }
     }
@@ -370,4 +368,3 @@ private fun RssReadRecordSheet(
         }
     }
 }
-

+ 208 - 142
app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt

@@ -7,6 +7,7 @@ import android.net.Uri
 import android.net.http.SslError
 import android.os.SystemClock
 import android.view.View
+import android.view.ViewGroup
 import android.webkit.JavascriptInterface
 import android.webkit.SslErrorHandler
 import android.webkit.URLUtil
@@ -19,10 +20,13 @@ import androidx.activity.compose.BackHandler
 import androidx.activity.compose.LocalActivity
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.clickable
 import androidx.compose.material.icons.Icons
@@ -43,17 +47,22 @@ import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBar
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.zIndex
 import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
@@ -61,6 +70,7 @@ 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.config.AppConfig
 import io.legado.app.help.http.CookieManager
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.selector
@@ -69,6 +79,7 @@ import io.legado.app.ui.association.OnLineImportActivity
 import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
 import io.legado.app.ui.widget.components.button.MediumIconButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.button.SmallIconButton
@@ -87,6 +98,7 @@ import io.legado.app.utils.setDarkeningAllowed
 import io.legado.app.utils.share
 import io.legado.app.utils.startActivity
 import io.legado.app.utils.toastOnUi
+import io.legado.app.utils.toggleSystemBar
 import org.apache.commons.text.StringEscapeUtils
 import org.jsoup.Jsoup
 import java.net.URLDecoder
@@ -115,6 +127,10 @@ fun RssReadRouteScreen(
     var showMenu by remember { mutableStateOf(false) }
     var showRedirectMenu by remember { mutableStateOf(false) }
     var webView by remember { mutableStateOf<WebView?>(null) }
+    var customView by remember { mutableStateOf<View?>(null) }
+    var customViewCallback by remember {
+        mutableStateOf<WebChromeClient.CustomViewCallback?>(null)
+    }
 
     var redirectPolicy by remember { mutableStateOf(RedirectPolicy.ALLOW_ALL) }
 
@@ -128,6 +144,15 @@ fun RssReadRouteScreen(
     val isSpeaking by viewModel.isSpeakingState.collectAsStateWithLifecycle()
     val fallbackUserAgent = OtherConfig.userAgent
 
+    fun hideCustomView() {
+        val currentCustomView = customView ?: return
+        (currentCustomView.parent as? ViewGroup)?.removeView(currentCustomView)
+        customView = null
+        customViewCallback = null
+        activity?.keepScreenOn(false)
+        activity?.toggleSystemBar(AppConfig.showStatusBar)
+    }
+
     LaunchedEffect(origin, link, openUrl, title) {
         viewModel.initData(
             ReadRssArgs(
@@ -167,169 +192,207 @@ fun RssReadRouteScreen(
 
     BackHandler {
         when {
+            customView != null -> customViewCallback?.onCustomViewHidden() ?: hideCustomView()
             webView?.canGoBack() == true && (webView?.copyBackForwardList()?.size ?: 0) > 1 -> webView?.goBack()
             else -> onBackClick()
         }
     }
 
-    Scaffold(
-        topBar = {
-            TopAppBar(
-                title = {
-                    Text(
-                        text = pageTitle.ifBlank { defaultTopBarTitle },
-                        maxLines = 1,
-                        overflow = TextOverflow.Ellipsis
-                    )
-                },
-                navigationIcon = {
-                    TopBarNavigationButton(onClick = onBackClick)
-                },
-                actions = {
-                    TopBarActionButton(
-                        imageVector = Icons.Default.Refresh,
-                        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
-                        }
-                    )
-                    Box {
+    DisposableEffect(Unit) {
+        onDispose {
+            customViewCallback?.onCustomViewHidden()
+            hideCustomView()
+        }
+    }
+
+    Box(modifier = Modifier.fillMaxSize()) {
+        Scaffold(
+            topBar = {
+                TopAppBar(
+                    title = {
+                        Text(
+                            text = pageTitle.ifBlank { defaultTopBarTitle },
+                            maxLines = 1,
+                            overflow = TextOverflow.Ellipsis
+                        )
+                    },
+                    navigationIcon = {
+                        TopBarNavigationButton(onClick = onBackClick)
+                    },
+                    actions = {
                         TopBarActionButton(
-                            imageVector = Icons.Default.MoreVert,
-                            contentDescription = "Menu",
-                            onClick = { showMenu = true }
+                            imageVector = Icons.Default.Refresh,
+                            contentDescription = stringResource(R.string.refresh),
+                            onClick = { viewModel.refresh { webView?.reload() } }
                         )
-                        RoundDropdownMenu(
-                            expanded = showMenu,
-                            onDismissRequest = { showMenu = false }
-                        ) { dismiss ->
-                            RoundDropdownMenuItem(
-                                text = stringResource(R.string.share),
-                                leadingIcon = { MenuItemIcon(Icons.Default.Share) },
-                                onClick = {
-                                    dismiss()
-                                    webView?.url?.let {
-                                        context.share(it)
-                                    } ?: viewModel.rssArticle?.let {
-                                        context.share(it.link)
-                                    } ?: context.toastOnUi(R.string.null_url)
-                                }
+                        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,
+                                contentDescription = "Menu",
+                                onClick = { showMenu = true }
                             )
-                            RoundDropdownMenuItem(
-                                text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
-                                    R.string.read_aloud
-                                ),
-                                leadingIcon = {
-                                    MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
-                                },
-                                onClick = {
-                                    dismiss()
-                                    if (isSpeaking) {
-                                        viewModel.stopReadAloud()
-                                    } else {
-                                        webView?.evaluateJavascript("document.documentElement.outerHTML") {
-                                            val html = StringEscapeUtils.unescapeJson(it)
-                                                .replace("^\"|\"$".toRegex(), "")
-                                            viewModel.readAloud(
-                                                Jsoup.parse(html).text()
-                                            )
-                                        }
+                            RoundDropdownMenu(
+                                expanded = showMenu,
+                                onDismissRequest = { showMenu = false }
+                            ) { dismiss ->
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.share),
+                                    leadingIcon = { MenuItemIcon(Icons.Default.Share) },
+                                    onClick = {
+                                        dismiss()
+                                        webView?.url?.let {
+                                            context.share(it)
+                                        } ?: viewModel.rssArticle?.let {
+                                            context.share(it.link)
+                                        } ?: context.toastOnUi(R.string.null_url)
                                     }
-                                }
-                            )
-                            if (!viewModel.rssSource?.loginUrl.isNullOrBlank()) {
+                                )
                                 RoundDropdownMenuItem(
-                                    text = stringResource(R.string.login),
-                                    leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
+                                    text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
+                                        R.string.read_aloud
+                                    ),
+                                    leadingIcon = {
+                                        MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
+                                    },
                                     onClick = {
                                         dismiss()
-                                        context.startActivity<SourceLoginActivity> {
-                                            putExtra("type", "rssSource")
-                                            putExtra("key", viewModel.rssSource?.sourceUrl)
+                                        if (isSpeaking) {
+                                            viewModel.stopReadAloud()
+                                        } else {
+                                            webView?.evaluateJavascript("document.documentElement.outerHTML") {
+                                                val html = StringEscapeUtils.unescapeJson(it)
+                                                    .replace("^\"|\"$".toRegex(), "")
+                                                viewModel.readAloud(
+                                                    Jsoup.parse(html).text()
+                                                )
+                                            }
                                         }
                                     }
                                 )
-                            }
-                            RoundDropdownMenuItem(
-                                text = stringResource(R.string.redirect_policy),
-                                onClick = { showRedirectMenu = true }
-                            )
-                            RoundDropdownMenuItem(
-                                text = stringResource(R.string.open_in_browser),
-                                leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
-                                onClick = {
-                                    dismiss()
-                                    webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
+                                if (!viewModel.rssSource?.loginUrl.isNullOrBlank()) {
+                                    RoundDropdownMenuItem(
+                                        text = stringResource(R.string.login),
+                                        leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
+                                        onClick = {
+                                            dismiss()
+                                            context.startActivity<SourceLoginActivity> {
+                                                putExtra("type", "rssSource")
+                                                putExtra("key", viewModel.rssSource?.sourceUrl)
+                                            }
+                                        }
+                                    )
                                 }
-                            )
-                        }
-                        RoundDropdownMenu(
-                            expanded = showRedirectMenu,
-                            onDismissRequest = { showRedirectMenu = false }
-                        ) { dismiss ->
-                            RedirectPolicy.entries.forEach { policy ->
                                 RoundDropdownMenuItem(
-                                    text = policy.title(),
+                                    text = stringResource(R.string.redirect_policy),
+                                    onClick = { showRedirectMenu = true }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.open_in_browser),
+                                    leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
                                     onClick = {
                                         dismiss()
-                                        showMenu = false
-                                        viewModel.rssSource?.let { source ->
-                                            viewModel.updateRssSourceRedirectPolicy(source.sourceUrl, policy.name)
-                                            redirectPolicy = policy
-                                        }
-                                        context.toastOnUi("重定向策略已更新")
-                                    },
-                                    trailingIcon = {
-                                        if (policy == redirectPolicy) {
-                                            Icon(Icons.Default.Check, null)
-                                        }
+                                        webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
                                     }
                                 )
                             }
+                            RoundDropdownMenu(
+                                expanded = showRedirectMenu,
+                                onDismissRequest = { showRedirectMenu = false }
+                            ) { dismiss ->
+                                RedirectPolicy.entries.forEach { policy ->
+                                    RoundDropdownMenuItem(
+                                        text = policy.title(),
+                                        onClick = {
+                                            dismiss()
+                                            showMenu = false
+                                            viewModel.rssSource?.let { source ->
+                                                viewModel.updateRssSourceRedirectPolicy(source.sourceUrl, policy.name)
+                                                redirectPolicy = policy
+                                            }
+                                            context.toastOnUi("重定向策略已更新")
+                                        },
+                                        trailingIcon = {
+                                            if (policy == redirectPolicy) {
+                                                Icon(Icons.Default.Check, null)
+                                            }
+                                        }
+                                    )
+                                }
+                            }
                         }
                     }
-                }
-            )
-        }
-    ) { paddingValues ->
-        Box(
-            modifier = Modifier
-                .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 }
-                            }
+                )
+            }
+        ) { paddingValues ->
+            Box(
+                modifier = Modifier
+                    .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 (webProgress in 0..99) {
+                    LinearProgressIndicator(
+                        progress = { webProgress / 100f },
+                        modifier = Modifier.fillMaxWidth()
                     )
                 }
-            )
-            if (webProgress in 0..99) {
-                LinearProgressIndicator(
-                    progress = { webProgress / 100f },
-                    modifier = Modifier.fillMaxWidth()
+            }
+        }
+
+        customView?.let { view ->
+            key(view) {
+                AndroidView(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .background(Color.Black)
+                        .zIndex(1f),
+                    factory = {
+                        (view.parent as? ViewGroup)?.removeView(view)
+                        view
+                    }
                 )
             }
         }
@@ -377,7 +440,7 @@ private fun FavoriteEditSheet(
         }
     ) {
 
-        Column(modifier = Modifier.padding(12.dp)) {
+        Column {
             AppTextField(
                 value = titleValue,
                 onValueChange = onTitleChange,
@@ -396,11 +459,14 @@ private fun FavoriteEditSheet(
             )
         }
 
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.action_save),
-            leadingIcon = { MenuItemIcon(Icons.Default.Check) },
-            onClick = onSave,
-            modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
+        Spacer(modifier = Modifier.height(16.dp))
+
+        ConfirmDismissButtonsRow(
+            modifier = Modifier.fillMaxWidth(),
+            onDismiss = onDismissRequest,
+            onConfirm = onSave,
+            dismissText = stringResource(R.string.cancel),
+            confirmText = stringResource(R.string.action_save)
         )
     }
 }

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

@@ -5,6 +5,7 @@ package io.legado.app.ui.rss.read
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.Context
+import android.graphics.Bitmap
 import android.net.Uri
 import android.net.http.SslError
 import android.os.SystemClock
@@ -19,10 +20,12 @@ import android.webkit.WebView
 import android.webkit.WebViewClient
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.graphics.createBitmap
 import androidx.core.net.toUri
 import androidx.lifecycle.lifecycleScope
 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.config.AppConfig
 import io.legado.app.lib.dialogs.SelectItem
@@ -39,7 +42,9 @@ import java.net.URLDecoder
 
 internal data class RssReadWebControllerCallbacks(
     val onProgressChanged: (Int) -> Unit,
-    val onPageTitleResolved: (String) -> Unit
+    val onPageTitleResolved: (String) -> Unit,
+    val onShowCustomView: (View?, WebChromeClient.CustomViewCallback?) -> Unit,
+    val onHideCustomView: () -> Unit
 )
 
 @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
@@ -54,9 +59,21 @@ internal fun configureRssReadWebView(
     callbacks: RssReadWebControllerCallbacks
 ) {
     webView.webChromeClient = object : WebChromeClient() {
+        override fun getDefaultVideoPoster(): Bitmap {
+            return super.getDefaultVideoPoster() ?: createBitmap(100, 100)
+        }
+
         override fun onProgressChanged(view: WebView?, newProgress: Int) {
             callbacks.onProgressChanged(newProgress)
         }
+
+        override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
+            callbacks.onShowCustomView(view, callback)
+        }
+
+        override fun onHideCustomView() {
+            callbacks.onHideCustomView()
+        }
     }
 
     webView.webViewClient = object : WebViewClient() {
@@ -218,13 +235,17 @@ internal fun configureRssReadWebView(
     webView.settings.apply {
         mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
         domStorageEnabled = true
+        mediaPlaybackRequiresUserGesture = false
         allowContentAccess = true
         builtInZoomControls = true
         displayZoomControls = false
+        textZoom = 100
         setDarkeningAllowed(AppConfig.isNightTheme)
-        if (viewModel.rssSource?.enableJs == true) {
-            javaScriptEnabled = true
+        userAgentString = viewModel.headerMap[AppConst.UA_NAME] ?: AppConfig.userAgent
+        viewModel.rssSource?.let { source ->
+            javaScriptEnabled = source.enableJs
         }
+        cacheMode = WebSettings.LOAD_DEFAULT
     }
 
     webView.addJavascriptInterface(object {

+ 20 - 6
app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt

@@ -6,8 +6,10 @@ import android.util.AttributeSet
 import android.view.ActionMode
 import android.view.Menu
 import android.view.MenuItem
+import android.view.ViewGroup
 import android.webkit.JavascriptInterface
 import android.webkit.WebView
+import android.widget.FrameLayout
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
@@ -144,19 +146,31 @@ fun VisibleWebViewCompose(
     AndroidView(
         modifier = modifier,
         factory = { context ->
-            VisibleWebView(context).also {
-                webViewHolder.webView = it
-                onCreated(it)
+            FrameLayout(context).apply {
+                clipChildren = false
+                clipToPadding = false
+                val webView = VisibleWebView(context).apply {
+                    layoutParams = FrameLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT
+                    )
+                }
+                addView(webView)
+                webViewHolder.webView = webView
+                onCreated(webView)
             }
         },
-        update = {
-            webViewHolder.webView = it
+        update = { container ->
+            webViewHolder.webView = container.getChildAt(0) as? VisibleWebView
         }
     )
     DisposableEffect(Unit) {
         onDispose {
             onDestroyed?.invoke()
-            webViewHolder.webView?.destroy()
+            webViewHolder.webView?.let { webView ->
+                (webView.parent as? ViewGroup)?.removeView(webView)
+                webView.destroy()
+            }
             webViewHolder.webView = null
         }
     }

+ 5 - 4
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceScreen.kt

@@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.data.entities.RssSource
+import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.GroupManageBottomSheet
@@ -55,7 +56,6 @@ import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
-import io.legado.app.ui.widget.components.text.AppText
 import org.koin.androidx.compose.koinViewModel
 import sh.calvin.reorderable.rememberReorderableLazyListState
 
@@ -200,6 +200,7 @@ fun RssSourceScreen(
         onDismissRequest = { viewModel.cancelImport() },
         onToggleItem = { viewModel.toggleImportSelection(it) },
         onToggleAll = { viewModel.toggleImportAll(it) },
+        onUpdateItem = { index, source -> viewModel.updateImportItem(index, source) },
         onConfirm = { viewModel.saveImportedRules() },
         itemTitle = { rule -> rule.sourceName },
         itemSubtitle = { rule ->
@@ -344,9 +345,9 @@ fun RssSourceScreen(
             FastScrollLazyColumn(
                 modifier = Modifier.fillMaxSize(),
                 state = listState,
-                contentPadding = PaddingValues(
-                    top = paddingValues.calculateTopPadding() + 8.dp,
-                    bottom = paddingValues.calculateBottomPadding() + 80.dp
+                contentPadding = adaptiveContentPadding(
+                    top = paddingValues.calculateTopPadding(),
+                    bottom = paddingValues.calculateBottomPadding() + 120.dp
                 ),
                 verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {

+ 221 - 112
app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.widget.components.importComponents
 
 import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
@@ -17,43 +18,38 @@ import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.material.icons.filled.SelectAll
 import androidx.compose.material3.AssistChip
-import androidx.compose.material3.Button
-import androidx.compose.material3.CenterAlignedTopAppBar
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.OutlinedToggleButton
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import com.google.gson.JsonPrimitive
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
 import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.text.AnimatedText
+import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.text.AppText
-import kotlinx.coroutines.launch
+import io.legado.app.utils.GSON
 
 @Composable
 fun SourceInputDialog(
@@ -117,6 +113,7 @@ fun <T> BatchImportDialog(
     onToggleItem: (index: Int) -> Unit,
     onToggleAll: (isSelected: Boolean) -> Unit,
     onItemInfoClick: (index: Int) -> Unit = {},
+    onUpdateItem: (index: Int, data: T) -> Unit = { _, _ -> },
     topBarActions: @Composable RowScope.() -> Unit = {},
     itemTitle: (data: T) -> String,
     itemSubtitle: (data: T) -> String? = { null }
@@ -130,81 +127,187 @@ fun <T> BatchImportDialog(
 
     if (!show && cachedState == null) return
 
-    val scope = rememberCoroutineScope()
-    val snackbarHostState = remember { SnackbarHostState() }
-
     val currentState = cachedState!!
+    var editingIndex by remember(currentState.source) { mutableStateOf<Int?>(null) }
+    val editingItem = editingIndex?.let { currentState.items.getOrNull(it) }
+    val isEditing = editingItem != null
     val selectedCount = currentState.items.count { it.isSelected }
     val totalCount = currentState.items.size
+    val allSelected = selectedCount == totalCount
+    val sheetTitle = when {
+        isEditing -> itemTitle(editingItem.data)
+        selectedCount > 0 -> {
+            stringResource(
+                R.string.select_count,
+                selectedCount,
+                totalCount
+            )
+        }
+
+        else -> title
+    }
 
     AppModalBottomSheet(
         show = show,
-        onDismissRequest = onDismissRequest
-    ) {
-        AppScaffold(
-            modifier = Modifier
-                .fillMaxWidth()
-                .heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
-            topBar = {
-                CenterAlignedTopAppBar(
-                    title = {
-                        AnimatedText(
-                            if (selectedCount > 0)
-                                stringResource(
-                                    R.string.select_count,
-                                    selectedCount,
-                                    totalCount
-                                )
-                            else
-                                title
-                        )
-                    },
-                    actions = topBarActions,
-                    colors = TopAppBarDefaults.topAppBarColors(
-                        containerColor = Color.Transparent
-                    )
-                )
-            },
-            snackbarHost = { SnackbarHost(snackbarHostState) },
-            bottomBar = {
-                ImportBottomBar(
-                    selectedCount = selectedCount,
-                    totalCount = totalCount,
-                    onToggleSelectAll = onToggleAll,
-                    onConfirm = {
-                        val selectedData =
-                            currentState.items.filter { it.isSelected }.map { it.data }
-                        onConfirm(selectedData)
-                    },
-                    onCancel = onDismissRequest
+        onDismissRequest = onDismissRequest,
+        modifier = Modifier.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
+        title = sheetTitle,
+        startAction = if (isEditing) {
+            {
+                SmallIconButton(
+                    onClick = { editingIndex = null },
+                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                    contentDescription = "返回"
                 )
             }
-        ) { padding ->
-            LazyColumn(
-                modifier = Modifier.padding(padding),
-                contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
-                verticalArrangement = Arrangement.spacedBy(8.dp)
-            ) {
-                itemsIndexed(
-                    currentState.items,
-                    key = { _, item -> item.data.hashCode() }) { index, itemWrapper ->
-                    ImportItemRow(
-                        title = itemTitle(itemWrapper.data),
-                        subtitle = itemSubtitle(itemWrapper.data),
-                        isSelected = itemWrapper.isSelected,
-                        status = itemWrapper.status,
-                        onClick = { onToggleItem(index) },
-                        onInfoClick = {
-                            scope.launch { snackbarHostState.showSnackbar("其实还没写桀桀桀") }
-                            onItemInfoClick(index)
-                        }
+        } else {
+            null
+        },
+        endAction = if (!isEditing) {
+            {
+                Row {
+                    topBarActions()
+                    SmallIconButton(
+                        onClick = { onToggleAll(!allSelected) },
+                        imageVector = Icons.Default.SelectAll,
+                        contentDescription = if (allSelected) "全不选" else "全选"
                     )
                 }
             }
+        } else {
+            null
+        }
+    ) {
+        if (isEditing) {
+            BatchImportJsonEditContent(
+                data = editingItem.data,
+                version = currentState.version
+            ) { data ->
+                editingIndex?.let { onUpdateItem(it, data) }
+            }
+        } else {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.58f)
+            ) {
+                LazyColumn(
+                    contentPadding = PaddingValues(vertical = 8.dp),
+                    verticalArrangement = Arrangement.spacedBy(8.dp)
+                ) {
+                    itemsIndexed(
+                        currentState.items,
+                        key = { _, item -> item.data.hashCode() }
+                    ) { index, itemWrapper ->
+                        ImportItemRow(
+                            title = itemTitle(itemWrapper.data),
+                            subtitle = itemSubtitle(itemWrapper.data),
+                            isSelected = itemWrapper.isSelected,
+                            status = itemWrapper.status,
+                            onClick = { onToggleItem(index) },
+                            onInfoClick = {
+                                onItemInfoClick(index)
+                                editingIndex = index
+                            }
+                        )
+                    }
+                }
+            }
         }
+
+        ConfirmDismissButtonsRow(
+            modifier = Modifier
+                .fillMaxWidth()
+                .navigationBarsPadding()
+                .padding(top = 8.dp, bottom = 8.dp),
+            onDismiss = onDismissRequest,
+            onConfirm = {
+                val selectedData = currentState.items.filter { it.isSelected }.map { it.data }
+                onConfirm(selectedData)
+            },
+            dismissText = "取消",
+            confirmText = "导入",
+            confirmEnabled = selectedCount > 0
+        )
     }
 }
 
+@SuppressLint("ConfigurationScreenWidthHeight")
+@Composable
+private fun <T> BatchImportJsonEditContent(
+    data: T,
+    version: Int,
+    onDataChange: (T) -> Unit
+) {
+    val jsonObject = remember(version) { data.toImportJsonObject() }
+
+    if (jsonObject == null) {
+        AppText("不支持编辑")
+        return
+    }
+
+    LazyColumn(
+        modifier = Modifier
+            .fillMaxWidth()
+            .heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.58f),
+        contentPadding = PaddingValues(vertical = 8.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        items(
+            items = jsonObject.entrySet().toList(),
+            key = { it.key }
+        ) { entry ->
+            BatchImportJsonField(
+                name = entry.key,
+                value = entry.value,
+                onValueChange = { value ->
+                    val updatedJsonObject = data.toImportJsonObject() ?: return@BatchImportJsonField
+                    updatedJsonObject.add(entry.key, value)
+                    updatedJsonObject.toImportDataLike(data)?.let(onDataChange)
+                }
+            )
+        }
+    }
+}
+
+@Composable
+private fun BatchImportJsonField(
+    name: String,
+    value: JsonElement,
+    onValueChange: (JsonElement) -> Unit
+) {
+    val primitive = value.takeIf { it.isJsonPrimitive }?.asJsonPrimitive
+    if (primitive?.isBoolean == true) {
+        GlassCard(
+            containerColor = LegadoTheme.colorScheme.onSheetContent
+        ) {
+            SwitchSettingItem(
+                title = name,
+                checked = primitive.asBoolean,
+                onCheckedChange = { onValueChange(JsonPrimitive(it)) }
+            )
+        }
+
+        return
+    }
+
+    val isJsonText = value.isJsonObject || value.isJsonArray
+    val initialText = value.toImportEditText()
+    var text by remember(name, initialText) { mutableStateOf(initialText) }
+
+    AppTextField(
+        value = text,
+        onValueChange = { newText ->
+            text = newText
+            newText.toImportJsonElement(value)?.let(onValueChange)
+        },
+        label = name,
+        modifier = Modifier.fillMaxWidth(),
+        singleLine = !isJsonText,
+        maxLines = if (isJsonText) 8 else 1
+    )
+}
+
 @Composable
 fun ImportItemRow(
     title: String,
@@ -220,7 +323,7 @@ fun ImportItemRow(
         isSelected = isSelected,
         inSelectionMode = true,
         onToggleSelection = onClick,
-        modifier = Modifier.padding(vertical = 4.dp),
+        containerColor = LegadoTheme.colorScheme.onSheetContent,
         trailingAction = {
             AppText(
                 text = when (status) {
@@ -248,43 +351,49 @@ fun ImportItemRow(
     )
 }
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun ImportBottomBar(
-    selectedCount: Int,
-    totalCount: Int,
-    onToggleSelectAll: (Boolean) -> Unit,
-    onConfirm: () -> Unit,
-    onCancel: () -> Unit
-) {
-    Row(
-        modifier = Modifier
-            .fillMaxWidth()
-            .navigationBarsPadding()
-            .padding(horizontal = 16.dp, vertical = 8.dp),
-        horizontalArrangement = Arrangement.SpaceBetween,
-        verticalAlignment = Alignment.CenterVertically
-    ) {
-        val allSelected = selectedCount == totalCount
-        OutlinedToggleButton(
-            checked = allSelected,
-            onCheckedChange = { checked ->
-                onToggleSelectAll(checked)
-            },
-        ) {
-            Icon(
-                imageVector = Icons.Default.SelectAll,
-                contentDescription = if (allSelected) "全不选" else "全选"
-            )
-        }
-        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
-            OutlinedButton(onClick = onCancel) { AppText("取消") }
-            Button(
-                enabled = selectedCount > 0,
-                onClick = onConfirm
-            ) {
-                AppText("导入")
+private fun Any?.toImportJsonObject(): JsonObject? {
+    return GSON.toJsonTree(this).takeIf { it.isJsonObject }?.asJsonObject
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun <T> JsonObject.toImportDataLike(data: T): T? {
+    val clazz = data?.let { it::class.java } ?: return null
+    return runCatching { GSON.fromJson(this, clazz) as T }.getOrNull()
+}
+
+private fun JsonElement.toImportEditText(): String {
+    return when {
+        this is JsonNull || isJsonNull -> ""
+        isJsonObject || isJsonArray -> GSON.toJson(this)
+        isJsonPrimitive -> asJsonPrimitive.asString
+        else -> toString()
+    }
+}
+
+private fun String.toImportJsonElement(oldValue: JsonElement): JsonElement? {
+    val text = trim()
+    if (oldValue.isJsonNull) {
+        return if (text.isEmpty()) JsonNull.INSTANCE else JsonPrimitive(this)
+    }
+
+    if (oldValue.isJsonObject || oldValue.isJsonArray) {
+        if (text.isEmpty()) return JsonNull.INSTANCE
+        return runCatching { JsonParser.parseString(this) }.getOrNull()
+    }
+
+    if (!oldValue.isJsonPrimitive) return JsonPrimitive(this)
+
+    val primitive = oldValue.asJsonPrimitive
+    return when {
+        primitive.isNumber -> {
+            if (text.isEmpty()) {
+                JsonNull.INSTANCE
+            } else {
+                text.toLongOrNull()?.let { JsonPrimitive(it) }
+                    ?: text.toDoubleOrNull()?.let { JsonPrimitive(it) }
             }
         }
+
+        else -> JsonPrimitive(this)
     }
 }

+ 2 - 1
app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportState.kt

@@ -24,9 +24,10 @@ sealed interface BaseImportUiState<out T> {
     data class Success<T>(
         val source: String,
         val items: List<ImportItemWrapper<T>>,
+        val version: Int = 0,
         // 导入配置项
         val keepOriginalName: Boolean = false,
         val customGroup: String? = null,
         val isAddGroup: Boolean = false
     ) : BaseImportUiState<T>
-}
+}