Explorar el Código

[优化] 优化界面

HapeLee hace 6 meses
padre
commit
c6ed112aa3
Se han modificado 60 ficheros con 1880 adiciones y 1804 borrados
  1. 5 0
      app/src/main/java/io/legado/app/di/appModule.kt
  2. 3 4
      app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt
  3. 12 168
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt
  4. 132 85
      app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
  5. 2 18
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt
  6. 34 41
      app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt
  7. 130 111
      app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
  8. 49 42
      app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt
  9. 3 4
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt
  10. 36 637
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  11. 78 15
      app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt
  12. 370 113
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  13. 4 9
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt
  14. 2 2
      app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt
  15. 6 5
      app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
  16. 73 85
      app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt
  17. 71 60
      app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt
  18. 28 40
      app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt
  19. 11 23
      app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt
  20. 2 1
      app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
  21. 2 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
  22. 21 7
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  23. 32 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt
  24. 14 0
      app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt
  25. 6 8
      app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt
  26. 3 3
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt
  27. 2 5
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt
  28. 7 0
      app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt
  29. 6 31
      app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
  30. 23 0
      app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt
  31. 5 3
      app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt
  32. 2 1
      app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt
  33. 2 1
      app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt
  34. 10 8
      app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt
  35. 30 0
      app/src/main/java/io/legado/app/ui/theme/Typography.kt
  36. 10 0
      app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt
  37. 34 0
      app/src/main/java/io/legado/app/ui/widget/components/AppLinearProgressIndicator.kt
  38. 168 0
      app/src/main/java/io/legado/app/ui/widget/components/AppSearchBar.kt
  39. 38 2
      app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt
  40. 3 3
      app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt
  41. 17 25
      app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt
  42. 2 0
      app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt
  43. 2 1
      app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt
  44. 31 6
      app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt
  45. 6 4
      app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt
  46. 21 11
      app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt
  47. 126 95
      app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
  48. 1 2
      app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt
  49. 27 26
      app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt
  50. 81 73
      app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt
  51. 6 7
      app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt
  52. 10 9
      app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt
  53. 21 0
      app/src/main/java/io/legado/app/ui/widget/components/cover/CoverImageRequest.kt
  54. 48 0
      app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt
  55. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt
  56. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
  57. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt
  58. 7 3
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
  59. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt
  60. 1 1
      gradle/libs.versions.toml

+ 5 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -13,6 +13,8 @@ import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepositoryImpl
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.RemoteBookRepository
+import io.legado.app.data.repository.SearchRepository
+import io.legado.app.data.repository.SearchRepositoryImpl
 import io.legado.app.data.repository.SearchContentRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.domain.usecase.ExploreKindUiUseCase
@@ -30,6 +32,7 @@ import io.legado.app.ui.book.import.local.ImportBookViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
 import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
+import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
@@ -69,6 +72,7 @@ val appModule = module {
 
     single<UploadRepository> { DirectLinkUploadRepository() }
     single<ExploreRepository> { ExploreRepositoryImpl(get()) }
+    single<SearchRepository> { SearchRepositoryImpl(get()) }
 
     single<ImageLoader> {
         ImageLoader.Builder(get())
@@ -111,6 +115,7 @@ val appModule = module {
     viewModelOf(::ChangeBookSourceViewModel)
     viewModelOf(::ExploreViewModel)
     viewModelOf(::RssViewModel)
+    viewModelOf(::SearchViewModel)
 
     viewModel { (route: ReplaceEditRoute) ->
         ReplaceEditViewModel(

+ 3 - 4
app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt

@@ -50,10 +50,9 @@ import io.legado.app.data.entities.Bookmark
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.adaptiveContentPadding
-import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.AppScaffold
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SearchBarSection
 import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
 import io.legado.app.ui.widget.components.bookmark.BookmarkItem
@@ -212,7 +211,7 @@ fun AllBookmarkScreen(
             ) { state ->
                 when (state) {
                     "LOADING" -> {
-                        EmptyMessageView(
+                        EmptyMessage(
                             message = "加载中...",
                             isLoading = true,
                             modifier = Modifier
@@ -225,7 +224,7 @@ fun AllBookmarkScreen(
                     }
 
                     "EMPTY" -> {
-                        EmptyMessageView(
+                        EmptyMessage(
                             message = "没有书签!",
                             modifier = Modifier
                                 .fillMaxSize()

+ 12 - 168
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -92,6 +92,8 @@ import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
 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.book.SearchBookGridItem
+import io.legado.app.ui.widget.components.book.SearchBookListItem
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
@@ -521,111 +523,12 @@ fun ExploreBookItem(
     onClick: () -> Unit,
     modifier: Modifier = Modifier
 ) {
-    val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
-
-    val badge: (@Composable RowScope.() -> Unit)?
-            = when (shelfState) {
-
-        BookShelfState.IN_SHELF -> {
-            {
-                Icon(
-                    imageVector = Icons.Default.Check,
-                    contentDescription = "已在书架",
-                    modifier = Modifier.size(12.dp)
-                )
-            }
-        }
-
-        BookShelfState.SAME_NAME_AUTHOR -> {
-            {
-                Icon(
-                    imageVector = Icons.Default.Warning,
-                    contentDescription = "同名书籍",
-                    modifier = Modifier.size(12.dp)
-                )
-
-            }
-        }
-
-        BookShelfState.NOT_IN_SHELF -> null
-    }
-
-    Row(
+    SearchBookListItem(
+        book = book,
+        shelfState = shelfState,
+        onClick = onClick,
         modifier = modifier
-            .fillMaxWidth()
-            .clickable(onClick = onClick)
-            .padding(horizontal = 16.dp, vertical = 8.dp)
-    ) {
-
-        Cover(
-            path = book.coverUrl,
-            modifier = Modifier.width(72.dp),
-            badgeContent = badge)
-
-        Spacer(modifier = Modifier.width(8.dp))
-
-        Column(modifier = Modifier
-            .weight(1f)
-            .align(Alignment.CenterVertically)) {
-
-            AppText(
-                text = book.name,
-                style = LegadoTheme.typography.titleSmall,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis
-            )
-
-            Row {
-                AppText(
-                    text = book.author,
-                    style = LegadoTheme.typography.bodySmall,
-                    maxLines = 1
-                )
-
-                val latestChapter = book.latestChapterTitle
-                if (!latestChapter.isNullOrEmpty()) {
-                    AppText(
-                        text = " • ",
-                        style = LegadoTheme.typography.bodySmall,
-                        color = Color.Gray,
-                        maxLines = 1
-                    )
-
-                    AppText(
-                        text = "最新: $latestChapter",
-                        style = LegadoTheme.typography.bodySmall,
-                        maxLines = 1,
-                        overflow = TextOverflow.Ellipsis
-                    )
-                }
-            }
-
-            Spacer(modifier = Modifier.height(4.dp))
-
-            val intro = book.intro?.replace("\\s+".toRegex(), "") ?: ""
-            if (intro.isNotEmpty()) {
-                AppText(
-                    text = intro,
-                    style = LegadoTheme.typography.labelSmall,
-                    color = Color.Gray,
-                    maxLines = 2,
-                    minLines = 2,
-                    overflow = TextOverflow.Ellipsis
-                )
-            }
-
-            val kinds = book.getKindList()
-            if (kinds.isNotEmpty()) {
-                Spacer(modifier = Modifier.height(4.dp))
-                Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
-                    kinds.forEach { kind ->
-                        TagChip(text = kind)
-                        Spacer(modifier = Modifier.width(6.dp))
-                    }
-                }
-            }
-        }
-    }
+    )
 }
 
 @Composable
@@ -635,71 +538,12 @@ fun ExploreBookGridItem(
     shelfState: Flow<BookShelfState>,
     modifier: Modifier = Modifier
 ) {
-
-    val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
-
-    val badgeText: String? = when (shelfState) {
-        BookShelfState.IN_SHELF -> "已在书架"
-        BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
-        BookShelfState.NOT_IN_SHELF -> null
-    }
-
-    val content: (@Composable RowScope.() -> Unit)? = if (!badgeText.isNullOrBlank()) {
-        {
-            AppText(
-                text = badgeText,
-                style = LegadoTheme.typography.labelSmall.copy(
-                    fontWeight = FontWeight.Bold,
-                    fontSize = 9.sp
-                )
-            )
-        }
-    } else {
-        null
-    }
-
-    Column(
+    SearchBookGridItem(
+        book = book,
+        shelfState = shelfState,
+        onClick = onClick,
         modifier = modifier
-            .width(IntrinsicSize.Min)
-            .clip(RoundedCornerShape(4.dp))
-            .clickable(onClick = onClick)
-            .padding(4.dp)
-    ) {
-
-        Cover(
-            path = book.coverUrl,
-            modifier = Modifier
-                .fillMaxWidth()
-                .aspectRatio(12 / 17f),
-            badgeContent = content
-        )
-
-        Spacer(modifier = Modifier.height(4.dp))
-
-        AppText(
-            text = book.name,
-            style = LegadoTheme.typography.bodySmall,
-            fontWeight = FontWeight.Bold,
-            maxLines = 2,
-            overflow = TextOverflow.Ellipsis
-        )
-    }
-}
-
-// 简单的标签组件
-@Composable
-fun TagChip(text: String) {
-    Surface(
-        color = LegadoTheme.colorScheme.cardContainer,
-        shape = RoundedCornerShape(4.dp)
-    ) {
-        AppText(
-            text = text,
-            modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
-            style = LegadoTheme.typography.labelSmall,
-            color = LegadoTheme.colorScheme.onCardContainer
-        )
-    }
+    )
 }
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)

+ 132 - 85
app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt

@@ -8,18 +8,16 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material.icons.filled.Restore
-import androidx.compose.material3.Button
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
-import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.OutlinedIconButton
-import androidx.compose.material3.OutlinedTextField
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
@@ -34,12 +32,16 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.data.entities.BookGroup
-import io.legado.app.lib.dialogs.alert
+import io.legado.app.ui.theme.LegadoTheme
+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.MediumIconButton
+import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.cover.BookCover
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
 import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.SelectImageContract
@@ -58,10 +60,35 @@ fun GroupEditSheet(
     onDismissRequest: () -> Unit,
     viewModel: GroupViewModel = koinViewModel()
 ) {
-    AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) {
+    var coverPath by remember(group) { mutableStateOf(group?.cover) }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        startAction = if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) {
+            {
+                GroupDeleteAction(
+                    group = group,
+                    onDismissRequest = onDismissRequest,
+                    viewModel = viewModel
+                )
+            }
+        } else {
+            null
+        },
+        endAction = {
+            GroupResetCoverAction(
+                group = group,
+                onCoverPathChange = { coverPath = it },
+                viewModel = viewModel
+            )
+        }
+    ) {
         GroupEditContent(
             group = group,
             onDismissRequest = onDismissRequest,
+            coverPath = coverPath,
+            onCoverPathChange = { coverPath = it },
             viewModel = viewModel
         )
     }
@@ -71,11 +98,12 @@ fun GroupEditSheet(
 fun GroupEditContent(
     group: BookGroup? = null,
     onDismissRequest: () -> Unit,
+    coverPath: String?,
+    onCoverPathChange: (String?) -> Unit,
     viewModel: GroupViewModel = koinViewModel()
 ) {
     val context = LocalContext.current
     var groupName by remember(group) { mutableStateOf(group?.groupName ?: "") }
-    var coverPath by remember(group) { mutableStateOf(group?.cover) }
     var enableRefresh by remember(group) { mutableStateOf(group?.enableRefresh ?: true) }
     var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) }
 
@@ -96,7 +124,7 @@ fun GroupEditContent(
                     FileOutputStream(file).use { output ->
                         context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) }
                     }
-                    coverPath = file.absolutePath
+                    onCoverPathChange(file.absolutePath)
                 }
             } catch (e: Exception) {
                 appCtx.toastOnUi(e.localizedMessage)
@@ -107,6 +135,7 @@ fun GroupEditContent(
     Column(
         modifier = Modifier
             .fillMaxWidth()
+            .padding(bottom = 16.dp)
             .verticalScroll(rememberScrollState()),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
@@ -129,23 +158,29 @@ fun GroupEditContent(
                 modifier = Modifier.weight(1f),
                 verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {
-                OutlinedTextField(
+                AppTextField(
                     value = groupName,
                     onValueChange = { groupName = it },
-                    label = { AppText(stringResource(R.string.group_name)) },
+                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
+                    label = stringResource(R.string.group_name),
                     modifier = Modifier.fillMaxWidth(),
                     singleLine = true
                 )
 
-                CompactDropdownSettingItem(
-                    title = stringResource(R.string.sort),
-                    selectedValue = selectedSortIndex.toString(),
-                    displayEntries = sortOptions,
-                    entryValues = sortEntryValues,
-                    onValueChange = {
-                        selectedSortIndex = it.toInt()
-                    }
-                )
+                GlassCard(
+                    containerColor = LegadoTheme.colorScheme.onSheetContent,
+                ) {
+                    CompactDropdownSettingItem(
+                        title = stringResource(R.string.sort),
+                        selectedValue = selectedSortIndex.toString(),
+                        color = LegadoTheme.colorScheme.onSheetContent,
+                        displayEntries = sortOptions,
+                        entryValues = sortEntryValues,
+                        onValueChange = {
+                            selectedSortIndex = it.toInt()
+                        }
+                    )
+                }
             }
         }
 
@@ -159,79 +194,91 @@ fun GroupEditContent(
 
         Spacer(modifier = Modifier.height(16.dp))
 
-        Row(
+        ConfirmDismissButtonsRow(
             modifier = Modifier.fillMaxWidth(),
-            verticalAlignment = Alignment.CenterVertically
-        ) {
-            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
-                if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) {
-                    OutlinedIconButton(onClick = {
-                        context.alert(R.string.delete, R.string.sure_del) {
-                            yesButton {
-                                viewModel.delGroup(group) {
-                                    onDismissRequest()
-                                }
-                            }
-                            noButton()
-                        }
-                    }) {
-                        Icon(
-                            Icons.Default.Delete,
-                            contentDescription = stringResource(R.string.delete)
-                        )
-                    }
-                }
-
-                OutlinedIconButton(onClick = {
-                    if (group != null) {
-                        viewModel.clearCover(group) {
-                            coverPath = null
-                            appCtx.toastOnUi("封面已重置")
+            onDismiss = onDismissRequest,
+            onConfirm = {
+                if (groupName.isEmpty()) {
+                    appCtx.toastOnUi("分组名称不能为空")
+                } else {
+                    if (group != null && group.groupId != 0b1L) {
+                        viewModel.upGroup(
+                            group.copy(
+                                groupName = groupName,
+                                cover = coverPath,
+                                bookSort = selectedSortIndex,
+                                enableRefresh = enableRefresh
+                            )
+                        ) {
+                            onDismissRequest()
                         }
                     } else {
-                        coverPath = null
+                        viewModel.addGroup(
+                            groupName,
+                            selectedSortIndex,
+                            enableRefresh,
+                            coverPath
+                        ) {
+                            onDismissRequest()
+                        }
                     }
-                }) {
-                    Icon(Icons.Default.Restore, contentDescription = stringResource(R.string.reset))
                 }
-            }
+            },
+            dismissText = stringResource(R.string.cancel),
+            confirmText = stringResource(R.string.ok)
+        )
+    }
+}
 
-            Spacer(modifier = Modifier.weight(1f))
+@Composable
+fun GroupDeleteAction(
+    group: BookGroup,
+    onDismissRequest: () -> Unit,
+    viewModel: GroupViewModel = koinViewModel()
+) {
+    var showDeleteDialog by remember { mutableStateOf(false) }
 
-            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
-                OutlinedButton(onClick = onDismissRequest) {
-                    AppText(stringResource(R.string.cancel))
-                }
-                Button(onClick = {
-                    if (groupName.isEmpty()) {
-                        appCtx.toastOnUi("分组名称不能为空")
-                    } else {
-                        if (group != null) {
-                            viewModel.upGroup(
-                                group.copy(
-                                    groupName = groupName,
-                                    cover = coverPath,
-                                    bookSort = selectedSortIndex,
-                                    enableRefresh = enableRefresh
-                                )
-                            ) {
-                                onDismissRequest()
-                            }
-                        } else {
-                            viewModel.addGroup(
-                                groupName,
-                                selectedSortIndex,
-                                enableRefresh,
-                                coverPath
-                            ) {
-                                onDismissRequest()
-                            }
-                        }
-                    }
-                }) {
-                    AppText(stringResource(R.string.ok))
+    MediumIconButton(
+        onClick = {
+            showDeleteDialog = true
+        },
+        imageVector = Icons.Default.Delete
+    )
+
+    AppAlertDialog(
+        show = showDeleteDialog,
+        onDismissRequest = { showDeleteDialog = false },
+        title = stringResource(R.string.delete),
+        text = stringResource(R.string.sure_del),
+        confirmText = stringResource(android.R.string.ok),
+        onConfirm = {
+            showDeleteDialog = false
+            viewModel.delGroup(group) {
+                onDismissRequest()
+            }
+        },
+        dismissText = stringResource(android.R.string.cancel),
+        onDismiss = { showDeleteDialog = false }
+    )
+}
+
+@Composable
+fun GroupResetCoverAction(
+    group: BookGroup? = null,
+    onCoverPathChange: (String?) -> Unit,
+    viewModel: GroupViewModel = koinViewModel()
+) {
+    MediumIconButton(
+        onClick = {
+            if (group != null) {
+                viewModel.clearCover(group) {
+                    onCoverPathChange(null)
+                    appCtx.toastOnUi("封面已重置")
                 }
+            } else {
+                onCoverPathChange(null)
             }
-        }
-    }
+        },
+        imageVector = Icons.Default.Restore
+    )
 }

+ 2 - 18
app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt

@@ -1,6 +1,5 @@
 package io.legado.app.ui.book.import.local
 
-import android.net.Uri
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
@@ -29,7 +28,6 @@ import androidx.compose.material.icons.filled.CloudDownload
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material.icons.filled.Folder
 import androidx.compose.material.icons.filled.FolderOpen
-import androidx.compose.material.icons.outlined.AddCircleOutline
 import androidx.compose.material.icons.outlined.Book
 import androidx.compose.material.icons.outlined.Description
 import androidx.compose.material3.CircularProgressIndicator
@@ -53,23 +51,15 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
-import androidx.core.net.toUri
-import androidx.documentfile.provider.DocumentFile
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.constant.AppConst
-import io.legado.app.constant.AppPattern
-import io.legado.app.data.appDb
-import io.legado.app.data.entities.Book
 import io.legado.app.ui.config.importBookConfig.ImportBookConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.selector
-import io.legado.app.lib.permission.Permissions
-import io.legado.app.lib.permission.PermissionsCompat
-import io.legado.app.model.localBook.LocalBook
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.ActionItem
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SelectionActions
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.button.SmallIconButton
@@ -83,15 +73,9 @@ import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.ConvertUtils
-import io.legado.app.utils.ArchiveUtils
-import io.legado.app.utils.FileDoc
-import io.legado.app.utils.isContentScheme
-import io.legado.app.utils.isUri
 import io.legado.app.utils.startActivityForBook
-import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
 import org.koin.androidx.compose.koinViewModel
-import java.io.File
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -222,7 +206,7 @@ private fun ImportBookContent(
                 }
 
                 state.items.isEmpty() -> {
-                    EmptyMessageView(
+                    EmptyMessage(
                         modifier = Modifier.fillMaxSize(),
                         message = stringResource(R.string.empty_msg_import_book)
                     )

+ 34 - 41
app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt

@@ -39,16 +39,11 @@ import androidx.compose.material.icons.outlined.AddCircleOutline
 import androidx.compose.material.icons.outlined.Book
 import androidx.compose.material.icons.outlined.CloudSync
 import androidx.compose.material.icons.outlined.Description
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.TextButton
 import androidx.compose.material3.pulltorefresh.PullToRefreshBox
 import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
 import androidx.compose.runtime.Composable
@@ -80,11 +75,14 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.AppRadioButton
 import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SelectionActions
 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.MediumIconButton
 import io.legado.app.ui.widget.components.button.SmallIconButton
 import io.legado.app.ui.widget.components.button.SmallTonalIconButton
+import io.legado.app.ui.widget.components.button.TopBarActionButton
 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.card.TextCard
@@ -93,7 +91,6 @@ 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.text.AppText
 import io.legado.app.utils.ConvertUtils
-import io.legado.app.utils.FileDoc
 import io.legado.app.utils.isUri
 import io.legado.app.utils.startActivityForBook
 import io.legado.app.utils.toastOnUi
@@ -173,9 +170,10 @@ fun RemoteBookScreen(
         },
         endAction = if (showSheet is RemoteBookSheet.Servers) {
             {
-                IconButton(onClick = { showSheet = RemoteBookSheet.ServerConfig(null) }) {
-                    Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add))
-                }
+                MediumIconButton(
+                    onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
+                    imageVector = Icons.Default.Add
+                )
             }
         } else {
             null
@@ -265,9 +263,10 @@ fun RemoteBookScreen(
         onSearchQueryChange = { viewModel.dispatch(RemoteBookIntent.SearchChange(it)) },
         searchPlaceholder = "搜索",
         topBarActions = {
-            IconButton(onClick = { showSheet = RemoteBookSheet.Servers }) {
-                Icon(Icons.Default.Storage, contentDescription = "配置服务器")
-            }
+            TopBarActionButton(
+                onClick = { showSheet = RemoteBookSheet.Servers },
+                imageVector = Icons.Default.Storage
+            )
         },
         dropDownMenuContent = { dismiss ->
             RoundDropdownMenuItem(
@@ -335,7 +334,7 @@ fun RemoteBookScreen(
                 if (uiState.isLoading) {
                     CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
                 } else {
-                    EmptyMessageView(
+                    EmptyMessage(
                         modifier = Modifier
                             .fillMaxSize()
                             .verticalScroll(rememberScrollState()),
@@ -453,7 +452,7 @@ private fun ServerItem(
         } else {
             null
         },
-        containerColor = LegadoTheme.colorScheme.surface,
+        containerColor = LegadoTheme.colorScheme.onSheetContent,
         selectedContainerColor = LegadoTheme.colorScheme.secondaryContainer,
         modifier = modifier
             .fillMaxWidth()
@@ -483,6 +482,7 @@ private fun ServerConfigSheetContent(
         AppTextField(
             value = name,
             onValueChange = { name = it },
+            backgroundColor = LegadoTheme.colorScheme.onSheetContent,
             label = "名称",
             modifier = Modifier.fillMaxWidth(),
             singleLine = true
@@ -491,6 +491,7 @@ private fun ServerConfigSheetContent(
         AppTextField(
             value = url,
             onValueChange = { url = it },
+            backgroundColor = LegadoTheme.colorScheme.onSheetContent,
             label = "URL",
             modifier = Modifier.fillMaxWidth(),
             singleLine = true
@@ -499,6 +500,7 @@ private fun ServerConfigSheetContent(
         AppTextField(
             value = username,
             onValueChange = { username = it },
+            backgroundColor = LegadoTheme.colorScheme.onSheetContent,
             label = "用户名",
             modifier = Modifier.fillMaxWidth(),
             singleLine = true
@@ -507,6 +509,7 @@ private fun ServerConfigSheetContent(
         AppTextField(
             value = password,
             onValueChange = { password = it },
+            backgroundColor = LegadoTheme.colorScheme.onSheetContent,
             label = "密码",
             modifier = Modifier.fillMaxWidth(),
             singleLine = true,
@@ -523,33 +526,23 @@ private fun ServerConfigSheetContent(
 
         Spacer(modifier = Modifier.height(24.dp))
 
-        Row(
+        ConfirmDismissButtonsRow(
             modifier = Modifier.fillMaxWidth(),
-            horizontalArrangement = Arrangement.spacedBy(8.dp)
-        ) {
-            OutlinedButton(
-                onClick = onCancel,
-                modifier = Modifier.weight(1f)
-            ) {
-                AppText(stringResource(android.R.string.cancel))
-            }
-            Button(
-                onClick = {
-                    val newServer = server?.copy(name = name) ?: Server(name = name)
-                    val newConfig = JSONObject().apply {
-                        put("url", url)
-                        put("username", username)
-                        put("password", password)
-                    }
-                    newServer.config = newConfig.toString()
-                    onSave(newServer)
-                },
-                modifier = Modifier.weight(1f),
-                enabled = name.isNotBlank() && url.isNotBlank()
-            ) {
-                AppText("保存")
-            }
-        }
+            onDismiss = onCancel,
+            onConfirm = {
+                val newServer = server?.copy(name = name) ?: Server(name = name)
+                val newConfig = JSONObject().apply {
+                    put("url", url)
+                    put("username", username)
+                    put("password", password)
+                }
+                newServer.config = newConfig.toString()
+                onSave(newServer)
+            },
+            dismissText = stringResource(android.R.string.cancel),
+            confirmText = "保存",
+            confirmEnabled = name.isNotBlank() && url.isNotBlank()
+        )
     }
 }
 

+ 130 - 111
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -3,7 +3,6 @@ package io.legado.app.ui.book.info
 import android.net.Uri
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.compose.animation.animateColorAsState
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
@@ -43,6 +42,7 @@ import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Share
 import androidx.compose.material.icons.filled.Star
 import androidx.compose.material.icons.outlined.Book
+import androidx.compose.material.icons.outlined.Bookmark
 import androidx.compose.material.icons.outlined.CollectionsBookmark
 import androidx.compose.material.icons.outlined.FolderZip
 import androidx.compose.material.icons.outlined.Image
@@ -53,13 +53,14 @@ import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ElevatedCard
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExtendedFloatingActionButton
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MediumFlexibleTopAppBar
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.pulltorefresh.PullToRefreshBox
 import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
 import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
@@ -78,6 +79,7 @@ import androidx.compose.ui.draw.blur
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalDensity
@@ -89,6 +91,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import coil.ImageLoader
 import coil.compose.AsyncImage
 import io.legado.app.R
+import io.legado.app.constant.BookType
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
@@ -105,11 +108,14 @@ import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LocalHazeState
 import io.legado.app.ui.theme.ProvideThemeOverride
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeOverrideState
 import io.legado.app.ui.theme.rememberImageSeedColor
 import io.legado.app.ui.theme.rememberThemeOverride
+import io.legado.app.ui.theme.responsiveHazeEffect
+import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
 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
@@ -123,10 +129,12 @@ import io.legado.app.ui.widget.components.icon.AppIcon
 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.cover.buildCoverImageRequest
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
 import io.legado.app.ui.widget.components.text.AppText
-import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarScrollBehavior
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior
 import io.legado.app.ui.widget.components.topbar.MiuixGlassScrollBehavior
 import io.legado.app.utils.StartActivityContract
 import io.legado.app.utils.startActivity
@@ -134,6 +142,7 @@ import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.delay
 import org.koin.compose.koinInject
 import org.koin.androidx.compose.koinViewModel
+import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
 import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
 
 @OptIn(ExperimentalMaterial3Api::class)
@@ -162,32 +171,12 @@ private fun BookInfoScreenContent(
     onBack: () -> Unit,
 ) {
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
-    val miuixScrollBehavior = if (isMiuix) {
-        GlassTopAppBarDefaults.defaultScrollBehavior() as? MiuixGlassScrollBehavior
+    val scrollBehavior = if (isMiuix) {
+        MiuixGlassScrollBehavior(MiuixScrollBehavior())
     } else {
-        null
+        M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior())
     }
-    val miuixCollapsedFraction = miuixScrollBehavior?.collapsedFraction ?: 0f
     val listState = rememberLazyListState()
-    val colorThresholdPx = with(LocalDensity.current) { 6.dp.roundToPx() }
-    val showAppBarColor by remember(listState, isMiuix, colorThresholdPx, miuixCollapsedFraction) {
-        derivedStateOf {
-            if (isMiuix) {
-                miuixCollapsedFraction > 0.01f
-            } else {
-                listState.firstVisibleItemIndex > 0 ||
-                    listState.firstVisibleItemScrollOffset > colorThresholdPx
-            }
-        }
-    }
-    val appBarColor by animateColorAsState(
-        targetValue = if (showAppBarColor) {
-            LegadoTheme.colorScheme.surfaceContainer
-        } else {
-            LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0f)
-        },
-        label = "book-info-top-bar-color",
-    )
     val pullState = rememberPullToRefreshState()
     var showMenu by rememberSaveable { mutableStateOf(false) }
 
@@ -196,63 +185,16 @@ private fun BookInfoScreenContent(
     AppScaffold(
         modifier = Modifier
             .fillMaxSize()
-            .then(
-                if (miuixScrollBehavior != null) {
-                    Modifier.nestedScroll(miuixScrollBehavior.nestedScrollConnection)
-                } else {
-                    Modifier
-                }
-            ),
+            .nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
-            if (isMiuix) {
-                MiuixTopAppBar(
-                    title = "",
-                    color = appBarColor,
-                    navigationIcon = {
-                        TopBarNavigationButton(
-                            onClick = { onIntent(BookInfoIntent.BackPressed) }
-                        )
-                    },
-                    actions = {
-                        BookInfoTopBarActions(
-                            state = state,
-                            showMenu = showMenu,
-                            onShowMenuChange = { showMenu = it },
-                            onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
-                        )
-                    },
-                    scrollBehavior = miuixScrollBehavior?.miuixBehavior
-                )
-            } else {
-                TopAppBar(
-                    title = {
-                        Text(
-                            text = "",
-                            maxLines = 1,
-                        )
-                    },
-                    navigationIcon = {
-                        TopBarNavigationButton(
-                            onClick = { onIntent(BookInfoIntent.BackPressed) }
-                        )
-                    },
-                    actions = {
-                        BookInfoTopBarActions(
-                            state = state,
-                            showMenu = showMenu,
-                            onShowMenuChange = { showMenu = it },
-                            onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
-                        )
-                    },
-                    colors = TopAppBarDefaults.topAppBarColors(
-                        containerColor = appBarColor,
-                        scrolledContainerColor = appBarColor,
-                        navigationIconContentColor = LegadoTheme.colorScheme.onSurface,
-                        titleContentColor = LegadoTheme.colorScheme.onSurface,
-                        actionIconContentColor = LegadoTheme.colorScheme.onSurface,
-                    ),
-                )
-            }
+            BookInfoTransparentTopAppBar(
+                state = state,
+                showMenu = showMenu,
+                onShowMenuChange = { showMenu = it },
+                onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
+                onBackPressed = { onIntent(BookInfoIntent.BackPressed) },
+                scrollBehavior = scrollBehavior,
+            )
         },
         floatingActionButton = {
             ExtendedFloatingActionButton(
@@ -322,6 +264,7 @@ private fun BookInfoScreenContent(
                                     .padding(bottom = 24.dp)
                             ) {
                                 BookInfoActions(
+                                    book = book,
                                     inBookshelf = state.inBookshelf,
                                     onShelfClick = { onIntent(BookInfoIntent.ShelfClick) },
                                     onTocClick = { onIntent(BookInfoIntent.TocClick) },
@@ -416,6 +359,70 @@ private fun BookInfoColorTheme(
     ProvideThemeOverride(theme = theme, content = content)
 }
 
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun BookInfoTransparentTopAppBar(
+    state: BookInfoUiState,
+    showMenu: Boolean,
+    onShowMenuChange: (Boolean) -> Unit,
+    onMenuAction: (BookInfoMenuAction) -> Unit,
+    onBackPressed: () -> Unit,
+    scrollBehavior: GlassTopAppBarScrollBehavior,
+) {
+    val hazeState = LocalHazeState.current
+    val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+    val collapsedColor = if (isMiuix) {
+        GlassTopAppBarDefaults.getMiuixAppBarColor()
+    } else {
+        GlassTopAppBarDefaults.scrolledContainerColor()
+    }
+    val isAtTop = scrollBehavior.collapsedFraction <= 0.001f
+    val resolvedColor = if (isAtTop) Color.Transparent else collapsedColor
+    val topBarColors = TopAppBarDefaults.topAppBarColors(
+        containerColor = resolvedColor,
+        scrolledContainerColor = resolvedColor,
+    )
+
+    if (isMiuix) {
+        MiuixTopAppBar(
+            modifier = hazeState?.let { Modifier.responsiveHazeEffectFixedStyle(it) } ?: Modifier,
+            title = "",
+            subtitle = "",
+            navigationIcon = {
+                TopBarNavigationButton(onClick = onBackPressed)
+            },
+            actions = {
+                BookInfoTopBarActions(
+                    state = state,
+                    showMenu = showMenu,
+                    onShowMenuChange = onShowMenuChange,
+                    onMenuAction = onMenuAction,
+                )
+            },
+            color = resolvedColor,
+            scrollBehavior = (scrollBehavior as? MiuixGlassScrollBehavior)?.miuixBehavior,
+        )
+    } else {
+        MediumFlexibleTopAppBar(
+            modifier = hazeState?.let { Modifier.responsiveHazeEffectFixedStyle(it) } ?: Modifier,
+            title = { Text(text = "", maxLines = 1) },
+            navigationIcon = {
+                TopBarNavigationButton(onClick = onBackPressed)
+            },
+            actions = {
+                BookInfoTopBarActions(
+                    state = state,
+                    showMenu = showMenu,
+                    onShowMenuChange = onShowMenuChange,
+                    onMenuAction = onMenuAction,
+                )
+            },
+            scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
+            colors = topBarColors,
+        )
+    }
+}
+
 @Composable
 private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
     val imageLoader = koinInject<ImageLoader>()
@@ -470,6 +477,19 @@ private fun BookInfoTopBarActions(
 @Composable
 private fun BookInfoBackdrop(book: Book) {
     val cover = book.getDisplayCover()
+    val sourceOrigin = book.origin
+    val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
+    val context = LocalContext.current
+    val imageLoader = koinInject<ImageLoader>()
+    val backdropRequest = remember(cover, sourceOrigin, loadOnlyWifi, context) {
+        buildCoverImageRequest(
+            context = context,
+            data = cover,
+            sourceOrigin = sourceOrigin,
+            loadOnlyWifi = loadOnlyWifi,
+            crossfade = false,
+        )
+    }
     val seedOverlay = lerp(
         LegadoTheme.colorScheme.secondaryContainer,
         LegadoTheme.seedColor,
@@ -478,7 +498,8 @@ private fun BookInfoBackdrop(book: Book) {
     Box(modifier = Modifier.fillMaxSize()) {
         if (!cover.isNullOrBlank()) {
             AsyncImage(
-                model = cover,
+                model = backdropRequest,
+                imageLoader = imageLoader,
                 contentDescription = null,
                 modifier = Modifier
                     .fillMaxSize()
@@ -567,22 +588,24 @@ private fun BookInfoOverflowMenu(
             text = stringResource(R.string.to_top),
             onClick = { onMenuAction(BookInfoMenuAction.Top) }
         )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.allow_update),
-            onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
-            trailingIcon = { if (book?.canUpdate == true) Icon(Icons.Default.Star, null) }
-        )
-        if (book?.isLocal == true && book.type and io.legado.app.constant.BookType.text > 0) {
+        if (book?.isLocal == false ){
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.allow_update),
+                onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
+                isSelected = book.canUpdate
+            )
+        }
+        if (book?.isLocal == true && book.type and BookType.text > 0) {
             RoundDropdownMenuItem(
                 text = stringResource(R.string.split_long_chapter),
                 onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) },
-                trailingIcon = { if (book.getSplitLongChapter()) Icon(Icons.Default.Star, null) }
+                isSelected = book.getSplitLongChapter()
             )
         }
         RoundDropdownMenuItem(
             text = stringResource(R.string.delete_alert),
             onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) },
-            trailingIcon = { if (io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert) Icon(Icons.Default.Star, null) }
+            isSelected = io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert
         )
         RoundDropdownMenuItem(
             text = stringResource(R.string.clear_cache),
@@ -606,11 +629,6 @@ private fun BookInfoHeader(
     onBookNameClick: (Boolean) -> Unit,
     onOriginClick: () -> Unit,
 ) {
-    val labelBackground = lerp(
-        LegadoTheme.colorScheme.surface,
-        LegadoTheme.seedColor,
-        0.1f
-    ).copy(alpha = 0.82f)
     Column(
         modifier = Modifier
             .fillMaxWidth()
@@ -688,6 +706,16 @@ private fun BookInfoHeader(
                     modifier = Modifier.fillMaxWidth(),
                     horizontalArrangement = Arrangement.spacedBy(8.dp),
                 ) {
+                    groupNames?.takeIf { it.isNotBlank() }?.let {
+                        item(key = "group-$it") {
+                            TextCard(
+                                text = stringResource(R.string.group_s, it),
+                                textStyle = LegadoTheme.typography.labelLargeEmphasized,
+                                backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
+                                contentColor = LegadoTheme.colorScheme.onSurface,
+                            )
+                        }
+                    }
                     itemsIndexed(
                         items = kindLabels,
                         key = { index, label -> "kind-$index-$label" }
@@ -695,20 +723,10 @@ private fun BookInfoHeader(
                         TextCard(
                             text = label,
                             textStyle = LegadoTheme.typography.labelLargeEmphasized,
-                            backgroundColor = labelBackground,
+                            backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
                             contentColor = LegadoTheme.colorScheme.onSurface,
                         )
                     }
-                    groupNames?.takeIf { it.isNotBlank() }?.let {
-                        item(key = "group-$it") {
-                            TextCard(
-                                text = stringResource(R.string.group_s, it),
-                                textStyle = LegadoTheme.typography.labelLargeEmphasized,
-                                backgroundColor = labelBackground,
-                                contentColor = LegadoTheme.colorScheme.onSurface,
-                            )
-                        }
-                    }
                 }
             }
         }
@@ -717,6 +735,7 @@ private fun BookInfoHeader(
 
 @Composable
 private fun BookInfoActions(
+    book: Book,
     inBookshelf: Boolean,
     onShelfClick: () -> Unit,
     onTocClick: () -> Unit,
@@ -747,7 +766,7 @@ private fun BookInfoActions(
         modifier = Modifier
             .fillMaxWidth()
             .background(LegadoTheme.colorScheme.surface)
-            .padding(horizontal = 16.dp, vertical = 4.dp),
+            .padding(horizontal = 16.dp, vertical = 8.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp),
     ) {
         BookInfoActionCard(
@@ -772,7 +791,7 @@ private fun BookInfoActions(
         )
         BookInfoActionCard(
             modifier = Modifier.weight(1f),
-            icon = Icons.Default.Bookmark,
+            icon = if (book.group > 0) Icons.Default.Bookmark else Icons.Outlined.Bookmark,
             label = stringResource(R.string.change_group),
             onClick = onGroupClick
         )
@@ -788,14 +807,14 @@ private fun BookInfoActions(
 @Composable
 private fun BookInfoActionCard(
     modifier: Modifier = Modifier,
-    icon: androidx.compose.ui.graphics.vector.ImageVector,
+    icon: ImageVector,
     label: String,
     onClick: () -> Unit
 ) {
     GlassCard(
         modifier = modifier,
         onClick = onClick,
-        containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f),
+        containerColor = LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.2f),
         contentColor = LegadoTheme.colorScheme.onSurface,
     ) {
         Column(

+ 49 - 42
app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.grid.GridCells
@@ -65,6 +66,7 @@ import io.legado.app.ui.book.group.GroupEditSheet
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppLinearProgressIndicator
 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
@@ -134,53 +136,58 @@ fun GroupSelectSheet(
         title = stringResource(R.string.group_select),
         endAction = { IconButton(onClick = { editingGroup = BookGroup() }) { Icon(Icons.Default.Add, null) } }
     ) {
-        LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
-            items(groups, key = { it.groupId }) { group ->
-                val isSelected = selectedGroupId and group.groupId > 0
-                SelectionItemCard(
-                    title = group.groupName,
-                    isSelected = isSelected,
-                    onToggleSelection = {
-                        selectedGroupId = if (isSelected) {
-                            selectedGroupId - group.groupId
-                        } else {
-                            selectedGroupId + group.groupId
-                        }
-                    },
-                    leadingContent = {
-                        AppCheckbox(
-                            checked = isSelected,
-                            onCheckedChange = {
-                                selectedGroupId = if (it) {
-                                    selectedGroupId + group.groupId
-                                } else {
-                                    selectedGroupId - group.groupId
-                                }
+        Column(modifier = Modifier.fillMaxWidth()) {
+            LazyColumn(
+                modifier = Modifier.heightIn(max = 600.dp),
+                verticalArrangement = Arrangement.spacedBy(8.dp)
+            ) {
+                items(groups, key = { it.groupId }) { group ->
+                    val isSelected = selectedGroupId and group.groupId > 0
+                    SelectionItemCard(
+                        title = group.groupName,
+                        isSelected = isSelected,
+                        onToggleSelection = {
+                            selectedGroupId = if (isSelected) {
+                                selectedGroupId - group.groupId
+                            } else {
+                                selectedGroupId + group.groupId
                             }
-                        )
-                    },
-                    trailingAction = {
-                        SmallIconButton(
-                            onClick = { editingGroup = group },
-                            imageVector = Icons.Default.Edit
-                        )
-                    },
-                    containerColor = LegadoTheme.colorScheme.surfaceContainerLow
-                )
+                        },
+                        leadingContent = {
+                            AppCheckbox(
+                                checked = isSelected,
+                                onCheckedChange = {
+                                    selectedGroupId = if (it) {
+                                        selectedGroupId + group.groupId
+                                    } else {
+                                        selectedGroupId - group.groupId
+                                    }
+                                }
+                            )
+                        },
+                        trailingAction = {
+                            SmallIconButton(
+                                onClick = { editingGroup = group },
+                                imageVector = Icons.Default.Edit
+                            )
+                        },
+                        containerColor = LegadoTheme.colorScheme.surfaceContainerLow
+                    )
+                }
             }
+            Spacer(modifier = Modifier.height(12.dp))
+            ConfirmDismissButtonsRow(
+                onDismiss = onDismissRequest,
+                onConfirm = { onConfirm(selectedGroupId) },
+                dismissText = stringResource(R.string.cancel),
+                confirmText = stringResource(R.string.ok),
+            )
+            Spacer(modifier = Modifier.height(12.dp))
         }
-        Spacer(modifier = Modifier.height(12.dp))
-        ConfirmDismissButtonsRow(
-            onDismiss = onDismissRequest,
-            onConfirm = { onConfirm(selectedGroupId) },
-            dismissText = stringResource(R.string.cancel),
-            confirmText = stringResource(R.string.ok),
-        )
-        Spacer(modifier = Modifier.height(12.dp))
     }
-
     GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null })
 }
+
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun ChangeCoverSheet(
@@ -376,7 +383,7 @@ fun ChangeSourceSheet(
         )
         Spacer(modifier = Modifier.height(12.dp))
         if (isSearching) {
-            LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+            AppLinearProgressIndicator(modifier = Modifier.fillMaxWidth())
             Spacer(modifier = Modifier.height(8.dp))
             AppText(
                 text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}",

+ 3 - 4
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt

@@ -69,12 +69,11 @@ import cn.hutool.core.date.DateUtil
 import io.legado.app.data.entities.readRecord.ReadRecord
 import io.legado.app.data.entities.readRecord.ReadRecordDetail
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.adaptiveContentPadding
 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.AppScaffold
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SearchBarSection
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.button.AppIconButton
@@ -248,7 +247,7 @@ fun ReadRecordScreen(
         ) { targetState ->
             when (targetState) {
                 "LOADING" -> {
-                    EmptyMessageView(
+                    EmptyMessage(
                         modifier = Modifier
                             .fillMaxSize()
                             .padding(
@@ -261,7 +260,7 @@ fun ReadRecordScreen(
                 }
 
                 "EMPTY" -> {
-                    EmptyMessageView(
+                    EmptyMessage(
                         modifier = Modifier
                             .fillMaxSize()
                             .padding(

+ 36 - 637
app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt

@@ -1,674 +1,73 @@
 package io.legado.app.ui.book.search
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
-import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View.VISIBLE
-import android.view.inputmethod.EditorInfo
-import androidx.activity.viewModels
-import androidx.appcompat.widget.PopupMenu
-import androidx.core.os.bundleOf
-import androidx.core.view.isVisible
-import androidx.core.widget.doAfterTextChanged
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.DefaultItemAnimator
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import androidx.transition.TransitionManager
-import com.google.android.flexbox.FlexboxLayoutManager
-import com.google.android.material.search.SearchBar
-import com.google.android.material.search.SearchView
-import io.legado.app.R
-import io.legado.app.base.VMBaseActivity
-import io.legado.app.constant.AppLog
-import io.legado.app.constant.PreferKey
-import io.legado.app.data.appDb
-import io.legado.app.data.entities.Book
-import io.legado.app.data.entities.SearchKeyword
-import io.legado.app.databinding.ActivityBookSearchBinding
-import io.legado.app.lib.dialogs.alert
-import io.legado.app.model.BookShelfState
-import io.legado.app.ui.about.AppLogDialog
+import androidx.compose.runtime.Composable
+import io.legado.app.base.BaseComposeActivity
 import io.legado.app.ui.book.info.BookInfoActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
-import io.legado.app.utils.applyNavigationBarMargin
-import io.legado.app.utils.getPrefBoolean
-import io.legado.app.utils.gone
-import io.legado.app.utils.invisible
-import io.legado.app.utils.putPrefBoolean
-import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.startActivity
-import io.legado.app.utils.transaction
-import io.legado.app.utils.viewbindingdelegate.viewBinding
-import io.legado.app.utils.visible
-import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.conflate
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import splitties.init.appCtx
-import kotlin.math.abs
+import org.koin.androidx.viewmodel.ext.android.viewModel
 
-class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel>(),
-    BookAdapter.CallBack,
-    HistoryKeyAdapter.CallBack,
-    SearchScopeDialog.Callback,
-    SearchAdapter.CallBack {
+class SearchActivity : BaseComposeActivity() {
 
-    override val binding by viewBinding(ActivityBookSearchBinding::inflate)
-    override val viewModel by viewModels<SearchViewModel>()
-
-    private val adapter by lazy { SearchAdapter(this, this) }
-    private val bookAdapter by lazy {
-        BookAdapter(this, this).apply {
-            setHasStableIds(true)
-        }
-    }
-    private val historyKeyAdapter by lazy {
-        HistoryKeyAdapter(this, this).apply {
-            setHasStableIds(true)
-        }
-    }
-
-    private val searchBar: SearchBar by lazy { binding.searchBar }
-    private val searchView: SearchView by lazy { binding.searchView }
-
-    private var menu: Menu? = null
-    private var groups: List<String>? = null
-    private var historyFlowJob: Job? = null
-    private var booksFlowJob: Job? = null
-    private var precisionSearchMenuItem: MenuItem? = null
-    private var isManualStopSearch = false
+    private val viewModel by viewModel<SearchViewModel>()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setSupportActionBar(binding.searchBar)
-        initRecyclerView()
-        initMaterialSearch()
-        initOtherView()
-        initData()
-        updateSelectedGroup()
-        receiptIntent(intent)
+        dispatchInit(intent)
     }
 
     override fun onNewIntent(intent: Intent) {
         super.onNewIntent(intent)
-        receiptIntent(intent)
+        setIntent(intent)
+        dispatchInit(intent)
     }
 
-    override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
-        menuInflater.inflate(R.menu.book_search, menu)
-        this.menu = menu
-        precisionSearchMenuItem = menu.findItem(R.id.menu_precision_search)
-        precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch)
-        return super.onCompatCreateOptionsMenu(menu)
+    override fun onResume() {
+        super.onResume()
+        viewModel.onIntent(SearchIntent.ResumeEngine)
     }
 
-    override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
-        buildSearchScopeMenu(menu)
-        updateSearchScopeChip()
-        return super.onMenuOpened(featureId, menu)
+    override fun onPause() {
+        viewModel.onIntent(SearchIntent.PauseEngine)
+        super.onPause()
     }
 
-    override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.menu_precision_search -> {
-                val newValue = !getPrefBoolean(PreferKey.precisionSearch)
-                putPrefBoolean(PreferKey.precisionSearch, newValue)
-                precisionSearchMenuItem?.isChecked = newValue
-                binding.chipPrecisionSearch.isChecked = newValue
-                binding.searchView.text.toString().trim().let {
-                    binding.searchView.setText(it)
-                }
-            }
-
-            R.id.menu_search_scope -> alertSearchScope()
-            R.id.menu_source_manage -> startActivity<BookSourceActivity>()
-            R.id.menu_log -> showDialogFragment(AppLogDialog())
-            R.id.menu_1 -> {
-                viewModel.searchScope.update("")
-                updateSelectedGroup()
-            }
-            else -> {
-                if (item.groupId == R.id.menu_group_1) {
-                    viewModel.searchScope.remove(item.title.toString())
-                } else if (item.groupId == R.id.menu_group_2) {
-                    viewModel.searchScope.update(item.title.toString())
+    @Composable
+    override fun Content() {
+        SearchScreen(
+            viewModel = viewModel,
+            onBack = { finish() },
+            onOpenBookInfo = { name, author, bookUrl ->
+                startActivity<BookInfoActivity> {
+                    putExtra("name", name)
+                    putExtra("author", author)
+                    putExtra("bookUrl", bookUrl)
                 }
-                updateSelectedGroup()
+            },
+            onOpenSourceManage = {
+                startActivity<BookSourceActivity>()
             }
-        }
-        return super.onCompatOptionsItemSelected(item)
+        )
     }
 
-    private fun initMaterialSearch() {
-        binding.chipPrecisionSearch.isChecked = getPrefBoolean(PreferKey.precisionSearch)
-        binding.chipPrecisionSearch.setOnCheckedChangeListener { chip, isChecked ->
-            putPrefBoolean(PreferKey.precisionSearch, isChecked)
-            precisionSearchMenuItem?.isChecked = isChecked
-            binding.searchView.text.toString().trim().let {
-                binding.searchView.setText(it)
-            }
-        }
-        binding.chipSearchScope.setOnClickListener {
-            alertSearchScope()
-        }
-        binding.chipGroup.setOnClickListener { view ->
-            binding.chipGroup.isChecked = true
-            val popup = PopupMenu(this, view)
-            val menu = popup.menu
-            buildSearchScopeMenu(menu)
-            updateSearchScopeChip()
-            var fromGroup1 = false
-            popup.setOnMenuItemClickListener { item ->
-                when (item.groupId) {
-                    R.id.menu_group_1 -> {
-                        viewModel.searchScope.remove(item.title.toString())
-                        fromGroup1 = true
-                    }
-                    R.id.menu_group_2 -> {
-                        if (item.itemId == R.id.menu_1) {
-                            viewModel.searchScope.update("")
-                            fromGroup1 = true
-                        } else {
-                            viewModel.searchScope.update(item.title.toString())
-                            fromGroup1 = false
-                        }
-                    }
-                    else -> {
-                        viewModel.searchScope.update(item.title.toString())
-                        fromGroup1 = false
-                    }
-                }
-
-                updateSelectedGroup()
-                true
-            }
-            popup.setOnDismissListener {
-                binding.chipGroup.isChecked = !fromGroup1
-                updateSearchScopeChip()
-            }
-            popup.show()
-        }
-
-        searchBar.setOnMenuItemClickListener { item ->
-            onCompatOptionsItemSelected(item)
-        }
-
-        searchBar.setNavigationOnClickListener {
-            finish()
-        }
-
-        searchView.setupWithSearchBar(searchBar)
-
-        binding.searchView.show()
-        binding.searchView.editText.requestFocus()
-
-        searchView.editText.hint = getString(R.string.search_book_key)
-        searchView.editText.setOnEditorActionListener { _, actionId, _ ->
-            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
-                doSearch()
-                true
-            } else {
-                false
-            }
-        }
-
-        searchView.editText.setOnKeyListener { _, keyCode, event ->
-            if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) {
-                doSearch()
-                true
-            } else {
-                false
-            }
-        }
-
-        searchView.editText.doAfterTextChanged {
-            viewModel.stop()
-            binding.fbStartStop.invisible()
-            upHistory(it?.toString()?.trim().orEmpty())
-        }
-
-        searchView.addTransitionListener { _, _, newState ->
-            if (newState == SearchView.TransitionState.HIDDEN) {
-                searchBar.setText(searchView.text)
-            }
-        }
-
-        searchView.addTransitionListener { _, oldState, newState ->
-            if (oldState == SearchView.TransitionState.HIDDEN && newState == SearchView.TransitionState.SHOWN) {
-                searchView.editText.requestFocus()
-            }
-        }
-
-        visibleInputHelp()
-    }
-
-    private fun doSearch() {
-        val key = searchView.text.toString().trim()
-        if (key.isNotEmpty()) {
-            isManualStopSearch = false
-            viewModel.saveSearchKey(key)
-            viewModel.searchKey = ""
-            viewModel.search(key)
-            visibleInputHelp()
-            searchView.hide()
-        }
-    }
-
-    private fun updateSelectedGroup() {
-        val displayNames = viewModel.searchScope.displayNames
-        val groupName = displayNames.firstOrNull().orEmpty()
-        binding.chipGroup.text = groupName.ifEmpty { getString(R.string.search_select_group) }
-        binding.chipGroup.isChecked = groupName.isNotEmpty()
-    }
-
-    private fun buildSearchScopeMenu(menu: Menu) {
-        menu.transaction {
-            menu.removeGroup(R.id.menu_group_1)
-            menu.removeGroup(R.id.menu_group_2)
-            var hasChecked = false
-            val searchScopeNames = viewModel.searchScope.displayNames
-
-            if (viewModel.searchScope.isSource()) {
-                menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, searchScopeNames.first()).apply {
-                    isChecked = true
-                    hasChecked = true
-                }
-            }
-
-            val allSourceMenu =
-                menu.add(R.id.menu_group_2, R.id.menu_1, Menu.NONE, getString(R.string.all_source))
-                    .apply {
-                        if (searchScopeNames.isEmpty()) {
-                            isChecked = true
-                            hasChecked = true
-                        }
-                    }
-
-            groups?.forEach { groupName ->
-                if (searchScopeNames.contains(groupName)) {
-                    menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, groupName).apply {
-                        isChecked = true
-                        hasChecked = true
-                    }
-                } else {
-                    menu.add(R.id.menu_group_2, Menu.NONE, Menu.NONE, groupName)
-                }
-            }
-
-            if (!hasChecked) {
-                viewModel.searchScope.update("")
-                allSourceMenu.isChecked = true
-            }
-
-            menu.setGroupCheckable(R.id.menu_group_1, true, false)
-            menu.setGroupCheckable(R.id.menu_group_2, true, true)
-        }
-    }
-
-
-    private fun initRecyclerView() {
-        binding.rvBookshelfSearch.layoutManager = FlexboxLayoutManager(this)
-        binding.rvBookshelfSearch.adapter = bookAdapter
-        binding.rvBookshelfSearch.itemAnimator = DefaultItemAnimator()
-
-        binding.rvHistoryKey.layoutManager = FlexboxLayoutManager(this)
-        binding.rvHistoryKey.adapter = historyKeyAdapter
-        binding.rvHistoryKey.itemAnimator = DefaultItemAnimator()
-
-        binding.recyclerView.layoutManager = LinearLayoutManager(this)
-        binding.recyclerView.adapter = adapter
-        binding.recyclerView.itemAnimator = DefaultItemAnimator()
-
-        adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
-            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
-                super.onItemRangeInserted(positionStart, itemCount)
-                if (positionStart == 0) {
-                    binding.recyclerView.scrollToPosition(0)
-                }
-            }
-
-            override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
-                super.onItemRangeMoved(fromPosition, toPosition, itemCount)
-                if (toPosition == 0) {
-                    binding.recyclerView.scrollToPosition(0)
-                }
-            }
-        })
-        binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
-                super.onScrolled(recyclerView, dx, dy)
-                if (!recyclerView.canScrollVertically(1)) {
-                    val layoutManager = recyclerView.layoutManager as LinearLayoutManager
-                    val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
-                    if (lastPosition == RecyclerView.NO_POSITION) {
-                        return
-                    }
-                    val lastView = layoutManager.findViewByPosition(lastPosition)
-                    if (lastView == null) {
-                        scrollToBottom()
-                        return
-                    }
-                    val bottom =
-                        abs(lastView.bottom - recyclerView.height) - recyclerView.paddingBottom
-                    if (bottom <= 1) {
-                        scrollToBottom()
-                    }
-                }
-            }
-        })
-    }
-
-    private fun initOtherView() {
-        binding.llStop.applyNavigationBarMargin()
-        binding.fbStartStop.setOnClickListener {
-            if (viewModel.isSearchLiveData.value == true) {
-                isManualStopSearch = true
-                viewModel.stop()
-            } else {
-                viewModel.search("")
-            }
-        }
-        binding.tvClearHistory.setOnClickListener { alertClearHistory() }
-    }
-
-    private fun initData() {
-        viewModel.searchScope.stateLiveData.observe(this) {
-            if (!binding.llInputHelp.isVisible) {
-                searchView.text.toString().trim().let {
-                    searchView.setText(it)
-                }
-            }
-        }
-        viewModel.isSearchLiveData.observe(this) {
-            if (it) {
-                startSearch()
-            } else {
-                searchFinally()
-            }
-        }
-        viewModel.searchBookLiveData.observe(this) {
-            adapter.setItems(it)
-            if (it.isNullOrEmpty()) {
-                binding.tvEmptyMsg.visible()
-            } else {
-                binding.tvEmptyMsg.gone()
-            }
-        }
-        lifecycleScope.launch {
-            appDb.bookSourceDao.flowEnabledGroups().collect {
-                groups = it
-            }
-        }
-        lifecycleScope.launch {
-            repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                viewModel.resume()
-                try {
-                    awaitCancellation()
-                } finally {
-                    viewModel.pause()
-                }
-            }
-        }
-    }
-
-    /**
-     * 处理传入数据
-     */
-    private fun receiptIntent(intent: Intent? = null) {
-        val searchScope = intent?.getStringExtra("searchScope")
-        searchScope?.let {
-            viewModel.searchScope.update(searchScope, false)
-            updateSearchScopeChip()
-        }
-        val key = intent?.getStringExtra("key")
-        if (key.isNullOrBlank()) {
-            searchView.editText.requestFocus()
-        } else {
-            searchView.setText(key)
-            searchView.postDelayed({doSearch()},500)
-        }
-    }
-
-    /**
-     * 滚动到底部事件
-     */
-    private fun scrollToBottom() {
-        if (isManualStopSearch) {
-            return
-        }
-        if (viewModel.isSearchLiveData.value == false
-            && viewModel.searchKey.isNotEmpty()
-            && viewModel.hasMore
-        ) {
-            viewModel.search("")
-        }
-    }
-
-    /**
-     * 打开关闭输入帮助
-     */
-    private fun visibleInputHelp() {
-        upHistory(searchView.text.toString())
-        binding.llInputHelp.visibility = VISIBLE
-    }
-
-    /**
-     * 更新搜索历史
-     */
-    private fun upHistory(key: String? = null) {
-        booksFlowJob?.cancel()
-        booksFlowJob = lifecycleScope.launch {
-            if (key.isNullOrBlank()) {
-                binding.tvBookShow.gone()
-                binding.rvBookshelfSearch.gone()
-            } else {
-                appDb.bookDao.flowSearch(key).conflate().collect {
-                    if (it.isEmpty()) {
-                        binding.tvBookShow.gone()
-                        binding.rvBookshelfSearch.gone()
-                    } else {
-                        binding.tvBookShow.visible()
-                        binding.rvBookshelfSearch.visible()
-                    }
-                    bookAdapter.setItems(it)
-                }
-            }
-        }
-        historyFlowJob?.cancel()
-        historyFlowJob = lifecycleScope.launch {
-            when {
-                key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime()
-                else -> appDb.searchKeywordDao.flowSearch(key)
-            }.catch {
-                AppLog.put("搜索界面获取搜索历史数据失败\n${it.localizedMessage}", it)
-            }.flowOn(IO).conflate().collect {
-                historyKeyAdapter.setItems(it)
-                if (it.isEmpty()) {
-                    binding.tvClearHistory.invisible()
-                } else {
-                    binding.tvClearHistory.visible()
-                }
-            }
-        }
-    }
-
-    /**
-     * 开始搜索
-     */
-    private fun startSearch() {
-        binding.refreshProgressBar.visible()
-        binding.cdProgress.visible()
-        //binding.refreshProgressBar.isAutoLoading = true
-        binding.fbStartStop.setImageResource(R.drawable.ic_stop_black_24dp)
-        binding.fbStartStop.visible()
-    }
-
-    /**
-     * 搜索结束
-     */
-    private fun searchFinally() {
-        TransitionManager.beginDelayedTransition(binding.root)
-        binding.refreshProgressBar.gone()
-        binding.cdProgress.gone()
-        if (!isManualStopSearch && viewModel.hasMore) {
-            binding.fbStartStop.setImageResource(R.drawable.ic_play)
-        } else {
-            binding.fbStartStop.invisible()
-        }
-    }
-
-    @SuppressLint("SetTextI18n")
-    override fun observeLiveBus() {
-        viewModel.upAdapterLiveData.observe(this) {
-            adapter.notifyItemRangeChanged(0, adapter.itemCount, bundleOf(it to null))
-        }
-        viewModel.searchProgressLiveData.observe(this) { (processed, total) ->
-            val progress = (processed * 100 / total).coerceAtMost(100)
-            binding.refreshProgressBar.setProgress(progress, true)
-            binding.tvProgress.text = "$processed / $total"
-        }
-        viewModel.searchFinishLiveData.observe(this) { isEmpty ->
-            if (!isEmpty || viewModel.searchScope.isAll()) return@observe
-            alert("搜索结果为空") {
-                val precisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch)
-                val displayScope = viewModel.searchScope.display
-                if (precisionSearch) {
-                    setMessage("${displayScope}分组搜索结果为空,是否关闭精准搜索?")
-                    yesButton {
-                        appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
-                        precisionSearchMenuItem?.isChecked = false
-                        viewModel.searchKey = ""
-                        viewModel.search(searchView.text.toString())
-                        doSearch()
-                    }
-                } else {
-                    setMessage("${displayScope}分组搜索结果为空,是否切换到全部分组?")
-                    yesButton {
-                        viewModel.searchScope.update("")
-                        doSearch()
-                    }
-                }
-                noButton()
-            }
-        }
-    }
-
-    /**
-     * 显示书籍详情
-     */
-    override fun showBookInfo(name: String, author: String, bookUrl: String) {
-        startActivity<BookInfoActivity> {
-            putExtra("name", name)
-            putExtra("author", author)
-            putExtra("bookUrl", bookUrl)
-        }
-    }
-
-    /**
-     * 是否已经加入书架
-     */
-    override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState {
-        return viewModel.isInBookShelf(name, author, url)
-    }
-
-    /**
-     * 显示书籍详情
-     */
-    override fun showBookInfo(book: Book) {
-        showBookInfo(book.name, book.author, book.bookUrl)
-    }
-
-    /**
-     * 点击历史关键字
-     */
-    override fun searchHistory(key: String) {
-        lifecycleScope.launch {
-            when {
-                searchView.text.toString() == key -> {
-                    searchView.setText(key)
-                }
-
-                withContext(IO) { appDb.bookDao.findByName(key).isEmpty() } -> {
-                    searchView.setText(key)
-                }
-
-                else -> {
-                    searchView.setText(key)
-                }
-            }
-        }
-    }
-
-    /**
-     * 删除搜索记录
-     */
-    override fun deleteHistory(searchKeyword: SearchKeyword) {
-        viewModel.deleteHistory(searchKeyword)
-    }
-
-
-    override fun onSearchScopeOk(searchScope: SearchScope) {
-        viewModel.searchScope.update(searchScope.toString())
-        updateSearchScopeChip()
-    }
-
-    private fun updateSearchScopeChip() {
-        binding.chipSearchScope.isCheckable = true
-        val scopeNames = viewModel.searchScope.displayNames
-        if (scopeNames.isEmpty() || viewModel.searchScope.isAll()) {
-            binding.chipSearchScope.apply {
-                isChecked = false
-                text = getString(R.string.all_source)
-            }
-        } else {
-            binding.chipSearchScope.apply {
-                isChecked = true
-                text = scopeNames.joinToString(", ")
-            }
-        }
-        binding.chipSearchScope.isCheckable = false
-    }
-
-    private fun alertSearchScope() {
-        showDialogFragment<SearchScopeDialog>()
-    }
-
-    private fun alertClearHistory() {
-        alert(R.string.draw) {
-            setMessage(R.string.sure_clear_search_history)
-            yesButton {
-                viewModel.clearHistory()
-            }
-            noButton()
-        }
-    }
-
-    override fun finish() {
-        if (searchView.hasFocus()) {
-            searchView.clearFocus()
-        }
-        super.finish()
+    private fun dispatchInit(intent: Intent?) {
+        viewModel.onIntent(
+            SearchIntent.Initialize(
+                key = intent?.getStringExtra("key"),
+                scopeRaw = intent?.getStringExtra("searchScope"),
+            )
+        )
     }
 
     companion object {
-
         fun start(context: Context, key: String?, searchScope: String? = null) {
             context.startActivity<SearchActivity> {
                 putExtra("key", key)
                 putExtra("searchScope", searchScope)
             }
         }
-
     }
-}
+}

+ 78 - 15
app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt

@@ -18,11 +18,11 @@ data class SearchScope(private var scope: String) {
     constructor(groups: List<String>) : this(groups.joinToString(","))
 
     constructor(source: BookSource) : this(
-        "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
+        encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
     )
 
     constructor(source: BookSourcePart) : this(
-        "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
+        encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
     )
 
     override fun toString(): String {
@@ -44,19 +44,35 @@ data class SearchScope(private var scope: String) {
     }
 
     fun update(source: BookSource) {
-        scope = "${source.bookSourceName}::${source.bookSourceUrl}"
+        scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
+        stateLiveData.postValue(scope)
+        save()
+    }
+
+    fun update(source: BookSourcePart) {
+        scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
+        stateLiveData.postValue(scope)
+        save()
+    }
+
+    fun updateSources(sources: List<BookSourcePart>) {
+        scope = encodeSourceScope(sources)
         stateLiveData.postValue(scope)
         save()
     }
 
     fun isSource(): Boolean {
-        return scope.contains("::")
+        val items = scopeItems()
+        if (items.isEmpty()) return false
+        return parseSourceItems(items).size == items.size
     }
 
     val display: String
         get() {
-            if (scope.contains("::")) {
-                return scope.substringBefore("::")
+            if (isSource()) {
+                val sourceNames = parseSourceItems().map { it.name }
+                if (sourceNames.isEmpty()) return appCtx.getString(R.string.all_source)
+                return sourceNames.joinToString(",")
             }
             if (scope.isEmpty()) {
                 return appCtx.getString(R.string.all_source)
@@ -70,22 +86,30 @@ data class SearchScope(private var scope: String) {
     val displayNames: List<String>
         get() {
             val list = arrayListOf<String>()
-            if (scope.contains("::")) {
-                list.add(scope.substringBefore("::"))
+            if (isSource()) {
+                parseSourceItems().forEach {
+                    list.add(it.name)
+                }
             } else {
-                scope.splitNotBlank(",").forEach {
+                scopeItems().forEach {
                     list.add(it)
                 }
             }
             return list
         }
 
+    val sourceUrls: List<String>
+        get() = parseSourceItems().map { it.url }
+
     fun remove(scope: String) {
         if (isSource()) {
-            this.scope = ""
+            val sourceItems = parseSourceItems().filterNot {
+                it.name == scope || it.url == scope
+            }
+            this.scope = sourceItems.joinToString(",") { "${it.name}::${it.url}" }
         } else {
             val stringBuilder = StringBuilder()
-            this.scope.split(",").forEach {
+            scopeItems().forEach {
                 if (it != scope) {
                     if (stringBuilder.isNotEmpty()) {
                         stringBuilder.append(",")
@@ -106,14 +130,14 @@ data class SearchScope(private var scope: String) {
         if (scope.isEmpty()) {
             list.addAll(appDb.bookSourceDao.allEnabledPart)
         } else {
-            if (scope.contains("::")) {
-                scope.substringAfter("::").let {
-                    appDb.bookSourceDao.getBookSourcePart(it)?.let { source ->
+            if (isSource()) {
+                parseSourceItems().forEach { sourceItem ->
+                    appDb.bookSourceDao.getBookSourcePart(sourceItem.url)?.let { source ->
                         list.add(source)
                     }
                 }
             } else {
-                val oldScope = scope.splitNotBlank(",")
+                val oldScope = scopeItems()
                 val newScope = oldScope.filter {
                     val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it)
                     list.addAll(bookSources)
@@ -150,4 +174,43 @@ data class SearchScope(private var scope: String) {
         }
     }
 
+    private fun scopeItems(): List<String> = scope.splitNotBlank(",").toList()
+
+    private fun parseSourceItems(
+        items: List<String> = scopeItems()
+    ): List<ScopeSourceItem> {
+        return items.mapNotNull { item ->
+            val splitIndex = item.indexOf("::")
+            if (splitIndex <= 0 || splitIndex >= item.lastIndex) {
+                null
+            } else {
+                ScopeSourceItem(
+                    name = item.substring(0, splitIndex),
+                    url = item.substring(splitIndex + 2)
+                )
+            }
+        }
+    }
+
+    private data class ScopeSourceItem(
+        val name: String,
+        val url: String,
+    )
+
+    companion object {
+        private fun sanitizeSourceName(name: String): String {
+            return name.replace(":", "").replace(",", "")
+        }
+
+        private fun encodeSourceToken(name: String, url: String): String {
+            return "${sanitizeSourceName(name)}::${url}"
+        }
+
+        private fun encodeSourceScope(sources: List<BookSourcePart>): String {
+            return sources.joinToString(",") { source ->
+                encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
+            }
+        }
+    }
+
 }

+ 370 - 113
app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt

@@ -1,166 +1,423 @@
 package io.legado.app.ui.book.search
 
-import android.app.Application
-import android.os.Handler
-import android.os.Looper
-import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import io.legado.app.base.BaseViewModel
-import io.legado.app.constant.AppLog
-import io.legado.app.data.appDb
+import io.legado.app.constant.PreferKey
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.entities.SearchBook
-import io.legado.app.data.entities.SearchKeyword
-import io.legado.app.help.book.isNotShelf
+import io.legado.app.data.repository.SearchRepository
+import io.legado.app.data.repository.SearchSessionEvent
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.BookShelfState
-import io.legado.app.model.webBook.SearchModel
-import io.legado.app.utils.ConflateLiveData
-import io.legado.app.utils.toastOnUi
+import io.legado.app.ui.main.bookshelf.BookShelfItem
+import io.legado.app.utils.getPrefBoolean
+import io.legado.app.utils.putPrefBoolean
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.mapLatest
-import java.util.concurrent.ConcurrentHashMap
-
-data class BookKey(val name: String, val author: String, val url: String?)
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import splitties.init.appCtx
 
 @OptIn(ExperimentalCoroutinesApi::class)
-class SearchViewModel(application: Application) : BaseViewModel(application) {
-    val handler = Handler(Looper.getMainLooper())
-    val bookshelf: MutableSet<BookKey> = ConcurrentHashMap.newKeySet()
-    val upAdapterLiveData = MutableLiveData<String>()
-    var searchBookLiveData = ConflateLiveData<List<SearchBook>>(1000)
-    val searchScope: SearchScope = SearchScope(AppConfig.searchScope)
-    var searchFinishLiveData = MutableLiveData<Boolean>()
-    var isSearchLiveData = MutableLiveData<Boolean>()
+class SearchViewModel(
+    private val repository: SearchRepository,
+) : ViewModel() {
+
+    private val _uiState = MutableStateFlow(
+        SearchUiState(
+            isPrecisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch),
+            scopeDisplay = SearchScope(AppConfig.searchScope).display,
+            scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames,
+            isAllScope = SearchScope(AppConfig.searchScope).isAll(),
+            isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
+        )
+    )
+    val uiState = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<SearchEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    private val queryFlow = MutableStateFlow("")
+    private val bookshelfKeys = MutableStateFlow<Set<BookKey>>(emptySet())
+    private val searchScope = SearchScope(AppConfig.searchScope)
+    private val searchSession = repository.createSearchSession { searchScope }
+
+    private var currentSearchId = 0L
+
+    init {
+        syncScopeState()
+        observeEnabledGroups()
+        observeEnabledSources()
+        observeBookshelf()
+        observeQueryHistory()
+        observeQueryBookshelfHints()
+        observeSearchSession()
+    }
+
+    fun onIntent(intent: SearchIntent) {
+        when (intent) {
+            is SearchIntent.Initialize -> initialize(intent.key, intent.scopeRaw)
+            is SearchIntent.UpdateQuery -> updateQuery(intent.query, showSuggestions = true)
+            SearchIntent.SubmitSearch -> submitSearch()
+            SearchIntent.LoadMore -> loadMore()
+            SearchIntent.StopSearch -> stopSearch()
+            SearchIntent.PauseEngine -> searchSession.pause()
+            SearchIntent.ResumeEngine -> searchSession.resume()
+            is SearchIntent.UseHistoryKeyword -> {
+                updateQuery(intent.keyword, showSuggestions = false)
+                submitSearch(intent.keyword)
+            }
+
+            is SearchIntent.OpenSearchBook -> {
+                emitEffect(
+                    SearchEffect.OpenBookInfo(
+                        name = intent.book.name,
+                        author = intent.book.author,
+                        bookUrl = intent.book.bookUrl,
+                    )
+                )
+            }
+
+            is SearchIntent.OpenBookshelfBook -> {
+                emitEffect(
+                    SearchEffect.OpenBookInfo(
+                        name = intent.book.name,
+                        author = intent.book.author,
+                        bookUrl = intent.book.bookUrl,
+                    )
+                )
+            }
+
+            is SearchIntent.DeleteHistory -> viewModelScope.launch {
+                repository.deleteSearchKeyword(intent.item)
+            }
+
+            is SearchIntent.SetClearHistoryDialogVisible -> {
+                _uiState.update { it.copy(showClearHistoryDialog = intent.visible) }
+            }
+
+            SearchIntent.ConfirmClearHistory -> {
+                _uiState.update { it.copy(showClearHistoryDialog = false) }
+                viewModelScope.launch {
+                    repository.clearSearchKeywords()
+                }
+            }
+
+            is SearchIntent.SetScopeSheetVisible -> {
+                _uiState.update { it.copy(showScopeSheet = intent.visible) }
+            }
+
+            SearchIntent.SelectAllScope -> {
+                searchScope.update("")
+                syncScopeState()
+            }
 
-    val searchProgressLiveData = MutableLiveData<Pair<Int, Int>>()
+            is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName)
+            is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source)
+            is SearchIntent.RemoveScopeItem -> {
+                searchScope.remove(intent.scopeName)
+                syncScopeState()
+            }
 
-    var searchKey: String = ""
-    var hasMore = true
-    private var searchID = 0L
-    private val searchModel = SearchModel(viewModelScope, object : SearchModel.CallBack {
+            is SearchIntent.TogglePrecision -> {
+                appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled)
+                _uiState.update { it.copy(isPrecisionSearch = intent.enabled) }
+                restartCommittedSearchIfNeeded()
+            }
 
-        override fun getSearchScope(): SearchScope {
-            return searchScope
+            SearchIntent.ConfirmEmptyScopeAction -> handleEmptyScopeActionConfirmed()
+            SearchIntent.DismissEmptyScopeAction -> {
+                _uiState.update { it.copy(emptyScopeAction = null) }
+            }
+
+            SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
         }
+    }
 
-        override fun onSearchStart() {
-            isSearchLiveData.postValue(true)
+    fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
+        return bookshelfKeys
+            .map { keys -> resolveBookShelfState(item, keys) }
+            .distinctUntilChanged()
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        searchSession.close()
+    }
+
+    private fun initialize(key: String?, scopeRaw: String?) {
+        scopeRaw?.let {
+            searchScope.update(it, postValue = false)
         }
+        syncScopeState()
 
-        override fun onSearchSuccess(
-            searchBooks: List<SearchBook>, processedSources: Int, totalSources: Int
-        ) {
-            searchBookLiveData.postValue(searchBooks)
-            searchProgressLiveData.postValue(Pair(processedSources, totalSources))
+        val initKey = key?.trim().orEmpty()
+        if (initKey.isNotEmpty()) {
+            updateQuery(initKey, showSuggestions = false)
+            submitSearch(initKey)
+        } else if (key != null) {
+            updateQuery(initKey, showSuggestions = true)
         }
+    }
 
-        override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) {
-            this@SearchViewModel.hasMore = hasMore
-            isSearchLiveData.postValue(false)
-            searchFinishLiveData.postValue(isEmpty)
+    private fun observeEnabledGroups() {
+        viewModelScope.launch {
+            repository.enabledGroups
+                .catch { emit(emptyList()) }
+                .collect { groups ->
+                    _uiState.update { it.copy(enabledGroups = groups) }
+                }
         }
+    }
 
-        override fun onSearchCancel(exception: Throwable?) {
-            isSearchLiveData.postValue(false)
-            exception?.let {
-                context.toastOnUi(it.localizedMessage)
-            }
+    private fun observeEnabledSources() {
+        viewModelScope.launch {
+            repository.enabledSources
+                .catch { emit(emptyList()) }
+                .collect { sources ->
+                    _uiState.update { it.copy(enabledSources = sources) }
+                }
         }
+    }
 
-    })
+    private fun observeBookshelf() {
+        viewModelScope.launch {
+            repository.bookshelfKeys
+                .catch { emit(emptySet()) }
+                .collect { keys ->
+                    bookshelfKeys.value = keys
+                }
+        }
+    }
 
-    init {
-        execute {
-            appDb.bookDao.flowAll().mapLatest { books ->
-                books.filterNot { it.isNotShelf }
-            }.catch {
-                AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it)
-            }.collect { books ->
-                bookshelf.clear()
-                bookshelf.addAll(books.map { BookKey(it.name, it.author, it.bookUrl) })
-                upAdapterLiveData.postValue("isInBookshelf")
+    private fun observeQueryHistory() {
+        viewModelScope.launch {
+            queryFlow
+                .map { it.trim() }
+                .distinctUntilChanged()
+                .flatMapLatest { repository.searchHistory(it) }
+                .catch { emit(emptyList()) }
+                .collect { history ->
+                    _uiState.update { it.copy(history = history) }
+                }
+        }
+    }
+
+    private fun observeQueryBookshelfHints() {
+        viewModelScope.launch {
+            queryFlow
+                .map { it.trim() }
+                .distinctUntilChanged()
+                .flatMapLatest { repository.searchBookshelf(it) }
+                .catch { emit(emptyList()) }
+                .collect { books ->
+                    _uiState.update { it.copy(bookshelfHints = books) }
+                }
+        }
+    }
+
+    private fun observeSearchSession() {
+        viewModelScope.launch {
+            searchSession.events.collect { event ->
+                when (event) {
+                    SearchSessionEvent.Started -> {
+                        _uiState.update { it.copy(isSearching = true) }
+                    }
+
+                    is SearchSessionEvent.Progress -> {
+                        _uiState.update {
+                            it.copy(
+                                results = event.books,
+                                processedSources = event.processedSources,
+                                totalSources = event.totalSources,
+                            )
+                        }
+                    }
+
+                    is SearchSessionEvent.Finished -> {
+                        _uiState.update { state ->
+                            val emptyAction = if (event.isEmpty && !searchScope.isAll()) {
+                                SearchEmptyScopeAction(
+                                    scopeDisplay = searchScope.display,
+                                    wasPrecisionSearch = state.isPrecisionSearch,
+                                )
+                            } else {
+                                null
+                            }
+                            state.copy(
+                                isSearching = false,
+                                hasMore = event.hasMore,
+                                emptyScopeAction = emptyAction,
+                            )
+                        }
+                    }
+
+                    is SearchSessionEvent.Canceled -> {
+                        _uiState.update { it.copy(isSearching = false) }
+                        event.throwable?.localizedMessage
+                            ?.takeIf { it.isNotBlank() }
+                            ?.let { emitEffect(SearchEffect.ShowMessage(it)) }
+                    }
+                }
             }
-        }.onError {
-            AppLog.put("加载书架数据失败", it)
         }
     }
 
-    fun isInBookShelf(name: String, author: String, url: String?): BookShelfState {
-        val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url }
-        if (exactMatch) return BookShelfState.IN_SHELF
+    private fun updateQuery(query: String, showSuggestions: Boolean) {
+        if (showSuggestions && _uiState.value.isSearching) {
+            searchSession.stop()
+        }
+        queryFlow.value = query
+        _uiState.update {
+            it.copy(
+                query = query,
+                showSuggestions = showSuggestions,
+                isManualStop = false,
+                emptyScopeAction = null,
+            )
+        }
+    }
 
-        val sameNameAuthor = bookshelf.any { it.name == name && it.author == author && it.url != url }
-        if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
+    private fun submitSearch(keyOverride: String? = null) {
+        val keyword = keyOverride?.trim() ?: queryFlow.value.trim()
+        if (keyword.isBlank()) return
 
-        return BookShelfState.NOT_IN_SHELF
+        updateQuery(keyword, showSuggestions = false)
+
+        currentSearchId = System.currentTimeMillis()
+        _uiState.update {
+            it.copy(
+                committedQuery = keyword,
+                results = emptyList(),
+                isManualStop = false,
+                hasMore = true,
+                processedSources = 0,
+                totalSources = 0,
+                emptyScopeAction = null,
+            )
+        }
+
+        viewModelScope.launch {
+            repository.saveSearchKeyword(keyword)
+        }
+        searchSession.search(currentSearchId, keyword)
     }
 
-    /**
-     * 开始搜索
-     */
-    fun search(key: String) {
-        execute {
-            if ((searchKey == key) || key.isNotEmpty()) {
-                searchModel.cancelSearch()
-                searchID = System.currentTimeMillis()
-                searchBookLiveData.postValue(emptyList())
-                searchKey = key
-                hasMore = true
-            }
-            if (searchKey.isEmpty()) {
-                return@execute
-            }
-            searchModel.search(searchID, searchKey)
+    private fun loadMore() {
+        val state = _uiState.value
+        if (state.isSearching) return
+        if (state.committedQuery.isBlank()) return
+        if (!state.hasMore) return
+
+        _uiState.update {
+            it.copy(
+                isManualStop = false,
+                showSuggestions = false,
+            )
         }
+        searchSession.search(currentSearchId, state.committedQuery)
     }
 
-    /**
-     * 停止搜索
-     */
-    fun stop() {
-        searchModel.cancelSearch()
+    private fun stopSearch() {
+        _uiState.update { it.copy(isManualStop = true) }
+        searchSession.stop()
     }
 
-    fun pause() {
-        searchModel.pause()
+    private fun toggleScopeGroup(groupName: String) {
+        if (searchScope.isSource()) {
+            searchScope.update("")
+        }
+        val selected = searchScope.displayNames.toMutableSet()
+        if (selected.contains(groupName)) {
+            selected.remove(groupName)
+        } else {
+            selected.add(groupName)
+        }
+        searchScope.update(selected.toList())
+        syncScopeState()
     }
 
-    fun resume() {
-        searchModel.resume()
+    private fun toggleScopeSource(source: BookSourcePart) {
+        val selectedUrls = if (searchScope.isSource()) {
+            searchScope.sourceUrls.toMutableSet()
+        } else {
+            mutableSetOf()
+        }
+
+        if (selectedUrls.contains(source.bookSourceUrl)) {
+            selectedUrls.remove(source.bookSourceUrl)
+        } else {
+            selectedUrls.add(source.bookSourceUrl)
+        }
+
+        if (selectedUrls.isEmpty()) {
+            searchScope.update("")
+        } else {
+            val selectedSources = _uiState.value.enabledSources.filter {
+                selectedUrls.contains(it.bookSourceUrl)
+            }
+            searchScope.updateSources(selectedSources)
+        }
+        syncScopeState()
     }
 
-    /**
-     * 保存搜索关键字
-     */
-    fun saveSearchKey(key: String) {
-        execute {
-            appDb.searchKeywordDao.get(key)?.let {
-                it.usage += 1
-                it.lastUseTime = System.currentTimeMillis()
-                appDb.searchKeywordDao.update(it)
-            } ?: appDb.searchKeywordDao.insert(SearchKeyword(key, 1))
+    private fun handleEmptyScopeActionConfirmed() {
+        val action = _uiState.value.emptyScopeAction ?: return
+        _uiState.update { it.copy(emptyScopeAction = null) }
+
+        if (action.wasPrecisionSearch) {
+            appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
+            _uiState.update { it.copy(isPrecisionSearch = false) }
+        } else {
+            searchScope.update("")
+            syncScopeState()
         }
+
+        restartCommittedSearchIfNeeded()
     }
 
-    /**
-     * 清楚搜索关键字
-     */
-    fun clearHistory() {
-        execute {
-            appDb.searchKeywordDao.deleteAll()
+    private fun restartCommittedSearchIfNeeded() {
+        val committed = _uiState.value.committedQuery
+        if (committed.isNotBlank()) {
+            submitSearch(committed)
         }
     }
 
-    fun deleteHistory(searchKeyword: SearchKeyword) {
-        execute {
-            appDb.searchKeywordDao.delete(searchKeyword)
+    private fun syncScopeState() {
+        _uiState.update {
+            it.copy(
+                scopeDisplay = searchScope.display,
+                scopeDisplayNames = searchScope.displayNames,
+                selectedScopeSourceUrls = searchScope.sourceUrls.toSet(),
+                isAllScope = searchScope.isAll(),
+                isSourceScope = searchScope.isSource(),
+            )
         }
     }
 
-    override fun onCleared() {
-        super.onCleared()
-        searchModel.close()
+    private fun resolveBookShelfState(item: SearchBook, shelf: Set<BookKey>): BookShelfState {
+        val exactMatch = shelf.any {
+            it.name == item.name && it.author == item.author && it.url == item.bookUrl
+        }
+        if (exactMatch) return BookShelfState.IN_SHELF
+
+        val sameNameAuthor = shelf.any {
+            it.name == item.name && it.author == item.author && it.url != item.bookUrl
+        }
+        if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
+
+        return BookShelfState.NOT_IN_SHELF
     }
 
+    private fun emitEffect(effect: SearchEffect) {
+        _effects.tryEmit(effect)
+    }
 }

+ 4 - 9
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt

@@ -15,8 +15,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.layout.size
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
@@ -42,8 +40,6 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.ListItemDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -62,10 +58,9 @@ import io.legado.app.data.entities.SearchContentHistory
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.AppScaffold
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SearchBarSection
 import io.legado.app.ui.widget.components.button.MediumOutlinedButton
-import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
 import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton
 import io.legado.app.ui.widget.components.button.SmallIconButton
 import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton
@@ -225,7 +220,7 @@ fun SearchContentScreen(
             ) { state ->
                 when (state) {
                     is SearchContentState.Error -> {
-                        EmptyMessageView(
+                        EmptyMessage(
                             message = state.throwable.localizedMessage ?: "发生未知错误",
                             modifier = Modifier
                                 .fillMaxSize()
@@ -244,7 +239,7 @@ fun SearchContentScreen(
                         )
                     }
                     SearchContentState.EmptyResult -> {
-                        EmptyMessageView(
+                        EmptyMessage(
                             message = "没有找到相关内容!",
                             modifier = Modifier
                                 .fillMaxSize()
@@ -319,7 +314,7 @@ fun SearchHistoryList(
         }
 
         if (history.isEmpty()) {
-            EmptyMessageView(
+            EmptyMessage(
                 message = "暂无搜索历史",
                 modifier = Modifier
                     .fillMaxSize()

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

@@ -99,7 +99,7 @@ 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.AppScaffold
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 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
@@ -864,7 +864,7 @@ fun BookmarkListContent(
                 ),
             contentAlignment = Alignment.Center
         ) {
-            EmptyMessageView(
+            EmptyMessage(
                 message = "暂无书签"
             )
         }

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

@@ -51,6 +51,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.button.SmallIconButton
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
+import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
@@ -204,17 +205,17 @@ fun TxtRuleScreen(
     }
 
     AppAlertDialog(
-        data = showDeleteRuleDialog, // 传入 nullable ��?rule 对象
+        data = showDeleteRuleDialog,
         onDismissRequest = { showDeleteRuleDialog = null },
         title = stringResource(R.string.delete),
+        text = stringResource(R.string.sure_del),
         confirmText = stringResource(R.string.ok),
         onConfirm = { rule ->
             viewModel.delete(rule)
             showDeleteRuleDialog = null
         },
         dismissText = stringResource(R.string.cancel),
-        onDismiss = { showDeleteRuleDialog = null },
-        content = { Text(text = stringResource(R.string.del_msg)) }
+        onDismiss = { showDeleteRuleDialog = null }
     )
 
     RuleEditSheet(
@@ -228,7 +229,7 @@ fun TxtRuleScreen(
             editingRule = null
         },
         onSave = { updatedRule ->
-            //TODO:我很想把他改为自增主键,但为了兼容性日后再��?
+            //TODO:我很想把他改为自增主键,但为了兼容性日后再�?
             if (editingRule == null) {
                 viewModel.insert(updatedRule)
             } else {
@@ -346,7 +347,7 @@ fun TxtRuleScreen(
                         trailingAction = {
                             SmallIconButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                imageVector = Icons.Default.Delete
+                                imageVector = AppIcons.Delete
                             )
                         }
                     )

+ 73 - 85
app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt

@@ -18,6 +18,8 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
+import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -36,116 +38,102 @@ fun CheckSourceBottomSheet(
 
     AppModalBottomSheet(
         show = show,
-        onDismissRequest = onDismiss
+        onDismissRequest = onDismiss,
+        title = stringResource(R.string.check_source_config)
     ) {
         Column(
             modifier = Modifier
                 .fillMaxWidth()
-                .padding(horizontal = 16.dp)
                 .padding(bottom = 32.dp)
         ) {
-            AppText(
-                text = stringResource(R.string.check_source_config),
-                style = LegadoTheme.typography.titleLarge,
-                modifier = Modifier.padding(vertical = 16.dp)
-            )
 
-            SliderSettingItem(
-                title = stringResource(R.string.check_source_timeout),
-                color = MaterialTheme.colorScheme.surface,
-                value = viewModel.checkSourceTimeout.toFloat(),
-                defaultValue = 180f,
-                onValueChange = { viewModel.checkSourceTimeout = it.toLong() },
-                valueRange = 0f..300f,
-            )
+            GlassCard {
+                SliderSettingItem(
+                    title = stringResource(R.string.check_source_timeout),
+                    value = viewModel.checkSourceTimeout.toFloat(),
+                    defaultValue = 180f,
+                    onValueChange = { viewModel.checkSourceTimeout = it.toLong() },
+                    valueRange = 0f..300f,
+                )
+            }
+
 
             Spacer(modifier = Modifier.padding(8.dp))
 
-            CheckboxGroupContainer(columns = 2) {
-                item {
-                    CheckboxItem(
-                        title = stringResource(R.string.search),
-                        checked = viewModel.checkSearch,
-                        onCheckedChange = {
-                            viewModel.checkSearch = it
-                            if (!it && !viewModel.checkDiscovery) {
-                                viewModel.checkDiscovery = true
-                            }
+            Column(
+                verticalArrangement = Arrangement.spacedBy(8.dp)
+            ) {
+                CheckboxItem(
+                    title = stringResource(R.string.search),
+                    checked = viewModel.checkSearch,
+                    onCheckedChange = {
+                        viewModel.checkSearch = it
+                        if (!it && !viewModel.checkDiscovery) {
+                            viewModel.checkDiscovery = true
                         }
-                    )
-                }
-
-                item {
-                    CheckboxItem(
-                        title = stringResource(R.string.discovery),
-                        checked = viewModel.checkDiscovery,
-                        onCheckedChange = {
-                            viewModel.checkDiscovery = it
-                            if (!it && !viewModel.checkSearch) {
-                                viewModel.checkSearch = true
-                            }
+                    }
+                )
+
+                CheckboxItem(
+                    title = stringResource(R.string.discovery),
+                    checked = viewModel.checkDiscovery,
+                    onCheckedChange = {
+                        viewModel.checkDiscovery = it
+                        if (!it && !viewModel.checkSearch) {
+                            viewModel.checkSearch = true
                         }
-                    )
-                }
-            }
+                    }
+                )
 
 
-            CheckboxGroupContainer(columns = 3) {
-                item {
-                    CheckboxItem(
-                        title = stringResource(R.string.source_tab_info),
-                        checked = viewModel.checkInfo,
-                        onCheckedChange = {
-                            viewModel.checkInfo = it
-                            if (!it) {
-                                viewModel.checkCategory = false
-                                viewModel.checkContent = false
-                            }
-                        }
-                    )
-                }
-
-                item {
-                    CheckboxItem(
-                        title = stringResource(R.string.chapter_list),
-                        checked = viewModel.checkCategory,
-                        enabled = viewModel.checkInfo,
-                        onCheckedChange = {
-                            viewModel.checkCategory = it
-                            if (!it) viewModel.checkContent = false
+
+                CheckboxItem(
+                    title = stringResource(R.string.source_tab_info),
+                    checked = viewModel.checkInfo,
+                    onCheckedChange = {
+                        viewModel.checkInfo = it
+                        if (!it) {
+                            viewModel.checkCategory = false
+                            viewModel.checkContent = false
                         }
-                    )
-                }
-
-                item {
-                    CheckboxItem(
-                        title = stringResource(R.string.source_tab_content),
-                        checked = viewModel.checkContent,
-                        enabled = viewModel.checkCategory,
-                        onCheckedChange = { viewModel.checkContent = it }
-                    )
-                }
+                    }
+                )
+
+
+                CheckboxItem(
+                    title = stringResource(R.string.chapter_list),
+                    checked = viewModel.checkCategory,
+                    enabled = viewModel.checkInfo,
+                    onCheckedChange = {
+                        viewModel.checkCategory = it
+                        if (!it) viewModel.checkContent = false
+                    }
+                )
+
+
+                CheckboxItem(
+                    title = stringResource(R.string.source_tab_content),
+                    checked = viewModel.checkContent,
+                    enabled = viewModel.checkCategory,
+                    onCheckedChange = { viewModel.checkContent = it }
+                )
             }
 
-            Row(
+            ConfirmDismissButtonsRow(
                 modifier = Modifier
                     .fillMaxWidth()
                     .padding(top = 16.dp),
-                horizontalArrangement = Arrangement.End
-            ) {
-                TextButton(onClick = onDismiss) {
-                    AppText(stringResource(R.string.cancel))
-                }
-                Button(onClick = {
+                onDismiss = onDismiss,
+                onConfirm = {
                     if (viewModel.saveCheckSourceConfig()) {
                         onDismiss()
                     } else {
                         Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show()
                     }
-                }) {
-                    AppText(stringResource(R.string.ok))
-                }
-            }
+                },
+                dismissText = stringResource(R.string.cancel),
+                confirmText = stringResource(R.string.ok)
+            )
         }
     }
 }

+ 71 - 60
app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.config.otherConfig
 
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -10,6 +11,7 @@ import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.selection.SelectionContainer
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Checklist
 import androidx.compose.material.icons.filled.ContentCopy
 import androidx.compose.material.icons.filled.ContentPaste
 import androidx.compose.material.icons.filled.Download
@@ -33,8 +35,10 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.lib.dialogs.selector
+import io.legado.app.ui.theme.LegadoTheme
 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.MediumIconButton
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -65,57 +69,68 @@ fun DirectLinkUploadBottomSheet(
     AppModalBottomSheet(
         show = show,
         title = stringResource(R.string.direct_link_upload_config),
-        endAction = {
+        startAction = {
             MediumIconButton(
-                onClick = { showMenu = true },
-                imageVector = Icons.Default.MoreVert
+                onClick = {
+                    viewModel.testRule { result -> showTestResult = result }
+                },
+                imageVector = Icons.Default.Checklist
             )
-            RoundDropdownMenu(
-                expanded = showMenu,
-                onDismissRequest = { showMenu = false }
-            ) {
-                RoundDropdownMenuItem(
-                    text = "导入默认",
-                    leadingIcon = { Icon(Icons.Default.Download, null) },
-                    onClick = {
-                        showMenu = false
-                        context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
-                            viewModel.upView(rule)
-                        }
-                    }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.copy_rule),
-                    leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
-                    onClick = {
-                        showMenu = false
-                        val rule = DirectLinkUpload.Rule(
-                            viewModel.uploadUrl,
-                            viewModel.downloadUrlRule,
-                            viewModel.summary,
-                            viewModel.compress
-                        )
-                        context.sendToClip(GSON.toJson(rule))
-                    }
+        },
+        endAction = {
+            Box {
+                MediumIconButton(
+                    onClick = { showMenu = true },
+                    imageVector = Icons.Default.MoreVert
                 )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.paste_rule),
-                    leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
-                    onClick = {
-                        showMenu = false
-                        runCatching {
-                            context.getClipText()?.let {
-                                val rule =
-                                    GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
-                                        .getOrThrow()
+                RoundDropdownMenu(
+                    expanded = showMenu,
+                    onDismissRequest = { showMenu = false }
+                ) {
+                    RoundDropdownMenuItem(
+                        text = "导入默认",
+                        leadingIcon = { Icon(Icons.Default.Download, null) },
+                        onClick = {
+                            showMenu = false
+                            context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
                                 viewModel.upView(rule)
                             }
-                        }.onFailure {
-                            context.toastOnUi("剪贴板为空或格式不对")
                         }
-                    }
-                )
+                    )
+                    RoundDropdownMenuItem(
+                        text = stringResource(R.string.copy_rule),
+                        leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
+                        onClick = {
+                            showMenu = false
+                            val rule = DirectLinkUpload.Rule(
+                                viewModel.uploadUrl,
+                                viewModel.downloadUrlRule,
+                                viewModel.summary,
+                                viewModel.compress
+                            )
+                            context.sendToClip(GSON.toJson(rule))
+                        }
+                    )
+                    RoundDropdownMenuItem(
+                        text = stringResource(R.string.paste_rule),
+                        leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
+                        onClick = {
+                            showMenu = false
+                            runCatching {
+                                context.getClipText()?.let {
+                                    val rule =
+                                        GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
+                                            .getOrThrow()
+                                    viewModel.upView(rule)
+                                }
+                            }.onFailure {
+                                context.toastOnUi("剪贴板为空或格式不对")
+                            }
+                        }
+                    )
+                }
             }
+
         },
         onDismissRequest = onDismiss
     ) {
@@ -128,6 +143,7 @@ fun DirectLinkUploadBottomSheet(
             AppTextField(
                 value = viewModel.uploadUrl,
                 onValueChange = { viewModel.uploadUrl = it },
+                backgroundColor = LegadoTheme.colorScheme.onSheetContent,
                 label = stringResource(R.string.upload_url),
                 modifier = Modifier.fillMaxWidth(),
             )
@@ -137,6 +153,7 @@ fun DirectLinkUploadBottomSheet(
             AppTextField(
                 value = viewModel.downloadUrlRule,
                 onValueChange = { viewModel.downloadUrlRule = it },
+                backgroundColor = LegadoTheme.colorScheme.onSheetContent,
                 label = stringResource(R.string.download_url_rule),
                 modifier = Modifier.fillMaxWidth(),
             )
@@ -146,6 +163,7 @@ fun DirectLinkUploadBottomSheet(
             AppTextField(
                 value = viewModel.summary,
                 onValueChange = { viewModel.summary = it },
+                backgroundColor = LegadoTheme.colorScheme.onSheetContent,
                 label = stringResource(R.string.summary),
                 modifier = Modifier.fillMaxWidth(),
             )
@@ -154,33 +172,26 @@ fun DirectLinkUploadBottomSheet(
 
             CheckboxItem(
                 title = stringResource(R.string.is_compress),
-                color = MaterialTheme.colorScheme.surface,
+                color = LegadoTheme.colorScheme.onSheetContent,
                 checked = viewModel.compress,
                 onCheckedChange = { viewModel.compress = it }
             )
 
             Spacer(Modifier.height(24.dp))
 
-            Row(modifier = Modifier.fillMaxWidth()) {
-                TextButton(onClick = {
-                    viewModel.testRule { result -> showTestResult = result }
-                }) {
-                    AppText("测试")
-                }
-                Spacer(Modifier.weight(1f))
-                OutlinedButton(onClick = onDismiss) {
-                    AppText(stringResource(R.string.cancel))
-                }
-                Button(onClick = {
+            ConfirmDismissButtonsRow(
+                modifier = Modifier.fillMaxWidth(),
+                onDismiss = onDismiss,
+                onConfirm = {
                     if (viewModel.saveDirectLinkRule()) {
                         onDismiss()
                     } else {
                         context.toastOnUi("请填写完整信息")
                     }
-                }) {
-                    AppText(stringResource(R.string.ok))
-                }
-            }
+                },
+                dismissText = stringResource(R.string.cancel),
+                confirmText = stringResource(R.string.ok)
+            )
         }
     }
 

+ 28 - 40
app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt

@@ -23,6 +23,8 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 
@@ -35,24 +37,22 @@ fun PageKeySheet(
     var prevKeys by remember { mutableStateOf(ReadConfig.prevKeys) }
     var nextKeys by remember { mutableStateOf(ReadConfig.nextKeys) }
 
-    AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) {
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.custom_page_key)
+    ) {
         Column(
-            modifier = Modifier.Companion
+            modifier = Modifier
                 .fillMaxWidth()
                 .padding(16.dp),
             verticalArrangement = Arrangement.spacedBy(16.dp)
         ) {
-            AppText(
-                text = stringResource(R.string.custom_page_key),
-                style = LegadoTheme.typography.titleLarge,
-                fontWeight = FontWeight.Companion.Bold
-            )
-
-            OutlinedTextField(
+            AppTextField(
                 value = prevKeys,
                 onValueChange = { prevKeys = it },
-                label = { AppText(stringResource(R.string.prev_page_key)) },
-                modifier = Modifier.Companion
+                label = stringResource(R.string.prev_page_key),
+                modifier = Modifier
                     .fillMaxWidth()
                     .onPreviewKeyEvent { event ->
                         if (event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) {
@@ -71,10 +71,10 @@ fun PageKeySheet(
                 singleLine = true
             )
 
-            OutlinedTextField(
+            AppTextField(
                 value = nextKeys,
                 onValueChange = { nextKeys = it },
-                label = { AppText(stringResource(R.string.next_page_key)) },
+                label = stringResource(R.string.next_page_key),
                 modifier = Modifier.Companion
                     .fillMaxWidth()
                     .onPreviewKeyEvent { event ->
@@ -96,35 +96,23 @@ fun PageKeySheet(
 
             AppText(
                 text = stringResource(R.string.page_key_set_help),
-                style = LegadoTheme.typography.bodyMedium,
-                color = MaterialTheme.colorScheme.onSurfaceVariant
+                style = LegadoTheme.typography.bodyMedium
             )
 
-            Row(
-                modifier = Modifier.Companion.fillMaxWidth(),
-                horizontalArrangement = Arrangement.spacedBy(8.dp)
-            ) {
-                OutlinedButton(
-                    onClick = {
-                        prevKeys = ""
-                        nextKeys = ""
-                    },
-                    modifier = Modifier.Companion.weight(1f)
-                ) {
-                    AppText(stringResource(R.string.reset))
-                }
-
-                Button(
-                    onClick = {
-                        ReadConfig.prevKeys = prevKeys
-                        ReadConfig.nextKeys = nextKeys
-                        onDismissRequest()
-                    },
-                    modifier = Modifier.Companion.weight(1f)
-                ) {
-                    AppText(stringResource(R.string.ok))
-                }
-            }
+            ConfirmDismissButtonsRow(
+                modifier = Modifier.fillMaxWidth(),
+                onDismiss = {
+                    prevKeys = ""
+                    nextKeys = ""
+                },
+                onConfirm = {
+                    ReadConfig.prevKeys = prevKeys
+                    ReadConfig.nextKeys = nextKeys
+                    onDismissRequest()
+                },
+                dismissText = stringResource(R.string.reset),
+                confirmText = stringResource(R.string.ok)
+            )
         }
     }
 }

+ 11 - 23
app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt

@@ -31,7 +31,9 @@ import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.SmallTonalIconButton
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 import kotlinx.coroutines.launch
@@ -68,35 +70,31 @@ fun BackgroundImageManageSheet(
 
     AppModalBottomSheet(
         show = show,
-        onDismissRequest = onDismissRequest
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.background_image),
     ) {
         Column(
             modifier = Modifier
                 .fillMaxWidth()
                 .padding(bottom = 24.dp)
         ) {
-            AppText(
-                text = stringResource(R.string.background_image),
-                style = LegadoTheme.typography.titleMedium,
-                modifier = Modifier.padding(16.dp)
-            )
 
             if (currentPath.isNullOrBlank()) {
                 Surface(
                     onClick = { showFilePicker = true },
                     shape = MaterialTheme.shapes.medium,
-                    color = MaterialTheme.colorScheme.surfaceContainerHigh,
+                    color = LegadoTheme.colorScheme.surfaceContainerHigh,
                     modifier = Modifier
                         .fillMaxWidth()
                         .aspectRatio(16f / 9f)
                         .padding(16.dp)
                 ) {
                     Box(contentAlignment = Alignment.Center) {
-                        Icon(
+                        AppIcon(
                             imageVector = Icons.Default.Add,
                             contentDescription = "Add",
                             modifier = Modifier.size(48.dp),
-                            tint = MaterialTheme.colorScheme.primary
+                            tint = LegadoTheme.colorScheme.primary
                         )
                     }
                 }
@@ -119,24 +117,14 @@ fun BackgroundImageManageSheet(
                             contentScale = ContentScale.Crop
                         )
                     }
-                    IconButton(
+                    SmallTonalIconButton(
                         onClick = { viewModel.removeBackground(isDarkTheme) },
                         modifier = Modifier
                             .align(Alignment.TopEnd)
                             .padding(8.dp)
-                            .size(32.dp)
-                    ) {
-                        Surface(
-                            shape = CircleShape,
-                            color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
-                        ) {
-                            Icon(
-                                imageVector = Icons.Default.Close,
-                                contentDescription = "Remove",
-                                modifier = Modifier.size(20.dp)
-                            )
-                        }
-                    }
+                            .size(32.dp),
+                        imageVector = Icons.Default.Close
+                    )
                 }
             }
         }

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

@@ -44,6 +44,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.button.SmallIconButton
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
+import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
@@ -320,7 +321,7 @@ fun DictRuleScreen(
                         trailingAction = {
                             SmallIconButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                imageVector = Icons.Default.Delete
+                                imageVector = AppIcons.Delete
                             )
                         }
                     )

+ 2 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -376,6 +376,7 @@ fun BookGroupItemList(
 fun BookItem(
     book: BookShelfItem,
     layoutMode: Int,
+    modifier: Modifier = Modifier,
     gridStyle: Int = 0,
     isCompact: Boolean = false,
     isUpdating: Boolean = false,
@@ -403,6 +404,7 @@ fun BookItem(
         isGrid = layoutMode != 0,
         gridStyle = gridStyle,
         isCompact = isCompact,
+        modifier = modifier,
         cover = { modifier ->
             BookshelfCover(
                 name = book.name,

+ 21 - 7
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -74,6 +74,8 @@ import io.legado.app.ui.theme.LegadoTheme
 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.theme.adaptiveHorizontalPaddingTab
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -197,20 +199,18 @@ fun BookshelfScreen(
     }
 
     val bookGroupStyle = BookshelfConfig.bookGroupStyle
-    // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视��?
+    // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
     var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
-    val baseTitle = when {
-        bookGroupStyle == 1 -> {
+    val baseTitle = when (bookGroupStyle) {
+        1 -> {
             uiState.groups.getOrNull(pagerState.currentPage)?.groupName
                 ?: stringResource(R.string.bookshelf)
         }
-
-        bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> {
+        2 if uiState.groups.isNotEmpty() -> {
             if (isInFolderRoot) stringResource(R.string.bookshelf)
             else uiState.groups.getOrNull(pagerState.currentPage)?.groupName
                 ?: stringResource(R.string.bookshelf)
         }
-
         else -> stringResource(R.string.bookshelf)
     }
     val title = if (uiState.upBooksCount > 0) {
@@ -329,7 +329,7 @@ fun BookshelfScreen(
                     Row(
                         modifier = Modifier
                             .fillMaxWidth()
-                            .adaptiveHorizontalPadding(),
+                            .adaptiveHorizontalPaddingTab(),
                         verticalAlignment = Alignment.CenterVertically
                     ) {
                         val tabTitles = remember(uiState.groups) {
@@ -600,6 +600,19 @@ fun BookshelfPage(
     onBookClick: (BookShelfItem) -> Unit,
     onBookLongClick: (BookShelfItem) -> Unit
 ) {
+    if (books.isEmpty()) {
+        EmptyMessage(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(
+                    top = paddingValues.calculateTopPadding(),
+                    bottom = paddingValues.calculateBottomPadding()
+                ),
+            messageResId = R.string.bookshelf_empty
+        )
+        return
+    }
+
     val columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
     val isGridMode = bookshelfLayoutMode != 0
     val totalHorizontalPadding =
@@ -623,6 +636,7 @@ fun BookshelfPage(
         items(books, key = { it.bookUrl }) { book ->
             BookItem(
                 book = book,
+                modifier = Modifier.animateItem(),
                 layoutMode = bookshelfLayoutMode,
                 gridStyle = BookshelfConfig.bookshelfGridLayout,
                 isCompact = BookshelfConfig.bookshelfLayoutCompact,

+ 32 - 2
app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt

@@ -27,6 +27,8 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.ui.book.group.GroupEditContent
+import io.legado.app.ui.book.group.GroupDeleteAction
+import io.legado.app.ui.book.group.GroupResetCoverAction
 import io.legado.app.ui.book.group.GroupViewModel
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.SmallIconButton
@@ -48,6 +50,7 @@ fun GroupManageSheet(
     val groups by bookshelfViewModel.allGroupsFlow.collectAsState()
     var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
     var isEditing by remember { mutableStateOf(false) }
+    var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) }
 
     var listData by remember { mutableStateOf(groups) }
     val listState = rememberLazyListState()
@@ -76,15 +79,36 @@ fun GroupManageSheet(
         show = show,
         onDismissRequest = onDismissRequest,
         title = if (!isEditing) stringResource(R.string.group_manage) else stringResource(R.string.group_edit),
+        startAction = editingGroup?.takeIf {
+            isEditing && (it.groupId > 0 || it.groupId == Long.MIN_VALUE)
+        }?.let { group ->
+            {
+                GroupDeleteAction(
+                    group = group,
+                    onDismissRequest = {
+                        editingGroup = null
+                        isEditing = false
+                    },
+                    viewModel = viewModel
+                )
+            }
+        },
         endAction = {
             if (!isEditing) {
                 SmallIconButton(
                     onClick = {
                         editingGroup = null
+                        coverPath = null
                         isEditing = true
                     },
                     imageVector = Icons.Default.Add
                 )
+            } else {
+                GroupResetCoverAction(
+                    group = editingGroup,
+                    onCoverPathChange = { coverPath = it },
+                    viewModel = viewModel
+                )
             }
         }
     ) {
@@ -98,7 +122,12 @@ fun GroupManageSheet(
             if (editing) {
                 GroupEditContent(
                     group = editingGroup,
-                    onDismissRequest = { isEditing = false },
+                    onDismissRequest = {
+                        editingGroup = null
+                        isEditing = false
+                    },
+                    coverPath = coverPath,
+                    onCoverPathChange = { coverPath = it },
                     viewModel = viewModel
                 )
             } else {
@@ -115,12 +144,13 @@ fun GroupManageSheet(
                             title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
                             subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
                             isEnabled = group.show,
-                            containerColor = LegadoTheme.colorScheme.surface,
+                            containerColor = LegadoTheme.colorScheme.onSheetContent,
                             onEnabledChange = { isChecked ->
                                 viewModel.upGroup(group.copy(show = isChecked))
                             },
                             onClickEdit = {
                                 editingGroup = group
+                                coverPath = group.cover
                                 isEditing = true
                             }
                         )

+ 14 - 0
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -67,6 +67,7 @@ import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.divider.PillHeaderDivider
 import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
 import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
@@ -159,6 +160,19 @@ fun ExploreScreen(
         }
     ) { paddingValues ->
         Box(modifier = Modifier.fillMaxSize()) {
+            if (uiState.items.isEmpty()) {
+                EmptyMessage(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .padding(
+                            top = paddingValues.calculateTopPadding(),
+                            bottom = paddingValues.calculateBottomPadding()
+                        ),
+                    messageResId = R.string.explore_empty
+                )
+                return@Box
+            }
+
             FastScrollLazyColumn(
                 state = listState,
                 modifier = Modifier.fillMaxSize(),

+ 6 - 8
app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt

@@ -72,6 +72,7 @@ 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.AlertButton
+import io.legado.app.ui.widget.components.button.MediumIconButton
 import io.legado.app.ui.widget.components.button.ToggleChip
 import io.legado.app.ui.widget.components.button.TopBarActionButton
 import io.legado.app.ui.widget.components.button.TopBarNavigationButton
@@ -368,21 +369,20 @@ fun GroupSelector(
                 }
             }
         }
-        IconButton(onClick = onManageClick) {
-            Icon(Icons.Default.Settings, "Manage")
-        }
+        MediumIconButton(
+            onClick = onManageClick,
+            imageVector = Icons.Default.Settings
+        )
     }
 }
 
 @Composable
 fun ManageGroupDialog(
-    show: Boolean, // 💡 新增 show 参数,外层直接传值,不要用 if 包裹!
+    show: Boolean,
     groups: List<String>,
     onDismiss: () -> Unit,
     onDelete: (List<String>) -> Unit
 ) {
-    // 💡 使用 Set 来管理多选状态,并且以 show 作为 key。
-    // 这样每次重新打开弹窗时,选中状态会自动清空,防止上次的残留
     var selectedGroups by remember(show) { mutableStateOf(emptySet<String>()) }
 
     AppAlertDialog(
@@ -395,7 +395,6 @@ fun ManageGroupDialog(
             } else {
                 Column(
                     modifier = Modifier.verticalScroll(rememberScrollState()),
-                    // 去掉了 spacedBy(8.dp),因为 CheckboxItem 内部已经有了合适的 padding
                 ) {
                     groups.forEach { group ->
                         val isSelected = selectedGroups.contains(group)
@@ -418,7 +417,6 @@ fun ManageGroupDialog(
         confirmText = "删除选中",
         onConfirm = {
             onDelete(selectedGroups.toList())
-            // 注意:外层需要在 onDelete 回调中将 show 置为 false
         },
         dismissText = "关闭",
         onDismiss = onDismiss

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

@@ -34,7 +34,7 @@ import io.legado.app.R
 import io.legado.app.data.entities.RssStar
 import io.legado.app.ui.rss.read.ReadRssActivity
 import io.legado.app.ui.widget.components.ActionItem
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SourceIcon
 import io.legado.app.ui.widget.components.button.SmallIconButton
 import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -186,7 +186,7 @@ fun RssFavoritesScreen(
                     .padding(paddingValues),
                 contentAlignment = Alignment.Center
             ) {
-                EmptyMessageView(
+                EmptyMessage(
                     message = "还没有收�订阅�",
                     isLoading = state.isLoading
                 )
@@ -203,7 +203,7 @@ fun RssFavoritesScreen(
                     SelectionItemCard(
                         title = rssStar.title,
                         subtitle = if (rssStar.group.isNotBlank()) {
-                            "${rssStar.group} �${rssStar.pubDate ?: ""}"
+                            "${rssStar.group} �?${rssStar.pubDate ?: ""}"
                         } else {
                             rssStar.pubDate
                         },

+ 2 - 5
app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt

@@ -16,15 +16,12 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.Sort
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Edit
-import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.TextButton
 import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -49,7 +46,7 @@ import io.legado.app.ui.association.ImportReplaceRuleDialog
 import io.legado.app.ui.association.ImportRssSourceDialog
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.EmptyMessageView
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
@@ -123,7 +120,7 @@ fun RuleSubScreen(
                     .padding(paddingValues),
                 contentAlignment = Alignment.Center
             ) {
-                EmptyMessageView(
+                EmptyMessage(
                     message = stringResource(R.string.rule_sub_empty_msg),
                     isLoading = state.isLoading
                 )

+ 7 - 0
app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt

@@ -14,6 +14,13 @@ fun Modifier.adaptiveHorizontalPadding(): Modifier {
     return this.padding(horizontal = horizontal)
 }
 
+@Composable
+fun Modifier.adaptiveHorizontalPaddingTab(): Modifier {
+    val start = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 0.dp
+    val end = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
+    return this.padding(start = start, end = end)
+}
+
 @Composable
 fun Modifier.adaptiveHorizontalPadding(
     vertical: Dp,

+ 6 - 31
app/src/main/java/io/legado/app/ui/theme/AppTheme.kt

@@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.colorResource
 import io.legado.app.ui.config.themeConfig.ThemeConfig
+import io.legado.app.ui.theme.LegadoTheme
 import top.yukonga.miuix.kmp.theme.MiuixTheme
 import top.yukonga.miuix.kmp.theme.ThemeController
 
@@ -194,8 +196,10 @@ fun AppTheme(
                         onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
                         onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
 
-                        cardContainer = miuixColorScheme.disabledPrimary,
-                        onCardContainer = miuixColorScheme.primary
+                        cardContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.32f)
+                            .compositeOver(miuixColorScheme.surfaceContainer),
+                        onCardContainer = miuixColorScheme.primary,
+                        onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f),
                     )
                 }
 
@@ -229,32 +233,3 @@ fun AppTheme(
         }
     }
 }
-
-private fun Typography.toLegadoTypography(): LegadoTypography {
-    return LegadoTypography(
-        headlineLarge = headlineLarge,
-        headlineLargeEmphasized = headlineLargeEmphasized,
-        headlineMedium = headlineMedium,
-        headlineMediumEmphasized = headlineMediumEmphasized,
-        headlineSmall = headlineSmall,
-        headlineSmallEmphasized = headlineSmallEmphasized,
-        titleLarge = titleLarge,
-        titleLargeEmphasized = titleLargeEmphasized,
-        titleMedium = titleMedium,
-        titleMediumEmphasized = titleMediumEmphasized,
-        titleSmall = titleSmall,
-        titleSmallEmphasized = titleSmallEmphasized,
-        bodyLarge = bodyLarge,
-        bodyLargeEmphasized = bodyLargeEmphasized,
-        bodyMedium = bodyMedium,
-        bodyMediumEmphasized = bodyMediumEmphasized,
-        bodySmall = bodySmall,
-        bodySmallEmphasized = bodySmallEmphasized,
-        labelLarge = labelLarge,
-        labelLargeEmphasized = labelLargeEmphasized,
-        labelMedium = labelMedium,
-        labelMediumEmphasized = labelMediumEmphasized,
-        labelSmall = labelSmall,
-        labelSmallEmphasized = labelSmallEmphasized
-    )
-}

+ 23 - 0
app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt

@@ -51,6 +51,29 @@ fun Modifier.responsiveHazeEffect(
     }
 }
 
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun Modifier.responsiveHazeEffectFixedStyle(
+    state: HazeState
+): Modifier {
+    val enableBlur = ThemeConfig.enableBlur
+
+    if (!enableBlur) return this
+
+    val style = HazeLegado.ultraThinPlus()
+
+    return this.hazeEffect(
+        state = state,
+        style = style
+    ) {
+        progressive =
+            HazeProgressive.verticalGradient(
+                startIntensity = 1f,
+                endIntensity = 0f
+            )
+    }
+}
+
 /**
  * 仅判断 enableBlur 的简单 HazeEffect
  */

+ 5 - 3
app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt

@@ -14,14 +14,15 @@ import androidx.core.graphics.scale
 import coil.ImageLoader
 import coil.request.ImageRequest
 import coil.request.SuccessResult
+import coil.size.Size
 import com.materialkolor.quantize.QuantizerCelebi
 import com.materialkolor.score.Score
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 
-private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 256
-private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 128
-private const val IMAGE_MAX_QUANTIZE_COLORS = 128
+private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 128
+private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 64
+private const val IMAGE_MAX_QUANTIZE_COLORS = 64
 private const val IMAGE_FALLBACK_SEED_COLOR = 0xFF4285F4.toInt()
 
 suspend fun ImageLoader.extractSeedColor(
@@ -32,6 +33,7 @@ suspend fun ImageLoader.extractSeedColor(
     val request = ImageRequest.Builder(context)
         .data(data)
         .allowHardware(false)
+        .size(Size(IMAGE_COLOR_EXTRACT_SIZE_PX, IMAGE_COLOR_EXTRACT_SIZE_PX))
         .apply(configureRequest)
         .build()
 

+ 2 - 1
app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt

@@ -73,7 +73,8 @@ data class LegadoColorScheme(
     val onTertiaryFixedVariant: Color,
 
     val cardContainer: Color,
-    val onCardContainer: Color
+    val onCardContainer: Color,
+    val onSheetContent: Color
 )
 
 data class LegadoTypography(

+ 2 - 1
app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt

@@ -68,7 +68,8 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
         onTertiaryFixed = onTertiaryFixed,
         onTertiaryFixedVariant = onTertiaryFixedVariant,
         cardContainer = primaryContainer.copy(alpha = 0.5f),
-        onCardContainer = primary
+        onCardContainer = primary,
+        onSheetContent = surface
     )
 }
 

+ 10 - 8
app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt

@@ -55,6 +55,7 @@ fun ProvideThemeOverride(
     content: @Composable () -> Unit,
 ) {
     var appliedTheme by remember { mutableStateOf<ThemeOverrideState?>(null) }
+    val baseTheme = LocalLegadoThemeColors.current
 
     LaunchedEffect(theme) {
         if (theme == null) {
@@ -66,15 +67,16 @@ fun ProvideThemeOverride(
     }
 
     val currentTheme = appliedTheme
-    if (currentTheme == null) {
-        content()
-    } else {
-        ProvideColorSchemeOverride(
-            colorScheme = currentTheme.colorScheme,
-            seedColor = currentTheme.seedColor,
-            content = content
+        ?: ThemeOverrideState(
+            seedColor = baseTheme.seedColor,
+            colorScheme = baseTheme.colorScheme
         )
-    }
+
+    ProvideColorSchemeOverride(
+        colorScheme = currentTheme.colorScheme,
+        seedColor = currentTheme.seedColor,
+        content = content
+    )
 }
 
 @Composable

+ 30 - 0
app/src/main/java/io/legado/app/ui/theme/Typography.kt

@@ -49,3 +49,33 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography {
         labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium)
     )
 }
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+fun Typography.toLegadoTypography(): LegadoTypography {
+    return LegadoTypography(
+        headlineLarge = headlineLarge,
+        headlineLargeEmphasized = headlineLargeEmphasized,
+        headlineMedium = headlineMedium,
+        headlineMediumEmphasized = headlineMediumEmphasized,
+        headlineSmall = headlineSmall,
+        headlineSmallEmphasized = headlineSmallEmphasized,
+        titleLarge = titleLarge,
+        titleLargeEmphasized = titleLargeEmphasized,
+        titleMedium = titleMedium,
+        titleMediumEmphasized = titleMediumEmphasized,
+        titleSmall = titleSmall,
+        titleSmallEmphasized = titleSmallEmphasized,
+        bodyLarge = bodyLarge,
+        bodyLargeEmphasized = bodyLargeEmphasized,
+        bodyMedium = bodyMedium,
+        bodyMediumEmphasized = bodyMediumEmphasized,
+        bodySmall = bodySmall,
+        bodySmallEmphasized = bodySmallEmphasized,
+        labelLarge = labelLarge,
+        labelLargeEmphasized = labelLargeEmphasized,
+        labelMedium = labelMedium,
+        labelMediumEmphasized = labelMediumEmphasized,
+        labelSmall = labelSmall,
+        labelSmallEmphasized = labelSmallEmphasized
+    )
+}

+ 10 - 0
app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt

@@ -14,6 +14,16 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
 
 object HazeLegado {
 
+    @Composable
+    @ReadOnlyComposable
+    fun ultraThinPlus(
+        containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
+    ): HazeStyle = hazeLegado(
+        containerColor = containerColor,
+        lightAlpha = 0.0f,
+        darkAlpha = 0.0f,
+    )
+
     @Composable
     @ReadOnlyComposable
     fun ultraThin(

+ 34 - 0
app/src/main/java/io/legado/app/ui/widget/components/AppLinearProgressIndicator.kt

@@ -0,0 +1,34 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+// 这里建议使用 as 关键字为两个库的控件起别名,避免同名冲突
+import androidx.compose.material3.LinearProgressIndicator
+// 请替换为实际的 MIUIX 控件包名
+import top.yukonga.miuix.kmp.basic.LinearProgressIndicator as MiuixLinearProgressIndicator
+
+@Composable
+fun AppLinearProgressIndicator(
+    modifier: Modifier = Modifier,
+    progress: Float? = null,
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        MiuixLinearProgressIndicator(
+            modifier = modifier,
+            progress = progress
+        )
+    } else {
+        if (progress != null) {
+            LinearProgressIndicator(
+                progress = { progress },
+                modifier = modifier
+            )
+        } else {
+            LinearProgressIndicator(
+                modifier = modifier
+            )
+        }
+    }
+}

+ 168 - 0
app/src/main/java/io/legado/app/ui/widget/components/AppSearchBar.kt

@@ -0,0 +1,168 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.input.rememberTextFieldState
+import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
+import androidx.compose.material3.ExpandedFullScreenContainedSearchBar
+import androidx.compose.material3.ExpandedFullScreenSearchBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.SearchBarValue
+import androidx.compose.material3.SearchBarValue.Collapsed
+import androidx.compose.material3.SearchBarValue.Expanded
+import androidx.compose.material3.rememberSearchBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ThemeResolver
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import top.yukonga.miuix.kmp.basic.InputField as MiuixInputField
+import top.yukonga.miuix.kmp.basic.SearchBar as MiuixSearchBar
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun AppSearchBar(
+    modifier: Modifier = Modifier,
+    query: String,
+    onQueryChange: (String) -> Unit,
+    onSearch: (String) -> Unit,
+    expanded: Boolean,
+    onExpandedChange: (Boolean) -> Unit,
+    label: String,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    content: @Composable ColumnScope.() -> Unit,
+) {
+    val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+    if (isMiuix) {
+        var internalExpanded by remember { mutableStateOf(expanded) }
+        var internalQuery by remember { mutableStateOf(query) }
+
+        LaunchedEffect(expanded) {
+            internalExpanded = expanded
+        }
+
+        LaunchedEffect(query) {
+            if (query != internalQuery) {
+                internalQuery = query
+            }
+        }
+
+        MiuixSearchBar(
+            modifier = modifier,
+            inputField = {
+                MiuixInputField(
+                    query = internalQuery,
+                    onQueryChange = { newQuery ->
+                        if (newQuery.isNotEmpty() || internalExpanded) {
+                            internalQuery = newQuery
+                            onQueryChange(newQuery)
+                        }
+                    },
+                    onSearch = {
+                        onSearch(it)
+                        onExpandedChange(false)
+                    },
+                    expanded = internalExpanded,
+                    onExpandedChange = { newExpanded ->
+                        internalExpanded = newExpanded
+                        onExpandedChange(newExpanded)
+                    },
+                    label = label,
+                    leadingIcon = leadingIcon,
+                    trailingIcon = trailingIcon,
+                )
+            },
+            expanded = internalExpanded,
+            onExpandedChange = onExpandedChange,
+            content = content,
+        )
+        return
+    }
+
+    val initialSearchBarValue = remember {
+        if (expanded) SearchBarValue.Expanded else SearchBarValue.Collapsed
+    }
+    val searchBarState = rememberSearchBarState(initialValue = initialSearchBarValue)
+    val textFieldState = rememberTextFieldState(initialText = query)
+    val scope = rememberCoroutineScope()
+    val latestQuery by rememberUpdatedState(query)
+    val latestExpanded by rememberUpdatedState(expanded)
+    val latestOnQueryChange by rememberUpdatedState(onQueryChange)
+    val latestOnExpandedChange by rememberUpdatedState(onExpandedChange)
+    val latestOnSearch by rememberUpdatedState(onSearch)
+
+    LaunchedEffect(query) {
+        if (query != textFieldState.text.toString()) {
+            textFieldState.setTextAndPlaceCursorAtEnd(query)
+        }
+    }
+
+    LaunchedEffect(textFieldState) {
+        snapshotFlow { textFieldState.text.toString() }
+            .distinctUntilChanged()
+            .collect { newQuery ->
+                if (newQuery != latestQuery) {
+                    latestOnQueryChange(newQuery)
+                }
+            }
+    }
+
+    LaunchedEffect(expanded) {
+        if (expanded && searchBarState.targetValue != Expanded) {
+            searchBarState.animateToExpanded()
+        } else if (!expanded && searchBarState.targetValue != Collapsed) {
+            searchBarState.animateToCollapsed()
+        }
+    }
+
+    LaunchedEffect(searchBarState) {
+        snapshotFlow { searchBarState.currentValue == Expanded }
+            .distinctUntilChanged()
+            .collect { isExpanded ->
+                if (isExpanded != latestExpanded) {
+                    latestOnExpandedChange(isExpanded)
+                }
+            }
+    }
+
+    val inputField: @Composable () -> Unit = {
+        SearchBarDefaults.InputField(
+            textFieldState = textFieldState,
+            searchBarState = searchBarState,
+            onSearch = { keyword ->
+                latestOnSearch(keyword)
+                scope.launch { searchBarState.animateToCollapsed() }
+            },
+            placeholder = placeholder,
+            leadingIcon = leadingIcon,
+            trailingIcon = trailingIcon,
+        )
+    }
+
+    SearchBar(
+        modifier = modifier.padding(horizontal = 16.dp),
+        state = searchBarState,
+        inputField = inputField
+    )
+
+    ExpandedFullScreenSearchBar(
+        state = searchBarState,
+        inputField = inputField,
+        content = content
+    )
+}

+ 38 - 2
app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt

@@ -28,6 +28,7 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.text.AppText
 import top.yukonga.miuix.kmp.theme.MiuixTheme
+import top.yukonga.miuix.kmp.basic.InputField as MiuixSearchBarInputField
 import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
 
 @OptIn(ExperimentalMaterial3Api::class)
@@ -300,13 +301,48 @@ fun AppDenseTextField(
     scrollState: ScrollState = rememberScrollState(),
     shape: Shape = TextFieldDefaults.shape,
     interactionSource: MutableInteractionSource? = null,
+    miuixUseSearchBarInputField: Boolean = false,
+    miuixSearchBarLabel: String = label ?: "",
+    miuixOnSearch: (String) -> Unit = {},
 ) {
+    val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+    if (isMiuix && miuixUseSearchBarInputField) {
+        MiuixSearchBarInputField(
+            query = state.text.toString(),
+            onQueryChange = { newQuery ->
+                val current = state.text.toString()
+                if (newQuery != current) {
+                    state.edit {
+                        replace(0, length, newQuery)
+                    }
+                }
+            },
+            onSearch = miuixOnSearch,
+            expanded = false,
+            onExpandedChange = {},
+            modifier = modifier.heightIn(min = 45.dp),
+            label = miuixSearchBarLabel,
+            enabled = enabled,
+            leadingIcon = leadingIcon,
+            trailingIcon = trailingIcon,
+            interactionSource = interactionSource
+        )
+        return
+    }
+
+    val denseMinHeight = if (isMiuix) 45.dp else 48.dp
+    val denseBackgroundColor = if (isMiuix && backgroundColor == Color.Unspecified) {
+        MiuixTheme.colorScheme.surfaceContainerHigh
+    } else {
+        backgroundColor
+    }
+
     AppTextField(
         state = state,
-        modifier = modifier.heightIn(min = 48.dp),
+        modifier = modifier.heightIn(min = denseMinHeight),
         enabled = enabled,
         readOnly = readOnly,
-        backgroundColor = backgroundColor,
+        backgroundColor = denseBackgroundColor,
         label = label,
         labelPosition = labelPosition,
         placeholder = placeholder,

+ 3 - 3
app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt → app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt

@@ -28,7 +28,7 @@ import io.legado.app.ui.widget.components.text.AnimatedTextLine
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
-fun EmptyMessageView(
+fun EmptyMessage(
     message: String,
     modifier: Modifier = Modifier,
     isLoading: Boolean = false,
@@ -81,7 +81,7 @@ fun EmptyMessageView(
 }
 
 @Composable
-fun EmptyMessageView(
+fun EmptyMessage(
     @StringRes messageResId: Int,
     modifier: Modifier = Modifier,
     isLoading: Boolean = false,
@@ -93,7 +93,7 @@ fun EmptyMessageView(
     onFaceClick: (() -> Unit)? = null
 ) {
     val message = stringResource(id = messageResId)
-    EmptyMessageView(
+    EmptyMessage(
         message = message,
         modifier = modifier,
         isLoading = isLoading,

+ 17 - 25
app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt

@@ -9,14 +9,9 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
-import androidx.compose.material3.TextFieldDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-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.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -49,7 +44,6 @@ fun SearchBarSection(
     trailingIcon: @Composable (() -> Unit)? = null,
     dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null
 ) {
-    var showMenu by remember { mutableStateOf(false) }
     val textFieldState = rememberTextFieldState(initialText = query)
 
     LaunchedEffect(query) {
@@ -76,30 +70,20 @@ fun SearchBarSection(
         if (isMiuix) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow
     }
 
-    val modifier = modifier
-        .fillMaxWidth()
-        .then(
-            if (isMiuix) modifier.padding(vertical = 4.dp)
-            else modifier
-        )
-
-    val searchTextField = @Composable {
+    if (isMiuix) {
         AppDenseTextField(
             state = textFieldState,
-            modifier = modifier,
+            modifier = modifier
+                .fillMaxWidth()
+                .padding(vertical = 4.dp),
             placeholder = { AppText(placeholder) },
             leadingIcon = leadingIcon,
+            trailingIcon = trailingIcon,
             lineLimits = TextFieldLineLimits.SingleLine,
-            backgroundColor = if (!isMiuix) {
-                Color.Transparent
-            } else {
-                resolvedBackgroundColor
-            }
+            backgroundColor = resolvedBackgroundColor,
+            miuixUseSearchBarInputField = true,
+            miuixSearchBarLabel = placeholder
         )
-    }
-
-    if (isMiuix) {
-        searchTextField()
     } else {
         Surface(
             modifier = Modifier
@@ -108,7 +92,15 @@ fun SearchBarSection(
             shape = RoundedCornerShape(32.dp),
             color = resolvedBackgroundColor
         ) {
-            searchTextField()
+            AppDenseTextField(
+                state = textFieldState,
+                modifier = modifier.fillMaxWidth(),
+                placeholder = { AppText(placeholder) },
+                leadingIcon = leadingIcon,
+                trailingIcon = trailingIcon,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                backgroundColor = Color.Transparent
+            )
         }
     }
 }

+ 2 - 0
app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt

@@ -154,6 +154,7 @@ fun <T> AppAlertDialog(
     data: T?,
     onDismissRequest: () -> Unit,
     title: String? = null,
+    text: String? = null,
     confirmText: String = "确定",
     onConfirm: ((T) -> Unit)? = null,
     dismissText: String = "取消",
@@ -173,6 +174,7 @@ fun <T> AppAlertDialog(
             show = data != null,
             onDismissRequest = onDismissRequest,
             title = title,
+            text = text,
             modifier = modifier,
             confirmText = confirmText,
             onConfirm = onConfirm?.let { { it(currentData) } },

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

@@ -73,7 +73,8 @@ fun SmallIconButton(
         ) {
             MiuixIcon(
                 imageVector = imageVector,
-                contentDescription = contentDescription
+                contentDescription = contentDescription,
+                modifier = Modifier.size(smallIconSize),
             )
         }
     } else {

+ 31 - 6
app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt

@@ -1,13 +1,19 @@
 package io.legado.app.ui.widget.components.button
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
@@ -15,9 +21,11 @@ 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.widget.components.text.AppText
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
 import top.yukonga.miuix.kmp.basic.Button as MiuixButton
 import top.yukonga.miuix.kmp.basic.ButtonDefaults as MiuixButtonDefaults
-import top.yukonga.miuix.kmp.basic.Text as MiuixText
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
 
 @Composable
 fun SmallTextButton(
@@ -27,14 +35,31 @@ fun SmallTextButton(
     onClick: () -> Unit
 ) {
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixButton(
+        Card(
             onClick = onClick,
             modifier = modifier,
-            colors = MiuixButtonDefaults.buttonColors()
-        ) {
-            MiuixText(
-                text = text
+            showIndication = true,
+            colors = CardDefaults.defaultColors(
+                color = LegadoTheme.colorScheme.surfaceVariant,
+                contentColor = LegadoTheme.colorScheme.onSurfaceVariant
             )
+        ) {
+            Row(
+                modifier = Modifier
+                    .padding(horizontal = 12.dp, vertical = 8.dp),
+                horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                MiuixIcon(
+                    imageVector = imageVector,
+                    contentDescription = null,
+                    modifier = Modifier.size(16.dp)
+                )
+                AppText(
+                    text = text,
+                    style = LegadoTheme.typography.labelMedium
+                )
+            }
         }
     } else {
         TextButton(

+ 6 - 4
app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt

@@ -46,14 +46,15 @@ fun ToggleChip(
             Row(
                 verticalAlignment = Alignment.CenterVertically,
                 horizontalArrangement = Arrangement.Center,
-                modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
+                modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
             ) {
                 AnimatedVisibility(visible = selected) {
                     Row(verticalAlignment = Alignment.CenterVertically) {
                         MiuixIcon(
                             imageVector = Icons.Default.Check,
                             contentDescription = checkedContentDescription,
-                            modifier = Modifier.size(16.dp)
+                            modifier = Modifier.size(18.dp),
+                            tint = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface
                         )
                         Spacer(Modifier.width(6.dp))
                     }
@@ -61,9 +62,10 @@ fun ToggleChip(
 
                 MiuixText(
                     text = label,
-                    style = LegadoTheme.typography.labelSmall,
+                    style = LegadoTheme.typography.labelMediumEmphasized,
                     maxLines = 1,
-                    softWrap = false
+                    softWrap = false,
+                    color = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface
                 )
             }
         }

+ 21 - 11
app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.widget.components.card
 
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material3.Card
 import androidx.compose.material3.CardDefaults
@@ -39,15 +40,24 @@ private fun BaseCard(
             color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)),
             contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
         )
-        MiuixCard(
-            modifier = modifier,
-            cornerRadius = cornerRadius,
-            pressFeedbackType = pressFeedbackType,
-            showIndication = true,
-            onClick = onClick,
-            content = content,
-            colors = colors
-        )
+        if (onClick != null) {
+            MiuixCard(
+                modifier = modifier,
+                cornerRadius = cornerRadius,
+                pressFeedbackType = pressFeedbackType,
+                showIndication = true,
+                onClick = onClick,
+                content = content,
+                colors = colors
+            )
+        } else {
+            MiuixCard(
+                modifier = modifier,
+                cornerRadius = cornerRadius,
+                content = content,
+                colors = colors
+            )
+        }
     } else {
         val colors = CardDefaults.cardColors(
             containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy(
@@ -60,7 +70,7 @@ private fun BaseCard(
         if (onClick != null) {
             Card(
                 modifier = modifier,
-                shape = shape,
+                shape = RoundedCornerShape(cornerRadius),
                 colors = colors,
                 elevation = elevation,
                 border = border,
@@ -70,7 +80,7 @@ private fun BaseCard(
         } else {
             Card(
                 modifier = modifier,
-                shape = shape,
+                shape = RoundedCornerShape(cornerRadius),
                 colors = colors,
                 elevation = elevation,
                 border = border,

+ 126 - 95
app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt

@@ -44,6 +44,8 @@ import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.AdaptiveSwitch
 import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.checkBox.AppCheckbox
+import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.text.AppText
 import sh.calvin.reorderable.ReorderableItem
@@ -70,8 +72,6 @@ fun SelectionItemCard(
     containerColor: Color? = null,
     selectedContainerColor: Color? = null
 ) {
-    var showMenu by remember { mutableStateOf(false) }
-
     val composeEngine = ThemeResolver.isMiuixEngine(composeEngine)
     val animatedContainerColor by animateColorAsState(
         targetValue = if (isSelected)
@@ -92,48 +92,104 @@ fun SelectionItemCard(
         containerColor = animatedContainerColor,
         elevation = CardDefaults.cardElevation(defaultElevation = elevation)
     ) {
-        Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .animateContentSize(),
-            verticalAlignment = Alignment.CenterVertically
+        SelectionItemCardContent(
+            title = title,
+            subtitle = subtitle,
+            supportingContent = supportingContent,
+            isEnabled = isEnabled,
+            isSelected = isSelected,
+            inSelectionMode = inSelectionMode,
+            leadingContent = leadingContent,
+            onEnabledChange = onEnabledChange,
+            onClickEdit = onClickEdit,
+            trailingAction = trailingAction,
+            dropdownContent = dropdownContent
+        )
+    }
+}
+
+@Composable
+fun SelectionItemCardContent(
+    title: String,
+    subtitle: String? = null,
+    supportingContent: @Composable (() -> Unit)? = null,
+    isEnabled: Boolean = true,
+    isSelected: Boolean = false,
+    inSelectionMode: Boolean = false,
+    leadingContent: @Composable (() -> Unit)? = null,
+    onEnabledChange: ((Boolean) -> Unit)? = null,
+    onClickEdit: (() -> Unit)? = null,
+    trailingAction: @Composable (RowScope.() -> Unit)? = null,
+    dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null
+) {
+    var showMenu by remember { mutableStateOf(false) }
+
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .animateContentSize(),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        AnimatedVisibility(
+            visible = inSelectionMode || leadingContent != null
         ) {
-            AnimatedVisibility(
-                visible = inSelectionMode || leadingContent != null
+            Box(
+                modifier = Modifier.padding(start = 12.dp),
+                contentAlignment = Alignment.Center
             ) {
-                Box(
-                    modifier = Modifier.padding(start = 12.dp),
-                    contentAlignment = Alignment.Center
-                ) {
-                    AnimatedContent(
-                        targetState = inSelectionMode,
-                        label = "LeadingContent"
-                    ) { selectionMode ->
-                        if (selectionMode) {
-                            Checkbox(
-                                checked = isSelected,
-                                onCheckedChange = null
-                            )
-                        } else {
-                            leadingContent?.invoke()
-                        }
+                AnimatedContent(
+                    targetState = inSelectionMode,
+                    label = "LeadingContent"
+                ) { selectionMode ->
+                    if (selectionMode) {
+                        AppCheckbox(
+                            checked = isSelected,
+                            onCheckedChange = null
+                        )
+                    } else {
+                        leadingContent?.invoke()
                     }
                 }
             }
+        }
 
-            if (composeEngine) {
-                BasicComponent(
-                    modifier = Modifier.weight(1f)
-                ) {
+        if (ThemeResolver.isMiuixEngine(composeEngine)) {
+            BasicComponent(
+                modifier = Modifier.weight(1f)
+            ) {
+                AppText(
+                    text = title,
+                    style = LegadoTheme.typography.titleSmall,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis
+                )
+                when {
+                    supportingContent != null -> supportingContent()
+                    !subtitle.isNullOrBlank() -> {
+                        AppText(
+                            text = subtitle,
+                            style = LegadoTheme.typography.bodySmall,
+                            maxLines = 1,
+                            overflow = TextOverflow.Ellipsis
+                        )
+                    }
+                }
+            }
+        } else {
+            ListItem(
+                modifier = Modifier.weight(1f),
+                headlineContent = {
                     AppText(
                         text = title,
                         style = LegadoTheme.typography.titleSmall,
                         maxLines = 1,
                         overflow = TextOverflow.Ellipsis
                     )
-                    when {
-                        supportingContent != null -> supportingContent()
-                        !subtitle.isNullOrBlank() -> {
+                },
+                supportingContent = when {
+                    supportingContent != null -> supportingContent
+                    !subtitle.isNullOrBlank() -> {
+                        {
                             AppText(
                                 text = subtitle,
                                 style = LegadoTheme.typography.bodySmall,
@@ -142,75 +198,50 @@ fun SelectionItemCard(
                             )
                         }
                     }
-                }
-            } else {
-                ListItem(
-                    modifier = Modifier.weight(1f),
-                    headlineContent = {
-                        AppText(
-                            text = title,
-                            style = LegadoTheme.typography.titleSmall,
-                            maxLines = 1,
-                            overflow = TextOverflow.Ellipsis
-                        )
-                    },
-                    supportingContent = when {
-                        supportingContent != null -> supportingContent
-                        !subtitle.isNullOrBlank() -> {
-                            {
-                                AppText(
-                                    text = subtitle,
-                                    style = LegadoTheme.typography.bodySmall,
-                                    maxLines = 1,
-                                    overflow = TextOverflow.Ellipsis
-                                )
-                            }
-                        }
-                        else -> null
-                    },
-                    colors = ListItemDefaults.colors(containerColor = Color.Transparent)
+                    else -> null
+                },
+                colors = ListItemDefaults.colors(containerColor = Color.Transparent)
+            )
+        }
+
+        Row(
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            modifier = Modifier
+                .padding(end = 8.dp)
+        ) {
+            onEnabledChange?.let {
+                AdaptiveSwitch(
+                    modifier = Modifier.scale(0.8f),
+                    checked = isEnabled,
+                    onCheckedChange = it
                 )
             }
 
-            Row(
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.spacedBy(8.dp),
-                modifier = Modifier
-                    .padding(end = 8.dp)
-            ) {
-                onEnabledChange?.let {
-                    AdaptiveSwitch(
-                        modifier = Modifier.scale(0.8f),
-                        checked = isEnabled,
-                        onCheckedChange = it
-                    )
-                }
+            if (onClickEdit != null) {
+                SmallIconButton(
+                    onClick = onClickEdit,
+                    imageVector = AppIcons.Edit,
+                    contentDescription = "Edit"
+                )
+            }
 
-                if (onClickEdit != null) {
+            if (trailingAction != null) {
+                trailingAction()
+            }
+
+            if (dropdownContent != null) {
+                Box {
                     SmallIconButton(
-                        onClick = onClickEdit,
-                        imageVector = Icons.Default.Edit,
-                        contentDescription = "Edit"
+                        onClick = { showMenu = true },
+                        imageVector = AppIcons.MoreVert,
+                        contentDescription = "More"
                     )
-                }
-
-                if (trailingAction != null) {
-                    trailingAction()
-                }
-
-                if (dropdownContent != null) {
-                    Box {
-                        SmallIconButton(
-                            onClick = { showMenu = true },
-                            imageVector = Icons.Default.MoreVert,
-                            contentDescription = "More"
-                        )
-                        RoundDropdownMenu(
-                            expanded = showMenu,
-                            onDismissRequest = { showMenu = false }
-                        ) {
-                            dropdownContent { showMenu = false }
-                        }
+                    RoundDropdownMenu(
+                        expanded = showMenu,
+                        onDismissRequest = { showMenu = false }
+                    ) {
+                        dropdownContent { showMenu = false }
                     }
                 }
             }

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

@@ -44,8 +44,7 @@ fun TextCard(
 
     NormalCard(
         modifier = modifier,
-        shape = RoundedCornerShape(cornerRadius),
-        cornerRadius = 4.dp,
+        cornerRadius = cornerRadius,
         containerColor = finalBackgroundColor,
         contentColor = finalContentColor,
         onClick = onClick

+ 27 - 26
app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt

@@ -14,44 +14,45 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.text.AppText
 
 @Composable
 fun CheckboxItem(
     title: String,
-    color: Color = Color.Transparent,
+    color: Color = LegadoTheme.colorScheme.onSheetContent,
     checked: Boolean,
     enabled: Boolean = true,
     onCheckedChange: (Boolean) -> Unit
 ) {
     val alpha = if (enabled) 1f else 0.5f
 
-    Row(
-        modifier = Modifier
-            .fillMaxWidth()
-            .clip(MaterialTheme.shapes.small)
-            .background(
-                color = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer
-                else color,
-                shape = MaterialTheme.shapes.small
-            )
-            .clickable(enabled = enabled) { onCheckedChange(!checked) }
-            .padding(vertical = 12.dp, horizontal = 12.dp),
-        verticalAlignment = Alignment.CenterVertically
+    GlassCard(
+        cornerRadius = 12.dp,
+        containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color,
     ) {
-        AppCheckbox(
-            checked = checked,
-            onCheckedChange = null,
-            enabled = enabled,
-            modifier = Modifier.alpha(alpha)
-        )
-        AppText(
-            text = title,
-            style = LegadoTheme.typography.bodyMediumEmphasized,
-            maxLines = 1,
+        Row(
             modifier = Modifier
-                .padding(start = 12.dp)
-                .alpha(alpha)
-        )
+                .fillMaxWidth()
+                .clickable(enabled = enabled) { onCheckedChange(!checked) }
+                .padding(vertical = 12.dp, horizontal = 12.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            AppCheckbox(
+                checked = checked,
+                onCheckedChange = null,
+                enabled = enabled,
+                modifier = Modifier.alpha(alpha)
+            )
+            AppText(
+                text = title,
+                style = LegadoTheme.typography.bodyMediumEmphasized,
+                maxLines = 1,
+                modifier = Modifier
+                    .padding(start = 12.dp)
+                    .alpha(alpha)
+            )
+        }
     }
+
 }

+ 81 - 73
app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt

@@ -39,9 +39,10 @@ import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.withSave
 import coil.compose.AsyncImage
-import coil.request.ImageRequest
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.GlassCard
+import io.legado.app.ui.widget.components.card.NormalCard
 import org.koin.compose.koinInject
 import io.legado.app.model.BookCover as BookCoverModel
 
@@ -81,86 +82,93 @@ fun BookCover(
         isFinalPathLoaded = false
     }
 
-    BoxWithConstraints(
-        modifier = modifier
-            .aspectRatio(5f / 7f)
-            .then(
-                if (CoverConfig.coverShowShadow) {
-                    Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
-                } else Modifier
-            )
-            .clip(RoundedCornerShape(4.dp))
-            .then(
-                if (!hasCustomDefault && !isOnlineCoverLoaded) {
-                    Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
-                } else Modifier
-            )
-    ) {
-        val density = LocalDensity.current
-        val iconSizeDp = with(density) {
-            (minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
-        }
+    NormalCard(
+        cornerRadius = 4.dp
+    ){
+        BoxWithConstraints(
+            modifier = modifier
+                .aspectRatio(5f / 7f)
+                .then(
+                    if (CoverConfig.coverShowShadow) {
+                        Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
+                    } else Modifier
+                )
+                .then(
+                    if (!hasCustomDefault && !isOnlineCoverLoaded) {
+                        Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
+                    } else Modifier
+                )
+        ) {
+            val density = LocalDensity.current
+            val iconSizeDp = with(density) {
+                (minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
+            }
 
-        if (hasCustomDefault && !isFinalPathLoaded) {
-            AsyncImage(
-                model = ImageRequest.Builder(context)
-                    .data(randomPath)
-                    .crossfade(true)
-                    .build(),
-                contentDescription = null,
-                imageLoader = koinInject(),
-                contentScale = ContentScale.Crop,
-                modifier = Modifier.fillMaxSize()
-            )
-        }
+            if (hasCustomDefault && !isFinalPathLoaded) {
+                AsyncImage(
+                    model = buildCoverImageRequest(
+                        context = context,
+                        data = randomPath,
+                        sourceOrigin = null,
+                        loadOnlyWifi = false,
+                        crossfade = true,
+                    ),
+                    contentDescription = null,
+                    imageLoader = koinInject(),
+                    contentScale = ContentScale.Crop,
+                    modifier = Modifier.fillMaxSize()
+                )
+            }
 
-        if (finalPath != null) {
-            AsyncImage(
-                model = ImageRequest.Builder(context)
-                    .data(finalPath)
-                    .crossfade(true)
-                    .setParameter("sourceOrigin", sourceOrigin)
-                    .setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi)
-                    .build(),
-                contentDescription = null,
-                imageLoader = koinInject(),
-                contentScale = ContentScale.Crop,
-                modifier = Modifier.fillMaxSize(),
-                onSuccess = {
-                    isOnlineCoverLoaded = true
-                    isFinalPathLoaded = true
-                    onLoadFinish?.invoke()
-                },
-                onError = {
-                    isOnlineCoverLoaded = false
+            if (finalPath != null) {
+                AsyncImage(
+                    model = buildCoverImageRequest(
+                        context = context,
+                        data = finalPath,
+                        sourceOrigin = sourceOrigin,
+                        loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
+                        crossfade = true,
+                    ),
+                    contentDescription = null,
+                    imageLoader = koinInject(),
+                    contentScale = ContentScale.Crop,
+                    modifier = Modifier.fillMaxSize(),
+                    onSuccess = {
+                        isOnlineCoverLoaded = true
+                        isFinalPathLoaded = true
+                        onLoadFinish?.invoke()
+                    },
+                    onError = {
+                        isOnlineCoverLoaded = false
+                        onLoadFinish?.invoke()
+                    }
+                )
+            } else {
+                LaunchedEffect(Unit) {
                     onLoadFinish?.invoke()
                 }
-            )
-        } else {
-            LaunchedEffect(Unit) {
-                onLoadFinish?.invoke()
             }
-        }
 
-        if (!hasCustomDefault && !isOnlineCoverLoaded) {
-            Icon(
-                Icons.Default.Book,
-                contentDescription = null,
-                tint = LegadoTheme.colorScheme.secondary,
-                modifier = Modifier
-                    .size(iconSizeDp)
-                    .align(Alignment.Center)
-            )
-        }
+            if (!hasCustomDefault && !isOnlineCoverLoaded) {
+                Icon(
+                    Icons.Default.Book,
+                    contentDescription = null,
+                    tint = LegadoTheme.colorScheme.secondary,
+                    modifier = Modifier
+                        .size(iconSizeDp)
+                        .align(Alignment.Center)
+                )
+            }
 
-        if (!isOnlineCoverLoaded) {
-            CoverTextOverlay(
-                name = name,
-                author = author,
-                isNight = isNight
-            )
-        }
+            if (!isOnlineCoverLoaded) {
+                CoverTextOverlay(
+                    name = name,
+                    author = author,
+                    isNight = isNight
+                )
+            }
 
+        }
     }
 }
 

+ 6 - 7
app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt

@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppLinearProgressIndicator
 import io.legado.app.ui.widget.components.card.TextCard
 
 @Composable
@@ -42,6 +43,7 @@ fun BookshelfCover(
             TextCard(
                 text = badgeText,
                 icon = if (showBadgeDot) Icons.Default.Update else null,
+                iconSize = 12.dp,
                 backgroundColor = LegadoTheme.colorScheme.cardContainer,
                 contentColor = LegadoTheme.colorScheme.onCardContainer,
                 modifier = Modifier
@@ -49,7 +51,7 @@ fun BookshelfCover(
                     .padding(2.dp),
                 cornerRadius = 4.dp,
                 horizontalPadding = 4.dp,
-                verticalPadding = 0.dp
+                verticalPadding = 2.dp
             )
         }
 
@@ -63,20 +65,17 @@ fun BookshelfCover(
                     .padding(2.dp),
                 cornerRadius = 4.dp,
                 horizontalPadding = 4.dp,
-                verticalPadding = 0.dp
+                verticalPadding = 2.dp
             )
         }
 
         if (isUpdating) {
-            LinearProgressIndicator(
+            AppLinearProgressIndicator(
                 modifier = Modifier
                     .align(Alignment.BottomCenter)
                     .fillMaxWidth()
                     .padding(horizontal = 4.dp, vertical = 6.dp)
-                    .height(3.dp),
-                color = LegadoTheme.colorScheme.primary,
-                trackColor = LegadoTheme.colorScheme.primary.copy(alpha = 0.2f),
-                strokeCap = StrokeCap.Round
+                    .height(3.dp)
             )
         }
     }

+ 10 - 9
app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt

@@ -24,7 +24,6 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
 import coil.ImageLoader
 import coil.compose.AsyncImage
-import coil.request.ImageRequest
 import io.legado.app.ui.theme.LegadoTheme
 import org.koin.compose.koinInject
 
@@ -57,16 +56,18 @@ fun Cover(
             )
         } else {
             AsyncImage(
-                model = ImageRequest.Builder(context)
-                    .data(path)
-                    .crossfade(true)
-                    .setParameter("sourceOrigin", sourceOrigin)
-                    .setParameter("loadOnlyWifi", loadOnlyWifi)
-                    .listener(
+                model = buildCoverImageRequest(
+                    context = context,
+                    data = path,
+                    sourceOrigin = sourceOrigin,
+                    loadOnlyWifi = loadOnlyWifi,
+                    crossfade = true,
+                ) {
+                    listener(
                         onSuccess = { _, _ -> onLoadFinish?.invoke() },
                         onError = { _, _ -> onLoadFinish?.invoke() }
                     )
-                    .build(),
+                },
                 imageLoader = imageLoader,
                 contentDescription = null,
                 contentScale = ContentScale.Crop,
@@ -93,4 +94,4 @@ fun Cover(
             }
         }
     }
-}
+}

+ 21 - 0
app/src/main/java/io/legado/app/ui/widget/components/cover/CoverImageRequest.kt

@@ -0,0 +1,21 @@
+package io.legado.app.ui.widget.components.cover
+
+import android.content.Context
+import coil.request.ImageRequest
+
+fun buildCoverImageRequest(
+    context: Context,
+    data: Any?,
+    sourceOrigin: String?,
+    loadOnlyWifi: Boolean,
+    crossfade: Boolean = true,
+    configure: ImageRequest.Builder.() -> Unit = {},
+): ImageRequest {
+    return ImageRequest.Builder(context)
+        .data(data)
+        .crossfade(crossfade)
+        .setParameter("sourceOrigin", sourceOrigin)
+        .setParameter("loadOnlyWifi", loadOnlyWifi)
+        .apply(configure)
+        .build()
+}

+ 48 - 0
app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt

@@ -5,11 +5,19 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.LibraryBooks
 import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
 import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Explore
+import androidx.compose.material.icons.filled.FilterList
+import androidx.compose.material.icons.filled.History
+import androidx.compose.material.icons.filled.LocationSearching
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.MyLocation
 import androidx.compose.material.icons.filled.Person
 import androidx.compose.material.icons.filled.Replay
 import androidx.compose.material.icons.filled.RssFeed
 import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.outlined.Explore
 import androidx.compose.material.icons.outlined.Person
 import androidx.compose.material.icons.outlined.RssFeed
@@ -18,15 +26,23 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import io.legado.app.ui.main.MainDestination
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
+import okhttp3.internal.http2.Settings
 import top.yukonga.miuix.kmp.icon.MiuixIcons
 import top.yukonga.miuix.kmp.icon.basic.Search
 import top.yukonga.miuix.kmp.icon.extended.Album
 import top.yukonga.miuix.kmp.icon.extended.Back
 import top.yukonga.miuix.kmp.icon.extended.Close
 import top.yukonga.miuix.kmp.icon.extended.ContactsBook
+import top.yukonga.miuix.kmp.icon.extended.Delete
+import top.yukonga.miuix.kmp.icon.extended.Edit
 import top.yukonga.miuix.kmp.icon.extended.Favorites
+import top.yukonga.miuix.kmp.icon.extended.Filter
+import top.yukonga.miuix.kmp.icon.extended.More
+import top.yukonga.miuix.kmp.icon.extended.Pin
 import top.yukonga.miuix.kmp.icon.extended.Refresh
 import top.yukonga.miuix.kmp.icon.extended.Settings
+import top.yukonga.miuix.kmp.icon.extended.Unpin
+import top.yukonga.miuix.kmp.icon.extended.WorldClock
 
 object AppIcons {
 
@@ -38,6 +54,18 @@ object AppIcons {
         @Composable
         get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search
 
+    val MoreVert: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.More else Icons.Default.MoreVert
+
+    val Edit: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.Edit else Icons.Default.Edit
+
+    val Delete: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.Delete else Icons.Default.Delete
+
     val Close: ImageVector
         @Composable
         get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear
@@ -46,6 +74,26 @@ object AppIcons {
         @Composable
         get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack
 
+    val Filter: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.Filter else Icons.Default.FilterList
+
+    val Settings: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.Settings else Icons.Default.Settings
+
+    val PrecisionSearch: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.Pin else Icons.Default.MyLocation
+
+    val UnPrecisionSearch: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.Unpin else Icons.Default.LocationSearching
+
+    val History: ImageVector
+        @Composable
+        get() = if (isMiuix) MiuixIcons.WorldClock else Icons.Default.History
+
     val Replay: ImageVector
         @Composable
         get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt

@@ -45,7 +45,7 @@ fun RoundDropdownMenu(
 
     if (isMiuix) {
         val popupContentColor = LegadoTheme.colorScheme.onSurface
-        OverlayListPopup(
+        WindowListPopup(
             show = expanded,
             onDismissRequest = onDismissRequest,
             popupModifier = modifier

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt

@@ -137,7 +137,7 @@ fun RoundDropdownMenuItem(
                 else -> defaultContentColor.copy(alpha = 0.38f)
             }
         }
-        val containerColor = if (isSelected) colorScheme.primaryContainer else colorScheme.surface
+        val containerColor = colorScheme.surface
 
         Surface(
             onClick = onClick,

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt

@@ -76,7 +76,7 @@ fun <T> RuleListScaffold(
         show = showDeleteConfirmDialog,
         onDismissRequest = { showDeleteConfirmDialog = false },
         title = stringResource(R.string.delete),
-        text = stringResource(R.string.del_msg),
+        text = stringResource(R.string.sure_del),
         confirmText = stringResource(R.string.ok),
         onConfirm = {
             onDeleteSelected(state.selectedIds)

+ 7 - 3
app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt

@@ -2,6 +2,7 @@
 
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
@@ -30,6 +31,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
+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.widget.components.button.ConfirmDismissButtonsRow
@@ -41,7 +43,6 @@ import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
 @Composable
 fun SliderSettingItem(
     title: String,
-    color: Color? = null,
     value: Float,
     defaultValue: Float,
     valueRange: ClosedFloatingPointRange<Float>,
@@ -56,7 +57,10 @@ fun SliderSettingItem(
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
 
-        Column(modifier = Modifier.fillMaxWidth()) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+        ) {
             BasicComponent(
                 title = title,
                 summary = description,
@@ -143,7 +147,7 @@ fun SliderSettingItem(
     } else {
         SettingItem(
             title = title,
-            color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
+            color = MaterialTheme.colorScheme.surfaceContainerLow,
             option = description,
             expanded = expanded,
             onExpandChange = { expanded = it },

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

@@ -75,7 +75,7 @@ fun <T> DynamicTopAppBar(
             if (!isSelecting) {
                 TopBarActionButton(
                     onClick = { onSearchToggle(!state.isSearch) },
-                    imageVector = Icons.Default.Search,
+                    imageVector = AppIcons.Search,
                     contentDescription = "搜索"
                 )
 

+ 1 - 1
gradle/libs.versions.toml

@@ -17,7 +17,7 @@ kotlinxSerialization = "1.10.0"
 kotlinxCoroutinesAndroid = "1.10.2"
 kotlinxSerializationJson = "1.10.0"
 ksp = "2.3.6"
-agp = "9.1.0"
+agp = "9.1.1"
 appcompat = "1.7.1"
 colorpicker = "1.1.0"
 colorpicker-compose = "1.1.3"