HapeLee 5 mesi fa
parent
commit
1ac5f6549b

+ 9 - 1
.claude/settings.local.json

@@ -90,7 +90,15 @@
       "WebFetch(domain:api.github.com)",
       "Bash(javap -public /tmp/classes.jar!top/yukonga/miuix/kmp/basic/NavigationBarKt.class)",
       "Bash(javap -public -cp /tmp/classes.jar top.yukonga.miuix.kmp.basic.NavigationBarKt)",
-      "PowerShell(Join-Path *)"
+      "PowerShell(Join-Path *)",
+      "Bash(dir /s /b \"C:\\\\Users\\\\*\\\\.gradle\\\\caches\")",
+      "Bash(findstr /i \"material3\")",
+      "Bash(findstr /i \"sources\")",
+      "Bash(dir /s /b \"%USERPROFILE%\\\\.gradle\\\\caches\")",
+      "Bash(findstr /i \"material3.*sources\")",
+      "WebFetch(domain:android.googlesource.com)",
+      "Read(//c/Users//**)",
+      "Read(//c/Users/**)"
     ]
   }
 }

+ 33 - 68
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt

@@ -3,6 +3,9 @@ package io.legado.app.ui.book.manage
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -22,10 +25,8 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.MenuOpen
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Bookmarks
-import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.Info
@@ -38,18 +39,9 @@ import androidx.compose.material.icons.filled.SkipNext
 import androidx.compose.material.icons.filled.Stop
 import androidx.compose.material.icons.filled.Upload
 import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.FloatingActionButtonMenu
-import androidx.compose.material3.FloatingActionButtonMenuItem
-import androidx.compose.material3.Icon
 import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.ToggleFloatingActionButton
-import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
-import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateOf
@@ -60,7 +52,6 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
@@ -81,13 +72,18 @@ import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
 import io.legado.app.ui.book.info.ChangeSourceSheet
 import io.legado.app.ui.book.info.GroupSelectSheet
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
+import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
 import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.FabMenuItem
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.button.MediumIconButton
 import io.legado.app.ui.widget.components.button.SmallTonalIconButton
 import io.legado.app.ui.widget.components.button.SmallTonalTextButton
+import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -119,11 +115,6 @@ import org.koin.androidx.compose.koinViewModel
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
 
-data class BookshelfManageFabAction(
-    val icon: androidx.compose.ui.graphics.vector.ImageVector,
-    val label: String,
-    val action: () -> Unit
-)
 
 private data class BookshelfManageListState(
     override val items: List<Book> = emptyList(),
@@ -150,7 +141,6 @@ fun BookshelfManageRouteScreen(
     )
 }
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun BookshelfManageScreen(
     viewModel: BookshelfManageScreenViewModel,
@@ -390,20 +380,20 @@ private fun BookshelfManageScreen(
         return if (targetBooks.all { it.group == firstGroup }) firstGroup else 0L
     }
     val fabItems = listOf(
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.SelectAll,
             stringResource(R.string.select_all)
         ) {
             selectedBookUrls = filteredBooks.mapTo(hashSetOf()) { it.bookUrl }
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Refresh,
             stringResource(R.string.revert_selection)
         ) {
             val filteredUrls = filteredBooks.map { it.bookUrl }.toSet()
             selectedBookUrls = (selectedBookUrls - filteredUrls) + (filteredUrls - selectedBookUrls)
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Download,
             "缓存选中"
         ) {
@@ -411,7 +401,7 @@ private fun BookshelfManageScreen(
                 showBatchDownloadConfirmDialog = true
             }
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Refresh,
             "批量换源"
         ) {
@@ -419,7 +409,7 @@ private fun BookshelfManageScreen(
                 showBatchSourcePickerSheet = true
             }
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Bookmarks,
             stringResource(R.string.move_to_group)
         ) {
@@ -429,20 +419,20 @@ private fun BookshelfManageScreen(
                 showGroupSelectSheet = true
             }
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Upload,
             "导出选中"
         ) {
             exportSelected()
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Delete,
             stringResource(R.string.clear_cache)
         ) {
             viewModel.dispatch(BookshelfManageScreenIntent.ClearCachesForBooks(selectedBookUrls))
             clearSelection()
         },
-        BookshelfManageFabAction(
+        FabMenuItem(
             Icons.Default.Delete,
             stringResource(R.string.delete)
         ) {
@@ -620,44 +610,14 @@ private fun BookshelfManageScreen(
             )
         },
         floatingActionButton = {
-            FloatingActionButtonMenu(
+            AppFloatingActionButtonMenu(
                 modifier = Modifier.offset(x = 16.dp, y = 16.dp),
                 expanded = fabMenuExpanded,
-                button = {
-                    ToggleFloatingActionButton(
-                        modifier = Modifier
-                            .animateFloatingActionButton(
-                                visible = true,
-                                alignment = Alignment.BottomEnd,
-                            )
-                            .focusRequester(focusRequester),
-                        checked = fabMenuExpanded,
-                        onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
-                    ) {
-                        val imageVector by remember {
-                            derivedStateOf {
-                                if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.AutoMirrored.Filled.MenuOpen
-                            }
-                        }
-                        Icon(
-                            imageVector = imageVector,
-                            contentDescription = "Menu",
-                            modifier = Modifier.animateIcon({ checkedProgress }),
-                        )
-                    }
-                }
-            ) {
-                fabItems.forEach { (icon, label, action) ->
-                    FloatingActionButtonMenuItem(
-                        onClick = {
-                            action()
-                            fabMenuExpanded = false
-                        },
-                        icon = { Icon(icon, contentDescription = null) },
-                        text = { Text(text = label) }
-                    )
-                }
-            }
+                onExpandedChange = { fabMenuExpanded = it },
+                items = fabItems,
+                visible = true,
+                focusRequester = focusRequester
+            )
         }
     ) { paddingValues ->
         val renderVersion by rememberUpdatedState(state.cacheVersion)
@@ -681,6 +641,15 @@ private fun BookshelfManageScreen(
                     stringResource(R.string.cache_download_failed, it)
                 }
                 val isSelected = selectedBookUrls.contains(book.bookUrl)
+                val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
+                val animatedContainerColor by animateColorAsState(
+                    targetValue = if (isSelected)
+                        LegadoTheme.colorScheme.secondaryContainer
+                    else
+                        if (isMiuix) LegadoTheme.colorScheme.surfaceContainer else LegadoTheme.colorScheme.surfaceContainerLow,
+                    animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
+                    label = "CardColor"
+                )
                 val exportMsg = remember(renderVersion, book.bookUrl) {
                     ExportBookService.exportMsg[book.bookUrl]
                 }
@@ -689,7 +658,7 @@ private fun BookshelfManageScreen(
                     key = book.bookUrl,
                     enabled = canReorderBooks
                 ) {
-                    NormalCard(
+                    GlassCard(
                         modifier = Modifier
                             .fillMaxWidth()
                             .then(
@@ -701,11 +670,7 @@ private fun BookshelfManageScreen(
                             ),
                         onClick = { toggleBookSelection(book) },
                         onLongClick = { toggleBookSelection(book) },
-                        containerColor = if (isSelected) {
-                            LegadoTheme.colorScheme.surfaceContainerHigh
-                        } else {
-                            LegadoTheme.colorScheme.surfaceContainerLow
-                        }
+                        containerColor = animatedContainerColor
                     ) {
                         Column(
                             modifier = Modifier

+ 12 - 49
app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt

@@ -36,10 +36,8 @@ import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
-import androidx.compose.material.icons.automirrored.filled.MenuOpen
 import androidx.compose.material.icons.filled.BookmarkAdd
 import androidx.compose.material.icons.filled.CheckCircle
-import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.DownloadForOffline
 import androidx.compose.material.icons.filled.ExpandMore
@@ -52,16 +50,10 @@ import androidx.compose.material.icons.outlined.DownloadForOffline
 import androidx.compose.material.icons.rounded.LocationOn
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.FloatingActionButtonMenu
-import androidx.compose.material3.FloatingActionButtonMenuItem
 import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.ToggleFloatingActionButton
-import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
-import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.derivedStateOf
@@ -76,7 +68,6 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
@@ -95,8 +86,10 @@ import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.CollapsibleHeader
 import io.legado.app.ui.widget.components.ActionItem
+import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.EmptyMessage
+import io.legado.app.ui.widget.components.FabMenuItem
 import io.legado.app.ui.widget.components.SelectionBottomBar
 import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
 import io.legado.app.ui.widget.components.bookmark.BookmarkItem
@@ -215,7 +208,7 @@ fun TocScreen(
 
     val fabItems = remember(state.items) {
         listOf(
-            FabAction(Icons.Default.LocationOn, "定位至当前阅读") {
+            FabMenuItem(Icons.Default.LocationOn, "定位至当前阅读") {
                 scope.launch {
                     val target = state.items.indexOfFirst { it.isDur }
                     if (target != -1) {
@@ -226,13 +219,13 @@ fun TocScreen(
                     }
                 }
             },
-            FabAction(Icons.Default.VerticalAlignTop, "移至顶部") {
+            FabMenuItem(Icons.Default.VerticalAlignTop, "移至顶部") {
                 scope.launch { listState.animateScrollToItem(0) }
             },
-            FabAction(Icons.Default.VerticalAlignBottom, "移至底部") {
+            FabMenuItem(Icons.Default.VerticalAlignBottom, "移至底部") {
                 scope.launch { listState.animateScrollToItem(state.items.size) }
             },
-            FabAction(Icons.Default.DownloadForOffline, "下载全部") {
+            FabMenuItem(Icons.Default.DownloadForOffline, "下载全部") {
                 viewModel.downloadAll()
             }
         )
@@ -509,45 +502,15 @@ fun TocScreen(
             )
         },
         floatingActionButton = {
-            FloatingActionButtonMenu(
+            AppFloatingActionButtonMenu(
                 modifier = Modifier
                     .offset(x = 16.dp, y = 16.dp),
                 expanded = fabMenuExpanded,
-                button = {
-                    ToggleFloatingActionButton(
-                        modifier = Modifier
-                            .animateFloatingActionButton(
-                                visible = shouldShowFab,
-                                alignment = Alignment.BottomEnd,
-                            )
-                            .focusRequester(focusRequester),
-                        checked = fabMenuExpanded,
-                        onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
-                    ) {
-                        val imageVector by remember {
-                            derivedStateOf {
-                                if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.AutoMirrored.Filled.MenuOpen
-                            }
-                        }
-                        Icon(
-                            imageVector = imageVector,
-                            contentDescription = "Menu",
-                            modifier = Modifier.animateIcon({ checkedProgress }),
-                        )
-                    }
-                }
-            ) {
-                fabItems.forEach { (icon, label, action) ->
-                    FloatingActionButtonMenuItem(
-                        onClick = {
-                            action()
-                            fabMenuExpanded = false
-                        },
-                        icon = { Icon(icon, contentDescription = null) },
-                        text = { Text(text = label) }
-                    )
-                }
-            }
+                onExpandedChange = { fabMenuExpanded = it },
+                items = fabItems,
+                visible = shouldShowFab,
+                focusRequester = focusRequester
+            )
         }
     ) { padding ->
         Box(modifier = Modifier.fillMaxSize()) {

+ 1 - 3
app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt

@@ -4,7 +4,7 @@ import android.app.Application
 import android.net.Uri
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.graphics.vector.ImageVector
+
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.viewModelScope
 import io.legado.app.base.BaseRuleViewModel
@@ -109,8 +109,6 @@ private data class TitleCacheKey(
     val chapterCount: Int
 )
 
-data class FabAction(val icon: ImageVector, val label: String, val action: () -> Unit)
-
 @OptIn(ExperimentalCoroutinesApi::class)
 class TocViewModel(
     application: Application,

+ 3 - 1
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt

@@ -219,7 +219,9 @@ fun RssFavoritesScreen(
                                     path = rssStar.image,
                                     sourceOrigin = rssStar.origin,
                                     contentScale = ContentScale.Crop,
-                                    modifier = Modifier.size(54.dp)
+                                    modifier = Modifier
+                                        .padding(vertical = 12.dp)
+                                        .size(54.dp)
                                 )
                             }
                         } else null,

+ 77 - 87
app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt

@@ -3,24 +3,15 @@
 package io.legado.app.ui.rss.read
 
 import android.annotation.SuppressLint
-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
 import android.webkit.WebChromeClient
-import android.webkit.WebResourceRequest
-import android.webkit.WebSettings
 import android.webkit.WebView
-import android.webkit.WebViewClient
 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.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
@@ -28,7 +19,6 @@ 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
 import androidx.compose.material.icons.automirrored.filled.Login
 import androidx.compose.material.icons.filled.Check
@@ -42,10 +32,6 @@ import androidx.compose.material.icons.filled.Stop
 import androidx.compose.material.icons.filled.VolumeUp
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
-import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
-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
@@ -59,40 +45,30 @@ 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
-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
-import io.legado.app.model.Download
-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.AppScaffold
 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
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
 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.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBar
+import io.legado.app.ui.widget.components.topbar.TopBarActionButton
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.utils.NetworkUtils
-import io.legado.app.utils.isTrue
 import io.legado.app.utils.keepScreenOn
-import io.legado.app.utils.longSnackbar
 import io.legado.app.utils.openUrl
 import io.legado.app.utils.setDarkeningAllowed
 import io.legado.app.utils.share
@@ -101,7 +77,6 @@ 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
 import org.koin.androidx.compose.koinViewModel
 
 @SuppressLint("SetJavaScriptEnabled")
@@ -140,7 +115,6 @@ fun RssReadRouteScreen(
 
     val content by viewModel.contentState.collectAsStateWithLifecycle()
     val analyzeUrl by viewModel.urlState.collectAsStateWithLifecycle()
-    val rssStar by viewModel.rssStarState.collectAsStateWithLifecycle()
     val isSpeaking by viewModel.isSpeakingState.collectAsStateWithLifecycle()
     val fallbackUserAgent = OtherConfig.userAgent
 
@@ -175,19 +149,29 @@ fun RssReadRouteScreen(
         val article = viewModel.rssArticle ?: return@LaunchedEffect
         val url = NetworkUtils.getAbsoluteURL(article.origin, article.link)
         val html = viewModel.clHtml(body)
-        if (viewModel.rssSource?.loadWithBaseUrl == true) {
-            currentWebView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
-        } else {
-            currentWebView.loadDataWithBaseURL(null, html, "text/html;charset=utf-8", "utf-8", url)
+        if (currentWebView.url != url) {
+            if (viewModel.rssSource?.loadWithBaseUrl == true) {
+                currentWebView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
+            } else {
+                currentWebView.loadDataWithBaseURL(
+                    null,
+                    html,
+                    "text/html;charset=utf-8",
+                    "utf-8",
+                    url
+                )
+            }
         }
     }
 
     LaunchedEffect(analyzeUrl, webView) {
         val url = analyzeUrl ?: return@LaunchedEffect
         val currentWebView = webView ?: return@LaunchedEffect
-        CookieManager.applyToWebView(url.url)
-        currentWebView.settings.userAgentString = url.getUserAgent()
-        currentWebView.loadUrl(url.url, url.headerMap)
+        if (currentWebView.url != url.url) {
+            CookieManager.applyToWebView(url.url)
+            currentWebView.settings.userAgentString = url.getUserAgent()
+            currentWebView.loadUrl(url.url, url.headerMap)
+        }
     }
 
     BackHandler {
@@ -205,17 +189,19 @@ fun RssReadRouteScreen(
         }
     }
 
+    val isNight = isSystemInDarkTheme()
+
+    LaunchedEffect(isNight, webView) {
+        val currentWebView = webView ?: return@LaunchedEffect
+        currentWebView.settings.setDarkeningAllowed(isNight)
+    }
+
     Box(modifier = Modifier.fillMaxSize()) {
-        Scaffold(
+        AppScaffold(
+            disableHazeSource = true,
             topBar = {
-                TopAppBar(
-                    title = {
-                        Text(
-                            text = pageTitle.ifBlank { defaultTopBarTitle },
-                            maxLines = 1,
-                            overflow = TextOverflow.Ellipsis
-                        )
-                    },
+                GlassTopAppBar(
+                    title = pageTitle.ifBlank { defaultTopBarTitle },
                     navigationIcon = {
                         TopBarNavigationButton(onClick = onBackClick)
                     },
@@ -236,11 +222,11 @@ fun RssReadRouteScreen(
                             }
                         )
                         Box {
-                            TopBarActionButton(
-                                imageVector = Icons.Default.MoreVert,
-                                contentDescription = "Menu",
-                                onClick = { showMenu = true }
-                            )
+                        TopBarActionButton(
+                            imageVector = Icons.Default.MoreVert,
+                            contentDescription = "Menu",
+                            onClick = { showMenu = true }
+                        )
                             RoundDropdownMenu(
                                 expanded = showMenu,
                                 onDismissRequest = { showMenu = false }
@@ -256,7 +242,7 @@ fun RssReadRouteScreen(
                                             context.share(it.link)
                                         } ?: context.toastOnUi(R.string.null_url)
                                     }
-                                )
+                            )
                                 RoundDropdownMenuItem(
                                     text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
                                         R.string.read_aloud
@@ -276,21 +262,21 @@ fun RssReadRouteScreen(
                                                     Jsoup.parse(html).text()
                                                 )
                                             }
-                                        }
+                                    }
                                     }
                                 )
                                 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)
-                                            }
+                                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)
                                         }
-                                    )
+                                    }
+                                )
                                 }
                                 RoundDropdownMenuItem(
                                     text = stringResource(R.string.redirect_policy),
@@ -301,8 +287,9 @@ fun RssReadRouteScreen(
                                     leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
                                     onClick = {
                                         dismiss()
-                                        webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
-                                    }
+                                        webView?.url?.let { context.openUrl(it) }
+                                            ?: context.toastOnUi("url null")
+                                }
                                 )
                             }
                             RoundDropdownMenu(
@@ -310,27 +297,30 @@ fun RssReadRouteScreen(
                                 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)
-                                            }
+                                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 ->
@@ -369,18 +359,18 @@ fun RssReadRouteScreen(
                                     }
                                 },
                                 onHideCustomView = { hideCustomView() }
-                            )
                         )
-                    }
+                    )
+                }
                 )
                 if (webProgress in 0..99) {
                     AppLinearProgressIndicator(
                         progress = webProgress / 100f,
                         modifier = Modifier.fillMaxWidth()
                     )
-                }
             }
         }
+        }
 
         customView?.let { view ->
             key(view) {

+ 1 - 0
app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt

@@ -29,6 +29,7 @@ class VisibleWebView(
     private var lastSelectedText: String = ""
 
     init {
+        setBackgroundColor(0)
         settings.javaScriptEnabled = true
         settings.domStorageEnabled = true
 

+ 156 - 1
app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt

@@ -1,23 +1,55 @@
 package io.legado.app.ui.widget.components
 
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.MenuOpen
+import androidx.compose.material.icons.filled.Close
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonMenu
+import androidx.compose.material3.FloatingActionButtonMenuItem
 import androidx.compose.material3.Icon
 import androidx.compose.material3.PlainTooltip
+import androidx.compose.material3.Text
+import androidx.compose.material3.ToggleFloatingActionButton
+import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
 import androidx.compose.material3.TooltipAnchorPosition
 import androidx.compose.material3.TooltipBox
 import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.material3.rememberTooltipState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
-import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.text.AppText
+import top.yukonga.miuix.kmp.basic.Button
+import top.yukonga.miuix.kmp.theme.MiuixTheme
 import top.yukonga.miuix.kmp.basic.FloatingActionButton as MiuixFloatingActionButton
 import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -85,3 +117,126 @@ fun AppFloatingActionButton(
         }
     }
 }
+
+data class FabMenuItem(
+    val icon: ImageVector,
+    val label: String,
+    val action: () -> Unit
+)
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun AppFloatingActionButtonMenu(
+    expanded: Boolean,
+    onExpandedChange: (Boolean) -> Unit,
+    items: List<FabMenuItem>,
+    modifier: Modifier = Modifier,
+    visible: Boolean = true,
+    focusRequester: FocusRequester = remember { FocusRequester() }
+) {
+    val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+
+    if (isMiuix) {
+        Column(
+            modifier = modifier.padding(horizontal = 16.dp, vertical = 24.dp),
+            horizontalAlignment = Alignment.End,
+            verticalArrangement = Arrangement.spacedBy(12.dp)
+        ) {
+            items.forEachIndexed { index, (icon, label, action) ->
+                AnimatedVisibility(
+                    visible = expanded,
+                    enter = fadeIn(tween(delayMillis = index * 40)) +
+                            scaleIn(tween(delayMillis = index * 40), initialScale = 0.8f),
+                    exit = fadeOut(tween(80)) + scaleOut(tween(80), targetScale = 0.8f)
+                ) {
+                    Button(
+                        onClick = {
+                            action()
+                            onExpandedChange(false)
+                        }
+                    ) {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically
+                        ) {
+                            MiuixIcon(
+                                imageVector = icon,
+                                contentDescription = null,
+                                modifier = Modifier.size(20.dp)
+                            )
+                            Spacer(modifier = Modifier.width(8.dp))
+                            MiuixText(
+                                text = label,
+                                style = MiuixTheme.textStyles.subtitle
+                            )
+                        }
+                    }
+                }
+            }
+
+            AnimatedVisibility(
+                visible = visible,
+                enter = fadeIn() + scaleIn(initialScale = 0.8f),
+                exit = fadeOut() + scaleOut(targetScale = 0.8f)
+            ) {
+                MiuixFloatingActionButton(
+                    onClick = { onExpandedChange(!expanded) }
+                ) {
+                    Row(
+                        verticalAlignment = Alignment.CenterVertically,
+                        horizontalArrangement = Arrangement.Center,
+                        modifier = Modifier.padding(horizontal = 16.dp)
+                    ) {
+                        MiuixIcon(
+                            imageVector = if (expanded) Icons.Filled.Close
+                            else Icons.AutoMirrored.Filled.MenuOpen,
+                            contentDescription = "Menu",
+                            tint = Color.White
+                        )
+                        Spacer(modifier = Modifier.width(8.dp))
+                        Text("菜单", color = Color.White)
+                    }
+                }
+            }
+        }
+    } else {
+        FloatingActionButtonMenu(
+            modifier = modifier,
+            expanded = expanded,
+            button = {
+                ToggleFloatingActionButton(
+                    modifier = Modifier
+                        .animateFloatingActionButton(
+                            visible = visible,
+                            alignment = Alignment.BottomEnd,
+                        )
+                        .focusRequester(focusRequester),
+                    checked = expanded,
+                    onCheckedChange = { onExpandedChange(!expanded) },
+                ) {
+                    val imageVector by remember {
+                        derivedStateOf {
+                            if (checkedProgress > 0.5f) Icons.Filled.Close
+                            else Icons.AutoMirrored.Filled.MenuOpen
+                        }
+                    }
+                    Icon(
+                        imageVector = imageVector,
+                        contentDescription = "Menu",
+                        modifier = Modifier.animateIcon({ checkedProgress }),
+                    )
+                }
+            }
+        ) {
+            items.forEach { (icon, label, action) ->
+                FloatingActionButtonMenuItem(
+                    onClick = {
+                        action()
+                        onExpandedChange(false)
+                    },
+                    icon = { Icon(icon, contentDescription = null) },
+                    text = { Text(text = label) }
+                )
+            }
+        }
+    }
+}

+ 9 - 3
app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt

@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.blur
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
@@ -44,6 +43,7 @@ fun AppScaffold(
     contentColor: Color = contentColorFor(MiuixTheme.colorScheme.surface),
     contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
     alwaysDrawBehindBars: Boolean = false,
+    disableHazeSource: Boolean = false,
     content: @Composable (PaddingValues) -> Unit
 ) {
     val isDark = isSystemInDarkTheme()
@@ -97,7 +97,10 @@ fun AppScaffold(
                         Box(
                             modifier = Modifier
                                 .fillMaxSize()
-                                .responsiveHazeSource(hazeState)
+                                .then(
+                                    if (!disableHazeSource) Modifier.responsiveHazeSource(hazeState)
+                                    else Modifier
+                                )
                                 .then(
                                     if (contentDrawsBehindBars) Modifier
                                     else Modifier.padding(scaffoldPadding)
@@ -136,7 +139,10 @@ fun AppScaffold(
                         Box(
                             modifier = Modifier
                                 .fillMaxSize()
-                                .responsiveHazeSource(hazeState)
+                                .then(
+                                    if (!disableHazeSource) Modifier.responsiveHazeSource(hazeState)
+                                    else Modifier
+                                )
                                 .then(
                                     if (contentDrawsBehindBars) Modifier
                                     else Modifier.padding(scaffoldPadding)

+ 0 - 9
app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt

@@ -22,9 +22,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.SearchBar
-import io.legado.app.ui.widget.components.icon.AppIcon
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -118,12 +115,6 @@ fun <T> DynamicTopAppBar(
                     onQueryChange = onSearchQueryChange,
                     onSearch = onSearchSubmit,
                     placeholder = searchPlaceholder,
-                    leadingIcon = {
-                        AppIcon(
-                            imageVector = searchLeadingIcon,
-                            contentDescription = null
-                        )
-                    },
                     trailingIcon = searchTrailingIcon,
                     dropdownMenu = searchDropdownMenu
                 )

+ 74 - 0
app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassTopAppBar.kt

@@ -0,0 +1,74 @@
+package io.legado.app.ui.widget.components.topbar
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextOverflow
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LocalHazeState
+import io.legado.app.ui.theme.ThemeResolver
+import io.legado.app.ui.theme.responsiveHazeEffect
+import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GlassTopAppBar(
+    title: String,
+    modifier: Modifier = Modifier,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+) {
+    val hazeState = LocalHazeState.current
+    val composeEngine = LegadoTheme.composeEngine
+    val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
+
+    val containerColor = if (!isMiuix) {
+        GlassTopAppBarDefaults.containerColor()
+    } else {
+        GlassTopAppBarDefaults.getMiuixAppBarColor()
+    }
+
+    val finalModifier = if (hazeState != null) {
+        modifier
+            .background(color = containerColor)
+            .responsiveHazeEffect(state = hazeState)
+    } else {
+        modifier.background(color = containerColor)
+    }
+
+    Column(modifier = finalModifier) {
+        if (isMiuix) {
+            MiuixTopAppBar(
+                title = title,
+                navigationIcon = navigationIcon,
+                actions = actions,
+                color = Color.Transparent
+            )
+        } else {
+            TopAppBar(
+                title = {
+                    Text(
+                        text = title,
+                        maxLines = 1,
+                        overflow = TextOverflow.Ellipsis,
+                        style = MaterialTheme.typography.titleLarge
+                    )
+                },
+                navigationIcon = navigationIcon,
+                actions = actions,
+                colors = TopAppBarDefaults.topAppBarColors(
+                    containerColor = Color.Transparent,
+                    scrolledContainerColor = Color.Transparent
+                )
+            )
+        }
+    }
+}

+ 2 - 2
app/src/main/java/io/legado/app/utils/ConfigurationExtensions.kt

@@ -3,9 +3,9 @@
 package io.legado.app.utils
 
 import android.content.res.Configuration
-import android.content.res.Resources
+import splitties.init.appCtx
 
-val sysConfiguration: Configuration = Resources.getSystem().configuration
+val sysConfiguration: Configuration get() = appCtx.resources.configuration
 
 val Configuration.isNightMode: Boolean
     get() {