HapeLee 5 месяцев назад
Родитель
Сommit
30fe70ffae
89 измененных файлов с 2274 добавлено и 1557 удалено
  1. 3 0
      app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt
  2. 3 2
      app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt
  3. 6 1
      app/src/main/java/io/legado/app/domain/gateway/ExploreBooksGateway.kt
  4. 10 0
      app/src/main/java/io/legado/app/domain/model/MatchMode.kt
  5. 7 4
      app/src/main/java/io/legado/app/domain/usecase/ExploreBooksUseCase.kt
  6. 7 2
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  7. 11 11
      app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt
  8. 65 0
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowContract.kt
  9. 109 115
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt
  10. 134 115
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  11. 5 5
      app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
  12. 6 6
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt
  13. 13 13
      app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt
  14. 3 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
  15. 20 20
      app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt
  16. 11 11
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt
  17. 35 36
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt
  18. 5 5
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewScreen.kt
  19. 1 1
      app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt
  20. 1 1
      app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt
  21. 111 0
      app/src/main/java/io/legado/app/ui/book/search/SearchSourceSection.kt
  22. 4 4
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt
  23. 5 3
      app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt
  24. 3 13
      app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
  25. 5 5
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt
  26. 5 10
      app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt
  27. 3 3
      app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt
  28. 14 14
      app/src/main/java/io/legado/app/ui/config/themeConfig/LabelColorManageSheet.kt
  29. 3 3
      app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt
  30. 7 7
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
  31. 3 3
      app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt
  32. 7 7
      app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt
  33. 3 3
      app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
  34. 1 1
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  35. 1 1
      app/src/main/java/io/legado/app/ui/main/MainIntent.kt
  36. 12 3
      app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
  37. 1 1
      app/src/main/java/io/legado/app/ui/main/MainNavigator.kt
  38. 10 6
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  39. 47 74
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  40. 4 4
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt
  41. 7 4
      app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt
  42. 2 5
      app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt
  43. 11 11
      app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt
  44. 5 4
      app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt
  45. 6 0
      app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt
  46. 9 9
      app/src/main/java/io/legado/app/ui/main/homepage/manage/SetDetailPage.kt
  47. 5 5
      app/src/main/java/io/legado/app/ui/main/homepage/manage/SetListPage.kt
  48. 11 11
      app/src/main/java/io/legado/app/ui/main/homepage/manage/SourceBrowseDetailPage.kt
  49. 15 16
      app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
  50. 8 27
      app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt
  51. 13 14
      app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt
  52. 3 3
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt
  53. 3 3
      app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt
  54. 3 7
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceScreen.kt
  55. 6 4
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt
  56. 8 0
      app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt
  57. 41 0
      app/src/main/java/io/legado/app/ui/widget/components/AppSlider.kt
  58. 4 4
      app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt
  59. 5 5
      app/src/main/java/io/legado/app/ui/widget/components/GroupManageBottomSheet.kt
  60. 5 5
      app/src/main/java/io/legado/app/ui/widget/components/JsonRawEditor.kt
  61. 5 5
      app/src/main/java/io/legado/app/ui/widget/components/ValueStepper.kt
  62. 0 43
      app/src/main/java/io/legado/app/ui/widget/components/button/AnimatedTextButton.kt
  63. 0 217
      app/src/main/java/io/legado/app/ui/widget/components/button/MediumIconButton.kt
  64. 0 66
      app/src/main/java/io/legado/app/ui/widget/components/button/MediumTextButton.kt
  65. 0 356
      app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt
  66. 0 142
      app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt
  67. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/button/series/AnimatedActionButtonCore.kt
  68. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/button/series/AnimatedIcon.kt
  69. 173 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumAnimatedButton.kt
  70. 78 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumOutlinedButton.kt
  71. 121 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumPlainButton.kt
  72. 152 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumToggleButton.kt
  73. 74 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumTonalButton.kt
  74. 176 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallAnimatedButton.kt
  75. 93 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallOutlinedButton.kt
  76. 161 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallPlainButton.kt
  77. 178 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallToggleButton.kt
  78. 91 0
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallTonalButton.kt
  79. 5 11
      app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
  80. 7 8
      app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapCalendarComponents.kt
  81. 7 7
      app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt
  82. 3 3
      app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt
  83. 1 3
      app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/OptionSheet.kt
  84. 5 15
      app/src/main/java/io/legado/app/ui/widget/components/rules/RuleEditSheet.kt
  85. 25 4
      app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassMediumFlexibleTopAppBar.kt
  86. 7 14
      app/src/main/java/io/legado/app/ui/widget/components/topbar/TopBarButton.kt
  87. 30 6
      app/src/main/java/io/legado/app/web/socket/BookSearchWebSocket.kt
  88. 4 2
      app/src/main/res/values-zh-rCN/strings.xml
  89. 2 0
      app/src/main/res/values/strings.xml

+ 3 - 0
app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt

@@ -4,10 +4,13 @@ import android.content.Context
 import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.preferencesDataStore
 
 val Context.localDataStore: DataStore<Preferences> by preferencesDataStore(name = "local_ui_status")
 
 object LocalPreferencesKeys {
     val SHOW_THEME_REFACTOR_TIP = booleanPreferencesKey("show_theme_refactor_tip")
+    val SEARCH_LAYOUT_MODE = intPreferencesKey("search_layout_mode")
+    val MATCH_MODE = intPreferencesKey("match_mode")
 }

+ 3 - 2
app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt

@@ -80,9 +80,10 @@ class ExploreRepositoryImpl(
     override suspend fun exploreBooks(
         bookSource: BookSource,
         url: String,
-        page: Int
+        page: Int,
+        key: String?
     ): List<SearchBook> {
-        return WebBook.exploreBookSuspend(bookSource, url, page)
+        return WebBook.exploreBookSuspend(bookSource, url, page, key = key, isSearch = key != null)
     }
 
     override suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind> = withContext(IO) {

+ 6 - 1
app/src/main/java/io/legado/app/domain/gateway/ExploreBooksGateway.kt

@@ -5,5 +5,10 @@ import io.legado.app.data.entities.SearchBook
 
 interface ExploreBooksGateway {
     suspend fun getBookSource(sourceUrl: String): BookSource?
-    suspend fun exploreBooks(bookSource: BookSource, url: String, page: Int): List<SearchBook>
+    suspend fun exploreBooks(
+        bookSource: BookSource,
+        url: String,
+        page: Int,
+        key: String? = null
+    ): List<SearchBook>
 }

+ 10 - 0
app/src/main/java/io/legado/app/domain/model/MatchMode.kt

@@ -0,0 +1,10 @@
+package io.legado.app.domain.model
+
+enum class MatchMode(val value: Int) {
+    DEFAULT(0),
+    EXACT(1);
+
+    companion object {
+        fun of(value: Int) = entries.getOrElse(value) { DEFAULT }
+    }
+}

+ 7 - 4
app/src/main/java/io/legado/app/domain/usecase/ExploreBooksUseCase.kt

@@ -20,14 +20,17 @@ class ExploreBooksUseCase(
         sourceUrl: String,
         moduleUrl: String?,
         args: String?,
-        page: Int = 1
+        page: Int = 1,
+        key: String? = null,
     ): ExploreResult = withContext(Dispatchers.IO) {
         val base = gateway.getBookSource(sourceUrl)
             ?: throw SourceNotFound(sourceUrl)
         val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base
-        val resolvedUrl = moduleUrl ?: source.exploreUrl
-        ?: throw NoExploreUrl(sourceUrl)
-        val books = gateway.exploreBooks(source, resolvedUrl, page)
+        val resolvedUrl = moduleUrl
+            ?: (if (key != null) source.searchUrl else null)
+            ?: source.exploreUrl
+            ?: throw NoExploreUrl(sourceUrl)
+        val books = gateway.exploreBooks(source, resolvedUrl, page, key = key)
         ExploreResult(resolvedUrl, books)
     }
 

+ 7 - 2
app/src/main/java/io/legado/app/model/webBook/WebBook.kt

@@ -109,9 +109,12 @@ object WebBook {
         url: String,
         page: Int? = 1,
         ruleData: RuleDataInterface = RuleData(),
+        key: String? = null,
+        isSearch: Boolean = false,
     ): ArrayList<SearchBook> {
         val analyzeUrl = AnalyzeUrl(
             mUrl = url,
+            key = key,
             page = page,
             baseUrl = bookSource.bookSourceUrl,
             source = bookSource,
@@ -137,7 +140,7 @@ object WebBook {
             analyzeUrl = analyzeUrl,
             baseUrl = res.url,
             body = res.body,
-            isSearch = false
+            isSearch = isSearch
         )
     }
 
@@ -146,8 +149,10 @@ object WebBook {
         url: String,
         page: Int? = 1,
         ruleData: RuleDataInterface = RuleData(),
+        key: String? = null,
+        isSearch: Boolean = false,
     ): List<SearchBook> {
-        return exploreBookAwait(bookSource, url, page, ruleData)
+        return exploreBookAwait(bookSource, url, page, ruleData, key, isSearch)
     }
 
     suspend fun exploreBookWithResolvedUrl(

+ 11 - 11
app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt

@@ -40,7 +40,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.SmallTonalIconButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.icon.AppIcon
@@ -347,7 +347,7 @@ private fun BookCacheBookCard(
                     color = LegadoTheme.colorScheme.onSurfaceVariant
                 )
                 if (item.hasDownloadTask || item.cachedCount < item.totalCount) {
-                    SmallTonalIconButton(
+                    SmallTonalButton(
                         onClick = {
                             if (item.hasActiveDownload) {
                                 onIntent(BookCacheManageIntent.StopBookDownload(item.bookUrl))
@@ -355,7 +355,7 @@ private fun BookCacheBookCard(
                                 onIntent(BookCacheManageIntent.StartBookDownload(item.bookUrl))
                             }
                         },
-                        imageVector = if (item.hasActiveDownload) Icons.Default.Stop else Icons.Default.PlayArrow,
+                        icon = if (item.hasActiveDownload) Icons.Default.Stop else Icons.Default.PlayArrow,
                         contentDescription = when {
                             item.hasActiveDownload -> "暂停本书下载"
                             item.isPaused -> "继续本书下载"
@@ -363,9 +363,9 @@ private fun BookCacheBookCard(
                         }
                     )
                 }
-                SmallTonalIconButton(
+                SmallTonalButton(
                     onClick = { onDeleteBook(item) },
-                    imageVector = Icons.Default.Delete,
+                    icon = Icons.Default.Delete,
                     contentDescription = null
                 )
             }
@@ -412,21 +412,21 @@ private fun BookCacheChapterRow(
             )
         }
         if (item.isWaiting || item.isDownloading) {
-            SmallTonalIconButton(
+            SmallTonalButton(
                 onClick = onStop,
-                imageVector = Icons.Default.Stop,
+                icon = Icons.Default.Stop,
                 contentDescription = "暂停章节下载"
             )
         } else if (item.isPaused || !item.isCached) {
-            SmallTonalIconButton(
+            SmallTonalButton(
                 onClick = onDownload,
-                imageVector = Icons.Default.Download,
+                icon = Icons.Default.Download,
                 contentDescription = if (item.isPaused) "继续章节下载" else "下载章节"
             )
         }
-        SmallTonalIconButton(
+        SmallTonalButton(
             onClick = onDelete,
-            imageVector = Icons.Default.Delete,
+            icon = Icons.Default.Delete,
             contentDescription = null
         )
     }

+ 65 - 0
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowContract.kt

@@ -0,0 +1,65 @@
+package io.legado.app.ui.book.explore
+
+import androidx.compose.runtime.Stable
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.data.entities.rule.ExploreKind
+import io.legado.app.domain.model.BookShelfState
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+@Stable
+data class ExploreShowUiState(
+    val sourceUrl: String? = null,
+    val books: ImmutableList<ExploreBookItemUi> = persistentListOf(),
+    val kinds: ImmutableList<ExploreKind> = persistentListOf(),
+    val selectedKindTitle: String? = null,
+    val layoutState: Int = 0,
+    val gridCount: Int = 3,
+    val isLoading: Boolean = false,
+    val isRefreshing: Boolean = false,
+    val isEnd: Boolean = false,
+    val errorMsg: String? = null,
+    val sheet: ExploreShowSheet = ExploreShowSheet.None,
+)
+
+@Stable
+data class ExploreBookItemUi(
+    val book: SearchBook,
+    val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
+)
+
+sealed interface ExploreShowSheet {
+    data object None : ExploreShowSheet
+    data object KindSelect : ExploreShowSheet
+    data object GridCount : ExploreShowSheet
+}
+
+sealed interface ExploreShowIntent {
+    data class InitData(
+        val sourceUrl: String?,
+        val exploreUrl: String?,
+    ) : ExploreShowIntent
+
+    data object LoadMore : ExploreShowIntent
+    data object Refresh : ExploreShowIntent
+    data class SwitchKind(val kind: ExploreKind) : ExploreShowIntent
+    data object ToggleLayout : ExploreShowIntent
+    data class SaveGridCount(val count: Int) : ExploreShowIntent
+    data class ShowSheet(val sheet: ExploreShowSheet) : ExploreShowIntent
+    data object DismissSheet : ExploreShowIntent
+    data class OpenBook(val book: SearchBook, val sharedCoverKey: String?) : ExploreShowIntent
+    data class AddToShelf(val book: SearchBook) : ExploreShowIntent
+}
+
+sealed interface ExploreShowEffect {
+    data class OpenBookInfo(
+        val name: String,
+        val author: String,
+        val bookUrl: String,
+        val origin: String?,
+        val coverPath: String?,
+        val sharedCoverKey: String?,
+    ) : ExploreShowEffect
+
+    data class ShowMessage(val message: String) : ExploreShowEffect
+}

+ 109 - 115
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -6,7 +6,6 @@ import androidx.compose.animation.AnimatedVisibilityScope
 import androidx.compose.animation.Crossfade
 import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
-import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
@@ -33,11 +32,8 @@ import androidx.compose.material.icons.filled.GridView
 import androidx.compose.material.icons.outlined.FilterAlt
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Slider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -48,19 +44,19 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import dev.chrisbanes.haze.HazeState
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.domain.model.BookShelfState
-import io.legado.app.domain.usecase.ExploreKindUiUseCase
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.main.bookCoverSharedElementKey
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.responsiveHazeEffect
 import io.legado.app.ui.theme.responsiveHazeSource
 import io.legado.app.ui.widget.components.AppPullToRefresh
 import io.legado.app.ui.widget.components.AppScaffold
+import io.legado.app.ui.widget.components.AppSlider
 import io.legado.app.ui.widget.components.LoadMoreFooter
 import io.legado.app.ui.widget.components.book.SearchBookGridItem
 import io.legado.app.ui.widget.components.book.SearchBookListItem
@@ -74,7 +70,17 @@ import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import org.koin.androidx.compose.koinViewModel
-import org.koin.compose.koinInject
+
+private enum class BookFilterState(val id: Int) {
+    SHOW_ALL(0),
+    HIDE_IN_SHELF(1),
+    HIDE_SAME_NAME_AUTHOR(2),
+    SHOW_NOT_IN_SHELF_ONLY(3);
+
+    companion object {
+        fun fromId(id: Int) = entries.getOrElse(id) { SHOW_ALL }
+    }
+}
 
 @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
 @OptIn(
@@ -83,55 +89,51 @@ import org.koin.compose.koinInject
 )
 @Composable
 fun ExploreShowScreen(
-    title: String,
-    sourceUrl: String?,
-    exploreUrl: String?,
+    viewModel: ExploreShowViewModel = koinViewModel(),
+    title: String = "",
     onBack: () -> Unit,
     onBookClick: (SearchBook, String?) -> Unit,
-    viewModel: ExploreShowViewModel = koinViewModel(),
     sharedTransitionScope: SharedTransitionScope? = null,
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
-    var previewBook by remember { mutableStateOf<SearchBook?>(null) }
-    var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
+    val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+    LaunchedEffect(viewModel) {
+        viewModel.effects.collect { effect ->
+            when (effect) {
+                is ExploreShowEffect.OpenBookInfo -> onBookClick(
+                    SearchBook(
+                        name = effect.name,
+                        author = effect.author,
+                        bookUrl = effect.bookUrl,
+                        origin = effect.origin ?: "",
+                        coverUrl = effect.coverPath,
+                    ),
+                    effect.sharedCoverKey,
+                )
 
-    LaunchedEffect(sourceUrl, exploreUrl, viewModel) {
-        viewModel.initData(sourceUrl, exploreUrl)
+                is ExploreShowEffect.ShowMessage -> {}
+            }
+        }
     }
 
-    val rawBooks by viewModel.uiBooks.collectAsState()
+    var previewBook by remember { mutableStateOf<SearchBook?>(null) }
+    var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
+
     val filterStateId = CoverConfig.exploreFilterState
-    val books = remember(rawBooks, filterStateId) {
+    val books = remember(state.books, filterStateId) {
         val filter = BookFilterState.fromId(filterStateId)
         when (filter) {
-            BookFilterState.SHOW_ALL -> rawBooks
-            BookFilterState.HIDE_IN_SHELF -> rawBooks.filter { it.shelfState != BookShelfState.IN_SHELF }
-            BookFilterState.HIDE_SAME_NAME_AUTHOR -> rawBooks.filter { it.shelfState != BookShelfState.SAME_NAME_AUTHOR }
-            BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> rawBooks.filter { it.shelfState == BookShelfState.NOT_IN_SHELF }
+            BookFilterState.SHOW_ALL -> state.books
+            BookFilterState.HIDE_IN_SHELF -> state.books.filter { it.shelfState != BookShelfState.IN_SHELF }
+            BookFilterState.HIDE_SAME_NAME_AUTHOR -> state.books.filter { it.shelfState != BookShelfState.SAME_NAME_AUTHOR }
+            BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> state.books.filter { it.shelfState == BookShelfState.NOT_IN_SHELF }
         }
     }
-    val isBookEnd by viewModel.isEnd.collectAsState()
-    val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
-    val isLoading by viewModel.isLoading.collectAsState()
-    val errorMsg by viewModel.errorMsg.collectAsState()
-    val selectedTitle by viewModel.selectedKindTitle.collectAsState()
     val listState = rememberLazyListState()
     val gridState = rememberLazyGridState()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-    var showKindSheet by remember { mutableStateOf(false) }
-    val layoutState by viewModel.layoutState.collectAsState()
-    val isGridMode = layoutState == 1
-    var showGridCountSheet by remember { mutableStateOf(false) }
-    val gridColumnCount by viewModel.gridCount.collectAsState()
-    val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
-    val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
-
-    LaunchedEffect(sourceUrl) {
-        exploreKindUseCase.warmUp(sourceUrl)
-    }
-
-    val isRefreshing by viewModel.isRefreshing.collectAsState()
-
+    val isGridMode = state.layoutState == 1
     val hazeState = remember { HazeState() }
     val shouldLoadMoreList = remember {
         derivedStateOf {
@@ -150,17 +152,11 @@ fun ExploreShowScreen(
     }
 
     LaunchedEffect(shouldLoadMoreList.value, isGridMode) {
-        if (!isGridMode && shouldLoadMoreList.value) viewModel.loadMore()
+        if (!isGridMode && shouldLoadMoreList.value) viewModel.onIntent(ExploreShowIntent.LoadMore)
     }
 
     LaunchedEffect(shouldLoadMoreGrid.value, isGridMode) {
-        if (isGridMode && shouldLoadMoreGrid.value) viewModel.loadMore()
-    }
-
-    LaunchedEffect(shouldTriggerAutoLoad) {
-        if (shouldTriggerAutoLoad) {
-            viewModel.loadMore()
-        }
+        if (isGridMode && shouldLoadMoreGrid.value) viewModel.onIntent(ExploreShowIntent.LoadMore)
     }
 
     LaunchedEffect(isGridMode) {
@@ -175,17 +171,14 @@ fun ExploreShowScreen(
         }
     }
 
-
     AppModalBottomSheet(
-        show = showGridCountSheet,
-        modifier = Modifier
-            .padding(16.dp),
-        onDismissRequest = { showGridCountSheet = false }
+        show = state.sheet == ExploreShowSheet.GridCount,
+        onDismissRequest = { viewModel.onIntent(ExploreShowIntent.DismissSheet) }
     ) {
         Row(
             modifier = Modifier
                 .fillMaxWidth()
-                .padding(bottom = 16.dp),
+                .padding(bottom = 32.dp),
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement = Arrangement.Center
         ) {
@@ -193,49 +186,38 @@ fun ExploreShowScreen(
                 text = "布局列数",
                 style = LegadoTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)
             )
-
             Spacer(modifier = Modifier.width(12.dp))
-
             TextCard(
-                text = "$gridColumnCount 列",
+                text = "${state.gridCount} 列",
                 textStyle = LegadoTheme.typography.titleSmall,
+                backgroundColor = LegadoTheme.colorScheme.onSheetContent,
                 verticalPadding = 4.dp,
                 horizontalPadding = 12.dp,
                 cornerRadius = 12.dp
             )
         }
 
-        Slider(
-            value = gridColumnCount.toFloat(),
+        AppSlider(
+            value = state.gridCount.toFloat(),
             onValueChange = {
-                val col = it.toInt().coerceIn(1, 10)
-                viewModel.saveGridCount(col)
+                viewModel.onIntent(ExploreShowIntent.SaveGridCount(it.toInt().coerceIn(1, 10)))
             },
             valueRange = 1f..10f,
             steps = 8,
             modifier = Modifier.padding(horizontal = 20.dp)
         )
 
-        Spacer(Modifier.height(24.dp))
+        Spacer(modifier = Modifier.height(32.dp))
 
-        OutlinedButton(
-            onClick = { showGridCountSheet = false },
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp)
-        ) {
-            AppText("完成")
-        }
     }
 
-
     ExploreKindSelectSheet(
-        show = showKindSheet,
-        onDismissRequest = { showKindSheet = false },
-        sourceUrl = sourceUrl,
+        show = state.sheet == ExploreShowSheet.KindSelect,
+        onDismissRequest = { viewModel.onIntent(ExploreShowIntent.DismissSheet) },
+        sourceUrl = state.sourceUrl,
         onSelected = { selectedKinds ->
             selectedKinds.firstOrNull()?.let { kind ->
-                viewModel.switchExploreUrl(kind)
+                viewModel.onIntent(ExploreShowIntent.SwitchKind(kind))
             }
         }
     )
@@ -245,39 +227,39 @@ fun ExploreShowScreen(
             .nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
             GlassMediumFlexibleTopAppBar(
-                modifier = Modifier.responsiveHazeEffect(
-                    state = hazeState
-                ),
-                title = selectedTitle ?: title,
+                modifier = Modifier.responsiveHazeEffect(state = hazeState),
+                title = state.selectedKindTitle ?: title,
                 navigationIcon = {
                     TopBarNavigationButton(onClick = onBack)
                 },
                 actions = {
-                    Row(
-                        horizontalArrangement = Arrangement.End,
-                        modifier = Modifier.animateContentSize(tween(300))
+
+                    AnimatedVisibility(
+                        visible = isGridMode,
+                        enter = fadeIn(tween(300)),
+                        exit = fadeOut(tween(300))
                     ) {
                         TopBarActionButton(
-                            onClick = { showKindSheet = true },
-                            imageVector = Icons.Outlined.FilterAlt,
-                            contentDescription = "分类"
+                            onClick = {
+                                viewModel.onIntent(
+                                    ExploreShowIntent.ShowSheet(
+                                        ExploreShowSheet.GridCount
+                                    )
+                                )
+                            },
+                            imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
+                            contentDescription = "列数设置"
                         )
-
-                        AnimatedVisibility(
-                            visible = isGridMode,
-                            enter = fadeIn(tween(300)),
-                            exit = fadeOut(tween(300))
-                        ) {
-                            TopBarActionButton(
-                                onClick = { showGridCountSheet = true },
-                                imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
-                                contentDescription = "列数设置"
-                            )
-                        }
                     }
 
                     TopBarActionButton(
-                        onClick = { viewModel.setLayout() },
+                        onClick = { viewModel.onIntent(ExploreShowIntent.ShowSheet(ExploreShowSheet.KindSelect)) },
+                        imageVector = Icons.Outlined.FilterAlt,
+                        contentDescription = "分类"
+                    )
+
+                    TopBarActionButton(
+                        onClick = { viewModel.onIntent(ExploreShowIntent.ToggleLayout) },
                         imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView,
                         contentDescription = "切换布局"
                     )
@@ -288,8 +270,8 @@ fun ExploreShowScreen(
     ) { paddingValues ->
         AppPullToRefresh(
             modifier = Modifier.fillMaxSize(),
-            isRefreshing = isRefreshing,
-            onRefresh = { viewModel.loadMore(isRefresh = true) },
+            isRefreshing = state.isRefreshing,
+            onRefresh = { viewModel.onIntent(ExploreShowIntent.Refresh) },
             topPadding = paddingValues.calculateTopPadding()
         ) {
             Crossfade(
@@ -303,7 +285,7 @@ fun ExploreShowScreen(
                         modifier = Modifier
                             .fillMaxSize()
                             .responsiveHazeSource(hazeState),
-                        columns = GridCells.Fixed(gridColumnCount),
+                        columns = GridCells.Fixed(state.gridCount),
                         contentPadding = PaddingValues(
                             top = paddingValues.calculateTopPadding() + 12.dp,
                             bottom = paddingValues.calculateBottomPadding() + 12.dp,
@@ -324,7 +306,14 @@ fun ExploreShowScreen(
                             ExploreBookGridItem(
                                 book = item.book,
                                 shelfState = item.shelfState,
-                                onClick = { onBookClick(item.book, sharedCoverKey) },
+                                onClick = {
+                                    viewModel.onIntent(
+                                        ExploreShowIntent.OpenBook(
+                                            item.book,
+                                            sharedCoverKey
+                                        )
+                                    )
+                                },
                                 onLongClick = { book, coverKey ->
                                     previewBook = book
                                     previewSharedCoverKey = coverKey
@@ -338,10 +327,10 @@ fun ExploreShowScreen(
 
                         item(span = { GridItemSpan(maxLineSpan) }) {
                             LoadMoreFooter(
-                                isLoading = isLoading,
-                                errorMsg = errorMsg,
-                                isEnd = isBookEnd,
-                                onRetry = viewModel::loadMore
+                                isLoading = state.isLoading,
+                                errorMsg = state.errorMsg,
+                                isEnd = state.isEnd,
+                                onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) }
                             )
                         }
                     }
@@ -367,7 +356,14 @@ fun ExploreShowScreen(
                             ExploreBookItem(
                                 book = item.book,
                                 shelfState = item.shelfState,
-                                onClick = { onBookClick(item.book, sharedCoverKey) },
+                                onClick = {
+                                    viewModel.onIntent(
+                                        ExploreShowIntent.OpenBook(
+                                            item.book,
+                                            sharedCoverKey
+                                        )
+                                    )
+                                },
                                 onLongClick = { book, coverKey ->
                                     previewBook = book
                                     previewSharedCoverKey = coverKey
@@ -381,15 +377,14 @@ fun ExploreShowScreen(
 
                         item {
                             LoadMoreFooter(
-                                isLoading = isLoading,
-                                errorMsg = errorMsg,
-                                isEnd = isBookEnd,
-                                onRetry = viewModel::loadMore
+                                isLoading = state.isLoading,
+                                errorMsg = state.errorMsg,
+                                isEnd = state.isEnd,
+                                onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) }
                             )
                         }
                     }
                 }
-
             }
         }
     }
@@ -408,7 +403,7 @@ fun ExploreShowScreen(
             onBookClick(book, sharedCoverKey)
         },
         onAddToShelf = { book ->
-            viewModel.onAddToShelf(book)
+            viewModel.onIntent(ExploreShowIntent.AddToShelf(book))
         },
     )
 }
@@ -454,10 +449,9 @@ fun ExploreBookGridItem(
         shelfState = shelfState,
         onClick = onClick,
         onLongClick = onLongClick,
-        modifier = modifier,
+        modifier = modifier.padding(4.dp),
         sharedTransitionScope = sharedTransitionScope,
         animatedVisibilityScope = animatedVisibilityScope,
         sharedCoverKey = sharedCoverKey
     )
 }
-

+ 134 - 115
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt

@@ -6,7 +6,6 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.data.repository.ExploreRepository
-import io.legado.app.domain.model.BookShelfState
 import io.legado.app.domain.usecase.AddToBookshelfUseCase
 import io.legado.app.domain.usecase.BookShelfKey
 import io.legado.app.domain.usecase.ExploreBooksUseCase
@@ -15,43 +14,16 @@ import io.legado.app.domain.usecase.SaveSearchBooksUseCase
 import io.legado.app.help.config.AppConfig
 import io.legado.app.utils.exploreLayoutGrid
 import io.legado.app.utils.stackTraceStr
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import splitties.init.appCtx
 
-sealed class BookFilterState(val id: Int) {
-    data object SHOW_ALL : BookFilterState(0)
-    data object HIDE_IN_SHELF : BookFilterState(1)
-    data object HIDE_SAME_NAME_AUTHOR : BookFilterState(2)
-    data object SHOW_NOT_IN_SHELF_ONLY : BookFilterState(3)
-
-    companion object {
-        fun fromId(id: Int): BookFilterState = when (id) {
-            1 -> HIDE_IN_SHELF
-            2 -> HIDE_SAME_NAME_AUTHOR
-            3 -> SHOW_NOT_IN_SHELF_ONLY
-            else -> SHOW_ALL
-        }
-    }
-}
-
-sealed class UiState<out T> {
-    data object Loading : UiState<Nothing>()
-    data class Success<T>(val data: T) : UiState<T>()
-    data class Error(val message: String) : UiState<Nothing>()
-    data object Empty : UiState<Nothing>()
-}
-
-data class ExploreBookItemUi(
-    val book: SearchBook,
-    val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
-)
-
 class ExploreShowViewModel(
     private val repository: ExploreRepository,
     private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
@@ -61,72 +33,128 @@ class ExploreShowViewModel(
 ) : ViewModel() {
 
     private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
+    private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
     private val _isLoading = MutableStateFlow(false)
-    private val _isRefreshing = MutableStateFlow(false)
+    private val _isEnd = MutableStateFlow(false)
     private val _errorMsg = MutableStateFlow<String?>(null)
+    private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
+    private val _selectedKindTitle = MutableStateFlow<String?>(null)
+
     private var bookSource: BookSource? = null
     private var sourceUrl: String? = null
     private var exploreUrl: String? = null
     private var page = 1
-    private val _isEndStateFlow = MutableStateFlow(false)
-    private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
-    private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
-    val kinds = _kinds.asStateFlow()
-    private val _selectedKindTitle = MutableStateFlow<String?>(null)
-    val selectedKindTitle = _selectedKindTitle.asStateFlow()
-    private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格
-    val layoutState: StateFlow<Int> = _layoutState.asStateFlow()
-    private val _gridCount = MutableStateFlow(appCtx.exploreLayoutGrid)
-    val gridCount = _gridCount.asStateFlow()
-    val isEnd: StateFlow<Boolean> = _isEndStateFlow.asStateFlow()
-
-    val isRefreshing = _isRefreshing.asStateFlow()
-
-    fun saveGridCount(value: Int) {
-        appCtx.exploreLayoutGrid = value
-        _gridCount.value = value
+
+    private val _uiState = MutableStateFlow(
+        ExploreShowUiState(
+            layoutState = AppConfig.exploreLayoutState,
+            gridCount = appCtx.exploreLayoutGrid,
+        )
+    )
+    val uiState = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<ExploreShowEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    init {
+        observeBookshelf()
+        combineUiState()
     }
 
-    val uiBooks = combine(
-        _rawBooks,
-        _bookshelf
-    ) { books, bookshelf ->
-        books.map { item ->
-            ExploreBookItemUi(
-                book = item,
-                shelfState = resolveBookShelfStateUseCase.execute(
-                    name = item.name,
-                    author = item.author,
-                    url = item.bookUrl,
-                    shelf = bookshelf
+    fun onIntent(intent: ExploreShowIntent) {
+        when (intent) {
+            is ExploreShowIntent.InitData -> initData(intent.sourceUrl, intent.exploreUrl)
+            ExploreShowIntent.LoadMore -> loadMore()
+            ExploreShowIntent.Refresh -> loadMore(isRefresh = true)
+            is ExploreShowIntent.SwitchKind -> switchKind(intent.kind)
+            ExploreShowIntent.ToggleLayout -> toggleLayout()
+            is ExploreShowIntent.SaveGridCount -> saveGridCount(intent.count)
+            is ExploreShowIntent.ShowSheet -> _uiState.update { it.copy(sheet = intent.sheet) }
+            ExploreShowIntent.DismissSheet -> _uiState.update { it.copy(sheet = ExploreShowSheet.None) }
+            is ExploreShowIntent.OpenBook -> emitEffect(
+                ExploreShowEffect.OpenBookInfo(
+                    name = intent.book.name,
+                    author = intent.book.author,
+                    bookUrl = intent.book.bookUrl,
+                    origin = intent.book.origin,
+                    coverPath = intent.book.coverUrl,
+                    sharedCoverKey = intent.sharedCoverKey,
                 )
             )
-        }
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
-
-    val shouldTriggerAutoLoad = combine(
-        _isLoading,
-        uiBooks,
-        _rawBooks,
-        _isEndStateFlow
-    ) { loading, uiBooks, rawBooks, isEnd ->
-        !loading && uiBooks.isEmpty() && rawBooks.isNotEmpty() && !isEnd
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
 
-    val isLoading = _isLoading.asStateFlow()
-    val errorMsg = _errorMsg.asStateFlow()
+            is ExploreShowIntent.AddToShelf -> viewModelScope.launch {
+                addToBookshelfUseCase.execute(intent.book)
+            }
+        }
+    }
 
-    init {
+    private fun observeBookshelf() {
         viewModelScope.launch {
             repository.getBookshelfItems().collect { list ->
-                val keys = list.map { BookShelfKey(it.name, it.author, it.bookUrl) }.toSet()
-                _bookshelf.value = keys
+                _bookshelf.value = list.map {
+                    BookShelfKey(it.name, it.author, it.bookUrl)
+                }.toSet()
+            }
+        }
+    }
+
+    private fun combineUiState() {
+        viewModelScope.launch {
+            combine(
+                _rawBooks,
+                _bookshelf,
+                _isLoading,
+                _isEnd,
+                _errorMsg,
+                _kinds,
+                _selectedKindTitle,
+            ) { values ->
+                @Suppress("UNCHECKED_CAST")
+                val rawBooks = values[0] as List<SearchBook>
+                val bookshelf = values[1] as Set<BookShelfKey>
+                val isLoading = values[2] as Boolean
+                val isEnd = values[3] as Boolean
+                val errorMsg = values[4] as String?
+                val kinds = values[5] as List<ExploreKind>
+                val selectedKindTitle = values[6] as String?
+
+                val books = rawBooks.map { item ->
+                    ExploreBookItemUi(
+                        book = item,
+                        shelfState = resolveBookShelfStateUseCase.execute(
+                            name = item.name,
+                            author = item.author,
+                            url = item.bookUrl,
+                            shelf = bookshelf,
+                        )
+                    )
+                }
+
+                ExploreShowUiState(
+                    sourceUrl = sourceUrl,
+                    books = books.toImmutableList(),
+                    kinds = kinds.toImmutableList(),
+                    selectedKindTitle = selectedKindTitle,
+                    layoutState = _uiState.value.layoutState,
+                    gridCount = _uiState.value.gridCount,
+                    isLoading = isLoading,
+                    isRefreshing = isLoading && page == 1,
+                    isEnd = isEnd,
+                    errorMsg = errorMsg,
+                    sheet = _uiState.value.sheet,
+                )
+            }.collect { newState ->
+                val oldState = _uiState.value
+                _uiState.value = newState
+
+                if (newState.books.isEmpty() && _rawBooks.value.isNotEmpty() && !newState.isEnd && !newState.isLoading) {
+                    loadMore()
+                }
             }
         }
     }
 
-    fun initData(incomingSourceUrl: String?, incomingExploreUrl: String?) {
-        // 允许 incomingExploreUrl 为空,此时加载书源默认发现页
+    private fun initData(incomingSourceUrl: String?, incomingExploreUrl: String?) {
         if (sourceUrl == incomingSourceUrl && exploreUrl == incomingExploreUrl && bookSource != null) {
             return
         }
@@ -135,16 +163,16 @@ class ExploreShowViewModel(
         page = 1
         bookSource = null
         _rawBooks.value = emptyList()
-        _isEndStateFlow.value = false
+        _isEnd.value = false
         _errorMsg.value = null
         _selectedKindTitle.value = null
+        _kinds.value = emptyList()
 
         viewModelScope.launch {
             if (bookSource == null && incomingSourceUrl != null) {
                 bookSource = repository.getBookSource(incomingSourceUrl)
             }
 
-            // 如果仍然没有发现 URL,且书源已加载,尝试使用书源的默认发现页
             if (exploreUrl == null && bookSource != null) {
                 loadKinds(incomingSourceUrl!!)
             }
@@ -153,33 +181,36 @@ class ExploreShowViewModel(
         }
     }
 
-    fun loadKinds(sourceUrl: String) {
+    private fun loadKinds(sourceUrl: String) {
         viewModelScope.launch {
             _kinds.value = repository.getSourceExploreKinds(sourceUrl)
         }
     }
 
-    fun refreshKinds() {
-        sourceUrl?.let { loadKinds(it) }
-    }
-
-    fun switchExploreUrl(kind: ExploreKind) {
+    private fun switchKind(kind: ExploreKind) {
         _selectedKindTitle.value = kind.title
         exploreUrl = kind.url
-        _isEndStateFlow.value = false
+        _isEnd.value = false
         loadMore(isRefresh = true)
     }
 
-    fun setLayout() {
-        val newState = if (_layoutState.value == 0) 1 else 0
-        _layoutState.value = newState
-        AppConfig.exploreLayoutState = newState
+    private fun toggleLayout() {
+        _uiState.update {
+            val newState = if (it.layoutState == 0) 1 else 0
+            AppConfig.exploreLayoutState = newState
+            it.copy(layoutState = newState)
+        }
+    }
+
+    private fun saveGridCount(count: Int) {
+        appCtx.exploreLayoutGrid = count
+        _uiState.update { it.copy(gridCount = count) }
     }
 
-    fun loadMore(isRefresh: Boolean = false) {
+    private fun loadMore(isRefresh: Boolean = false) {
         val source = bookSource
         val url = exploreUrl ?: source?.exploreUrl
-        if (source == null || url == null || _isLoading.value || (_isEndStateFlow.value && !isRefresh)) return
+        if (source == null || url == null || _isLoading.value || (_isEnd.value && !isRefresh)) return
 
         viewModelScope.launch {
             _isLoading.value = true
@@ -187,7 +218,7 @@ class ExploreShowViewModel(
 
             if (isRefresh) {
                 page = 1
-                _isEndStateFlow.value = false
+                _isEnd.value = false
                 _rawBooks.value = emptyList()
             }
 
@@ -195,7 +226,7 @@ class ExploreShowViewModel(
                 exploreBooksUseCase.execute(source.bookSourceUrl, url, args = null, page)
             }.onSuccess { result ->
                 if (result.books.isEmpty()) {
-                    _isEndStateFlow.value = true
+                    _isEnd.value = true
                 } else {
                     saveSearchBooksUseCase.save(result.books)
 
@@ -207,34 +238,22 @@ class ExploreShowViewModel(
                         .distinctBy { it.bookUrl }
 
                     if (uniqueNewBooks.isEmpty()) {
-                        _isEndStateFlow.value = true
+                        _isEnd.value = true
                     } else {
                         _rawBooks.value = currentList + uniqueNewBooks
                         page++
-                        _isEndStateFlow.value = false
+                        _isEnd.value = false
                     }
                 }
+            }.onFailure {
+                _errorMsg.value = it.stackTraceStr
             }
-                .onFailure {
-                    _errorMsg.value = it.stackTraceStr
-                }
 
             _isLoading.value = false
         }
     }
 
-    fun getCurrentBookShelfState(item: SearchBook): BookShelfState {
-        return resolveBookShelfStateUseCase.execute(
-            name = item.name,
-            author = item.author,
-            url = item.bookUrl,
-            shelf = _bookshelf.value
-        )
-    }
-
-    fun onAddToShelf(book: SearchBook) {
-        viewModelScope.launch {
-            addToBookshelfUseCase.execute(book)
-        }
+    private fun emitEffect(effect: ExploreShowEffect) {
+        _effects.tryEmit(effect)
     }
 }

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

@@ -34,7 +34,7 @@ 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.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.image.cover.CoilBookCover
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -236,11 +236,11 @@ fun GroupDeleteAction(
 ) {
     var showDeleteDialog by remember { mutableStateOf(false) }
 
-    MediumIconButton(
+    MediumPlainButton(
         onClick = {
             showDeleteDialog = true
         },
-        imageVector = Icons.Default.Delete
+        icon = Icons.Default.Delete
     )
 
     AppAlertDialog(
@@ -266,7 +266,7 @@ fun GroupResetCoverAction(
     onCoverPathChange: (String?) -> Unit,
     viewModel: GroupViewModel = koinViewModel()
 ) {
-    MediumIconButton(
+    MediumPlainButton(
         onClick = {
             if (group != null) {
                 viewModel.clearCover(group) {
@@ -277,6 +277,6 @@ fun GroupResetCoverAction(
                 onCoverPathChange(null)
             }
         },
-        imageVector = Icons.Default.Restore
+        icon = Icons.Default.Restore
     )
 }

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

@@ -61,8 +61,8 @@ import io.legado.app.ui.widget.components.AppPullToRefresh
 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
-import io.legado.app.ui.widget.components.button.SmallTonalIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
@@ -427,9 +427,9 @@ private fun ImportPathNavigationBar(
         }
 
         if (canGoBack) {
-            SmallTonalIconButton(
+            SmallTonalButton(
                 onClick = onNavigateBack,
-                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                icon = Icons.AutoMirrored.Filled.ArrowBack,
                 contentDescription = "back"
             )
         }
@@ -511,9 +511,9 @@ private fun ImportBookItem(
 
             if (!item.isDir && !item.isOnBookShelf) {
                 Spacer(modifier = Modifier.width(8.dp))
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = onAddToBookshelf,
-                    imageVector = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline
+                    icon = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline
                 )
             }
         }

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

@@ -76,9 +76,9 @@ 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.series.MediumPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 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
@@ -167,9 +167,9 @@ fun RemoteBookScreen(
         },
         endAction = if (showSheet is RemoteBookSheet.Servers) {
             {
-                MediumIconButton(
+                MediumPlainButton(
                     onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
-                    imageVector = Icons.Default.Add
+                    icon = Icons.Default.Add
                 )
             }
         } else {
@@ -430,16 +430,16 @@ private fun ServerItem(
             {
                 Row {
                     onEdit?.let {
-                        SmallIconButton(
+                        SmallPlainButton(
                             onClick = it,
-                            imageVector = Icons.Default.Edit,
+                            icon = Icons.Default.Edit,
                             contentDescription = "Edit"
                         )
                     }
                     onDelete?.let {
-                        SmallIconButton(
+                        SmallPlainButton(
                             onClick = it,
-                            imageVector = Icons.Default.Delete,
+                            icon = Icons.Default.Delete,
                             contentDescription = "Delete"
                         )
                     }
@@ -602,9 +602,9 @@ private fun PathNavigationBar(
         }
 
         if (canGoBack) {
-            SmallTonalIconButton(
+            SmallTonalButton(
                 onClick = onNavigateBack,
-                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                icon = Icons.AutoMirrored.Filled.ArrowBack,
                 contentDescription = "返回上级"
             )
         }
@@ -703,7 +703,7 @@ private fun RemoteBookItem(
             if (!book.isDir) {
                 Spacer(modifier = Modifier.width(8.dp))
 
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = {
                         if (book.isOnBookShelf) {
                             onUpdateClick(book)
@@ -711,7 +711,7 @@ private fun RemoteBookItem(
                             onAddClick(book)
                         }
                     },
-                    imageVector = if (book.isOnBookShelf)
+                    icon = if (book.isOnBookShelf)
                         Icons.Outlined.CloudSync
                     else
                         Icons.Outlined.AddCircleOutline,

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

@@ -89,7 +89,7 @@ import io.legado.app.ui.widget.components.AppPullToRefresh
 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.SmallTonalIconButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.icon.AppIcon
@@ -1122,9 +1122,9 @@ private fun RelatedBooksBanner(
                     fontWeight = FontWeight.Bold,
                     modifier = Modifier.weight(1f),
                 )
-                SmallTonalIconButton(
+                SmallTonalButton(
                     onClick = onMoreClick,
-                    imageVector = Icons.AutoMirrored.Filled.ArrowForward,
+                    icon = Icons.AutoMirrored.Filled.ArrowForward,
                     contentDescription = "more",
                 )
             }

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

@@ -72,8 +72,8 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.MediumIconButton
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 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.checkBox.AppCheckbox
@@ -144,15 +144,15 @@ fun GroupSelectSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.group_select),
         startAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = { editingGroup = BookGroup() },
-                imageVector = Icons.Default.Add
+                icon = Icons.Default.Add
             )
         },
         endAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = { onConfirm(selectedGroupId) },
-                imageVector = Icons.Default.Check
+                icon = Icons.Default.Check
             )
         }
     ) {
@@ -186,9 +186,9 @@ fun GroupSelectSheet(
                             )
                         },
                         trailingAction = {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { editingGroup = group },
-                                imageVector = Icons.Default.Edit
+                                icon = Icons.Default.Edit
                             )
                         },
                         containerColor = LegadoTheme.colorScheme.surfaceContainerLow
@@ -228,9 +228,9 @@ fun ChangeCoverSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.change_cover_source),
         endAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = { viewModel.startOrStopSearch() },
-                imageVector = if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh
+                icon = if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh
             )
         }
     ) {
@@ -362,9 +362,9 @@ fun ChangeSourceSheet(
         startAction = {
             Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                 Box {
-                    MediumIconButton(
+                    MediumPlainButton(
                         onClick = { showOptionsMenu = true },
-                        imageVector = Icons.Default.MoreVert
+                        icon = Icons.Default.MoreVert
                     )
                     RoundDropdownMenu(
                         expanded = showOptionsMenu,
@@ -411,21 +411,21 @@ fun ChangeSourceSheet(
                         )
                     }
                 }
-                MediumIconButton(
+                MediumPlainButton(
                     onClick = { showMigrationOptions = true },
-                    imageVector = Icons.Outlined.Settings
+                    icon = Icons.Outlined.Settings
                 )
             }
         },
         endAction = {
             Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
-                MediumIconButton(
+                MediumPlainButton(
                     onClick = { viewModel.startOrStopSearch() },
-                    imageVector = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
+                    icon = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
                 )
-                MediumIconButton(
+                MediumPlainButton(
                     onClick = { showFilterSheet = true },
-                    imageVector = Icons.Default.FilterList
+                    icon = Icons.Default.FilterList
                 )
             }
         }
@@ -476,11 +476,11 @@ fun ChangeSourceSheet(
                         containerColor = LegadoTheme.colorScheme.onSheetContent,
                         selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f),
                         leadingContent = {
-                            MediumIconButton(
+                            MediumPlainButton(
                                 onClick = {
                                     viewModel.onBookScoreClick(item)
                                 },
-                                imageVector = Icons.Default.PushPin,
+                                icon = Icons.Default.PushPin,
                                 tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
                                 contentDescription = null
                             )

+ 11 - 11
app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt

@@ -59,7 +59,7 @@ import io.legado.app.ui.theme.fadingEdge
 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.MediumOutlinedIconButton
+import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
 import io.legado.app.ui.widget.components.image.cover.CoilBookCover
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -156,7 +156,7 @@ fun BookInfoEditContent(
                 horizontalAlignment = Alignment.CenterHorizontally
             ) {
                 Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
-                    MediumOutlinedIconButton(
+                    MediumOutlinedButton(
                         onClick = {
                             (context as? BookInfoEditActivity)?.showDialogFragment(
                                 ChangeCoverDialog(
@@ -165,15 +165,15 @@ fun BookInfoEditContent(
                                 )
                             )
                         },
-                        imageVector = Icons.Default.ImageSearch
+                        icon = Icons.Default.ImageSearch
                     )
-                    MediumOutlinedIconButton(
+                    MediumOutlinedButton(
                         onClick = { selectCover.launch() },
-                        imageVector = Icons.Default.FolderOpen
+                        icon = Icons.Default.FolderOpen
                     )
-                    MediumOutlinedIconButton(
+                    MediumOutlinedButton(
                         onClick = { viewModel.resetCover() },
-                        imageVector = Icons.Default.Replay
+                        icon = Icons.Default.Replay
                     )
                 }
                 Spacer(modifier = Modifier.height(4.dp))
@@ -371,19 +371,19 @@ fun KindEditor(
             }
         }
         Spacer(modifier = Modifier.width(8.dp))
-        MediumOutlinedIconButton(
+        MediumOutlinedButton(
             onClick = {
                 onReset()
             },
-            imageVector = Icons.Default.Replay
+            icon = Icons.Default.Replay
         )
         Spacer(modifier = Modifier.width(8.dp))
-        MediumOutlinedIconButton(
+        MediumOutlinedButton(
             onClick = {
                 editingIndex = -1
                 editText = ""
             },
-            imageVector = Icons.Default.Add
+            icon = Icons.Default.Add
         )
     }
 

+ 35 - 36
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt

@@ -82,8 +82,7 @@ import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.FabMenuItem
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.SmallTonalIconButton
-import io.legado.app.ui.widget.components.button.SmallTonalTextButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -768,32 +767,32 @@ private fun BookshelfManageScreen(
                                             }
                                         )
                                     }
-                                    SmallTonalIconButton(
+                                    SmallTonalButton(
                                         onClick = {
                                             if (!book.isLocal) {
                                                 viewModel.dispatch(BookshelfManageScreenIntent.ToggleBookDownload(book))
                                             }
                                         },
-                                        imageVector = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
+                                        icon = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
                                         contentDescription = "download"
                                     )
-                                    SmallTonalIconButton(
+                                    SmallTonalButton(
                                         onClick = { exportBook(book) },
-                                        imageVector = Icons.Default.Upload,
+                                        icon = Icons.Default.Upload,
                                         contentDescription = "upload"
                                     )
-                                    SmallTonalIconButton(
+                                    SmallTonalButton(
                                         onClick = {
                                             pendingMoveGroupBookUrl = book.bookUrl
                                             groupPickerCurrentGroupId = book.group.coerceAtLeast(0L)
                                             showGroupSelectSheet = true
                                         },
-                                        imageVector = Icons.Default.Bookmarks,
+                                        icon = Icons.Default.Bookmarks,
                                         contentDescription = "group"
                                     )
-                                    SmallTonalIconButton(
+                                    SmallTonalButton(
                                         onClick = { moreMenuBookUrl = book.bookUrl },
-                                        imageVector = Icons.Default.MoreVert,
+                                        icon = Icons.Default.MoreVert,
                                         contentDescription = "more"
                                     )
                                 }
@@ -1288,12 +1287,12 @@ private fun BookSourcePickerSheet(
         onDismissRequest = onDismissRequest,
         title = title,
         endAction = {
-            SmallTonalTextButton(
-                text = stringResource(android.R.string.ok),
-                imageVector = Icons.Default.PlayArrow,
+            SmallTonalButton(
                 onClick = {
                     onConfirm(selectedSources.mapNotNull { it.getBookSource() })
-                }
+                },
+                icon = Icons.Default.PlayArrow,
+                text = stringResource(android.R.string.ok)
             )
         }
     ) {
@@ -1402,17 +1401,17 @@ private fun BatchChangePreviewSheet(
         onDismissRequest = onDismissRequest,
         title = "批量换源预览",
         startAction = {
-            SmallTonalTextButton(
-                text = "新增全部",
-                imageVector = Icons.Default.Add,
+            SmallTonalButton(
                 onClick = onAddAllToShelf,
+                icon = Icons.Default.Add,
+                text = "新增全部"
             )
         },
         endAction = {
-            SmallTonalTextButton(
-                text = "迁移全部",
-                imageVector = Icons.Default.PlayArrow,
+            SmallTonalButton(
                 onClick = onMigrateAll,
+                icon = Icons.Default.PlayArrow,
+                text = "迁移全部"
             )
         }
     ) { items ->
@@ -1498,30 +1497,30 @@ private fun BatchChangePreviewRow(
                 modifier = Modifier.fillMaxWidth(),
                 horizontalArrangement = Arrangement.SpaceBetween
             ) {
-                SmallTonalTextButton(
-                    imageVector = Icons.Default.Search,
+                SmallTonalButton(
                     onClick = { onManualSearch(item.oldBook) },
+                    icon = Icons.Default.Search
                 )
-                SmallTonalTextButton(
-                    text = "不迁移",
-                    imageVector = Icons.Default.Clear,
+                SmallTonalButton(
                     onClick = { onSkip(item.oldBook.bookUrl) },
+                    icon = Icons.Default.Clear,
+                    text = "不迁移"
                 )
-                SmallTonalTextButton(
-                    text = "迁移",
-                    imageVector = Icons.Default.PlayArrow,
+                SmallTonalButton(
                     onClick = { onMigrate(item.oldBook.bookUrl) },
+                    icon = Icons.Default.PlayArrow,
+                    text = "迁移"
                 )
-                SmallTonalTextButton(
-                    text = "新增",
-                    imageVector = Icons.Default.Add,
+                SmallTonalButton(
                     onClick = { onAddToShelf(item.oldBook.bookUrl) },
+                    icon = Icons.Default.Add,
+                    text = "新增"
                 )
                 if (item.candidates.size > 1) {
-                    SmallTonalTextButton(
-                        text = "查看其他源信息",
-                        imageVector = Icons.Default.Info,
+                    SmallTonalButton(
                         onClick = { onShowOtherSources(item) },
+                        icon = Icons.Default.Info,
+                        text = "查看其他源信息"
                     )
                 }
             }
@@ -1605,9 +1604,9 @@ private fun OtherSourceOptionsSheet(
                     isSelected = index == currentItem.selectedCandidateIndex,
                     onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) },
                     trailingAction = {
-                        SmallTonalIconButton(
+                        SmallTonalButton(
                             onClick = { onOpenBook(candidate.book) },
-                            imageVector = Icons.Default.Info,
+                            icon = Icons.Default.Info,
                             contentDescription = null,
                         )
                     },

+ 5 - 5
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewScreen.kt

@@ -54,7 +54,7 @@ import io.legado.app.ui.book.readRecord.component.StatsGridCard
 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.button.MediumIconButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.heatmap.HeatmapMode
 import io.legado.app.ui.widget.components.image.cover.CoilBookCover
@@ -187,9 +187,9 @@ fun DateNavigator(
         horizontalArrangement = Arrangement.SpaceBetween,
         verticalAlignment = Alignment.CenterVertically
     ) {
-        MediumIconButton(
+        MediumPlainButton(
             onClick = onPrevClick,
-            imageVector = Icons.AutoMirrored.Filled.ArrowLeft
+            icon = Icons.AutoMirrored.Filled.ArrowLeft
         )
         AnimatedContent(
             targetState = referenceDate,
@@ -224,9 +224,9 @@ fun DateNavigator(
                 modifier = Modifier.padding(horizontal = 16.dp)
             )
         }
-        MediumIconButton(
+        MediumPlainButton(
             onClick = onNextClick,
-            imageVector = Icons.AutoMirrored.Filled.ArrowRight
+            icon = Icons.AutoMirrored.Filled.ArrowRight
         )
     }
 }

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt

@@ -25,7 +25,7 @@ import io.legado.app.R
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.SearchBar
-import io.legado.app.ui.widget.components.button.MediumPlainButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt

@@ -71,7 +71,7 @@ import io.legado.app.ui.widget.components.SearchBar
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.book.SearchBookListItem
 import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
-import io.legado.app.ui.widget.components.button.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.icon.AppIcon

+ 111 - 0
app/src/main/java/io/legado/app/ui/book/search/SearchSourceSection.kt

@@ -0,0 +1,111 @@
+package io.legado.app.ui.book.search
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.ui.main.bookCoverSharedElementKey
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.adaptiveHorizonalPadding
+import io.legado.app.ui.theme.adaptiveHorizontalPadding
+import io.legado.app.ui.theme.fadingEdge
+import io.legado.app.ui.widget.components.book.SearchBookGridItem
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
+import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.text.AppText
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun SearchSourceSection(
+    sourceName: String,
+    items: List<SearchResultItemUi>,
+    onClickBook: (SearchBook, String?) -> Unit,
+    onLongClickBook: ((SearchBook, String?) -> Unit)? = null,
+    onViewAll: (() -> Unit)? = null,
+    modifier: Modifier = Modifier,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+    sourceSectionIndex: Int = 0,
+) {
+    if (items.isEmpty()) return
+
+    Column(modifier = modifier) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .adaptiveHorizontalPadding()
+                .padding(vertical = 12.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            AppText(
+                text = sourceName,
+                style = LegadoTheme.typography.titleSmall,
+                fontWeight = FontWeight.Bold,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+            )
+            TextCard(
+                text = "${items.size}",
+                modifier = Modifier
+                    .padding(start = 12.dp)
+            )
+
+            Spacer(modifier = Modifier.weight(1f))
+
+            if (onViewAll != null) {
+                SmallTonalButton(
+                    onClick = onViewAll,
+                    icon = Icons.AutoMirrored.Filled.ArrowForward
+                )
+            }
+        }
+
+        val lazyListState = rememberLazyListState()
+        LazyRow(
+            state = lazyListState,
+            modifier = Modifier
+                .fillMaxWidth()
+                .fadingEdge(lazyListState, gradientWidth = 16.dp),
+            horizontalArrangement = Arrangement.spacedBy(12.dp),
+            contentPadding = adaptiveHorizonalPadding(),
+        ) {
+            itemsIndexed(
+                items = items,
+                key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
+            ) { index, item ->
+                val sharedCoverKey = bookCoverSharedElementKey(
+                    item.book.bookUrl,
+                    "search_source:$sourceSectionIndex:$index"
+                )
+                SearchBookGridItem(
+                    book = item.book,
+                    shelfState = item.shelfState,
+                    onClick = { onClickBook(item.book, sharedCoverKey) },
+                    onLongClick = onLongClickBook,
+                    modifier = Modifier.width(100.dp),
+                    sharedTransitionScope = sharedTransitionScope,
+                    animatedVisibilityScope = animatedVisibilityScope,
+                    sharedCoverKey = sharedCoverKey,
+                )
+            }
+        }
+    }
+}

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

@@ -59,10 +59,10 @@ import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SearchBar
-import io.legado.app.ui.widget.components.button.MediumOutlinedButton
-import io.legado.app.ui.widget.components.button.SmallPlainButton
-import io.legado.app.ui.widget.components.button.SmallToggleButton
-import io.legado.app.ui.widget.components.button.ToggleStyle
+import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallToggleButton
+import io.legado.app.ui.widget.components.button.series.ToggleStyle
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn

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

@@ -95,7 +95,8 @@ import io.legado.app.ui.widget.components.FabMenuItem
 import io.legado.app.ui.widget.components.SelectionBottomBar
 import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
 import io.legado.app.ui.widget.components.bookmark.BookmarkItem
-import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
+import io.legado.app.ui.widget.components.button.series.SmallToggleButton
+import io.legado.app.ui.widget.components.button.series.ToggleStyle
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.divider.PillDivider
@@ -476,10 +477,11 @@ fun TocScreen(
 
                         if (pagerState.currentPage == 0 && hasVolumes) {
                             Box {
-                                SmallOutlinedIconToggleButton(
+                                SmallToggleButton(
                                     checked = showVolumeMenu,
                                     onCheckedChange = { showVolumeMenu = it },
-                                    imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
+                                    style = ToggleStyle.Outlined,
+                                    icon = Icons.AutoMirrored.Filled.FormatListBulleted,
                                     contentDescription = stringResource(R.string.volume_management)
                                 )
                                 RoundDropdownMenu(

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

@@ -5,24 +5,15 @@ import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.filled.FileOpen
-import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
 import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -48,7 +39,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 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
@@ -59,7 +50,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.rules.RuleEditFields
 import io.legado.app.ui.widget.components.rules.RuleEditSheet
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
-import io.legado.app.ui.widget.components.text.AppText
 import org.koin.androidx.compose.koinViewModel
 import sh.calvin.reorderable.rememberReorderableLazyListState
 
@@ -350,9 +340,9 @@ fun TxtRuleScreen(
                         onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enable = enabled)) },
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         trailingAction = {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                imageVector = AppIcons.Delete
+                                icon = AppIcons.Delete
                             )
                         }
                     )

+ 5 - 5
app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt

@@ -23,7 +23,7 @@ import io.legado.app.R
 import io.legado.app.help.DefaultData
 import io.legado.app.model.BookCover
 import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.button.MediumIconButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.utils.toastOnUi
@@ -59,8 +59,8 @@ fun CoverRuleConfigSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.cover_rule),
         startAction = {
-            MediumIconButton(
-                imageVector = Icons.Default.SettingsBackupRestore,
+            MediumPlainButton(
+                icon = Icons.Default.SettingsBackupRestore,
                 onClick = {
                     DefaultData.coverRule.let {
                         enable = it.enable
@@ -72,8 +72,8 @@ fun CoverRuleConfigSheet(
             )
         },
         endAction = {
-            MediumIconButton(
-                imageVector = Icons.Default.Save,
+            MediumPlainButton(
+                icon = Icons.Default.Save,
                 onClick = {
                     if (searchUrl.isBlank() || coverRule.isBlank()) {
                         appCtx.toastOnUi(R.string.cover_rule_fields_required)

+ 5 - 10
app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt

@@ -2,7 +2,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
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -16,12 +15,8 @@ import androidx.compose.material.icons.filled.ContentCopy
 import androidx.compose.material.icons.filled.ContentPaste
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material3.Button
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -39,7 +34,7 @@ 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.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -70,18 +65,18 @@ fun DirectLinkUploadBottomSheet(
         show = show,
         title = stringResource(R.string.direct_link_upload_config),
         startAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = {
                     viewModel.testRule { result -> showTestResult = result }
                 },
-                imageVector = Icons.Default.Checklist
+                icon = Icons.Default.Checklist
             )
         },
         endAction = {
             Box {
-                MediumIconButton(
+                MediumPlainButton(
                     onClick = { showMenu = true },
-                    imageVector = Icons.Default.MoreVert
+                    icon = Icons.Default.MoreVert
                 )
                 RoundDropdownMenu(
                     expanded = showMenu,

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

@@ -27,7 +27,7 @@ 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.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcon
@@ -114,13 +114,13 @@ fun BackgroundImageManageSheet(
                             contentScale = ContentScale.Crop
                         )
                     }
-                    SmallTonalIconButton(
+                    SmallTonalButton(
                         onClick = { viewModel.removeBackground(isDark) },
                         modifier = Modifier
                             .align(Alignment.TopEnd)
                             .padding(8.dp)
                             .size(32.dp),
-                        imageVector = Icons.Default.Close
+                        icon = Icons.Default.Close
                     )
                 }
             }

+ 14 - 14
app/src/main/java/io/legado/app/ui/config/themeConfig/LabelColorManageSheet.kt

@@ -5,21 +5,19 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.AutoAwesome
 import androidx.compose.material.icons.filled.Delete
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.toMutableStateList
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.toMutableStateList
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -30,8 +28,8 @@ import androidx.core.graphics.ColorUtils
 import io.legado.app.R
 import io.legado.app.help.config.TagColorGenerator
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
@@ -57,7 +55,7 @@ fun LabelColorManageSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.theme_config_manage_label_colors),
         startAction = {
-            MediumOutlinedIconButton(
+            MediumOutlinedButton(
                 onClick = {
                     val baseColor = if (themeColor != 0) Color(themeColor) else primaryColor
                     val generatedColors = TagColorGenerator.generateTagColors(baseColor)
@@ -65,23 +63,25 @@ fun LabelColorManageSheet(
                     tagColors.addAll(generatedColors)
                     ThemeConfig.saveCustomTagColors(tagColors)
                 },
-                imageVector = Icons.Default.AutoAwesome
+                icon = Icons.Default.AutoAwesome
             )
         },
         endAction = {
-            MediumOutlinedIconButton(
+            MediumOutlinedButton(
                 onClick = {
                     tagColors.add(TagColorPair(0, 0))
                     editingIndex = tagColors.size - 1
                     editingTextColor = 0
                     showColorPicker = true
                 },
-                imageVector = Icons.Default.Add
+                icon = Icons.Default.Add
             )
         }
     ) {
         LazyColumn(
-            modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(bottom = 24.dp),
             verticalArrangement = Arrangement.spacedBy(8.dp)
         ) {
             items(tagColors.size) { index ->
@@ -110,20 +110,20 @@ fun LabelColorManageSheet(
                         Row(
                             horizontalArrangement = Arrangement.spacedBy(4.dp)
                         ) {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = {
                                     editingIndex = index
                                     editingTextColor = colorPair.textColor
                                     showColorPicker = true
                                 },
-                                imageVector = Icons.Default.Edit
+                                icon = Icons.Default.Edit
                             )
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = {
                                     tagColors.removeAt(index)
                                     ThemeConfig.saveCustomTagColors(tagColors)
                                 },
-                                imageVector = Icons.Default.Delete
+                                icon = Icons.Default.Delete
                             )
                         }
                     }

+ 3 - 3
app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt

@@ -30,7 +30,7 @@ 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.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -123,13 +123,13 @@ fun NavIconManageSheet(
                                         modifier = Modifier.fillMaxSize(),
                                         contentScale = ContentScale.Fit
                                     )
-                                    SmallTonalIconButton(
+                                    SmallTonalButton(
                                         onClick = { dest.onSetPath("") },
                                         modifier = Modifier
                                             .align(Alignment.TopEnd)
                                             .padding(4.dp)
                                             .size(24.dp),
-                                        imageVector = Icons.Default.Close
+                                        icon = Icons.Default.Close
                                     )
                                 }
                             } else {

+ 7 - 7
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -88,7 +88,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.SplicedColumnGroup
 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.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcons
@@ -222,8 +222,8 @@ fun ThemeConfigScreen(
                                 style = LegadoTheme.typography.labelLargeEmphasized,
                                 modifier = Modifier.weight(1f)
                             )
-                            SmallIconButton(
-                                imageVector = AppIcons.Close,
+                            SmallPlainButton(
+                                icon = AppIcons.Close,
                                 contentDescription = "关闭",
                                 onClick = {
                                     viewModel.setShowThemeRefactorTip(false)
@@ -759,8 +759,8 @@ fun ThemeConfigScreen(
         onDismissRequest = { showFontSheet = false },
         title = stringResource(R.string.font_setting),
         startAction = {
-            SmallIconButton(
-                imageVector = Icons.Default.Delete,
+            SmallPlainButton(
+                icon = Icons.Default.Delete,
                 contentDescription = stringResource(R.string.clear),
                 onClick = {
                     ThemeConfig.appFontPath = null
@@ -769,8 +769,8 @@ fun ThemeConfigScreen(
             )
         },
         endAction = {
-            SmallIconButton(
-                imageVector = Icons.Default.Add,
+            SmallPlainButton(
+                icon = Icons.Default.Add,
                 contentDescription = stringResource(R.string.select_folder),
                 onClick = { fontFolderLauncher.launch(null) }
             )

+ 3 - 3
app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt

@@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.help.config.ThemeExportData
 import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.button.MediumIconButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
@@ -60,13 +60,13 @@ fun EditThemeSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.theme_manage_edit_theme),
         endAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = {
                     if (name.isNotBlank()) {
                         onSave(name, data)
                     }
                 },
-                imageVector = Icons.Default.Done,
+                icon = Icons.Default.Done,
                 contentDescription = "Save"
             )
         }

+ 7 - 7
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt

@@ -47,7 +47,7 @@ import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.SplicedColumnGroup
 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.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
 import io.legado.app.ui.widget.components.text.AppText
@@ -393,17 +393,17 @@ private fun SavedThemeItem(
                     horizontalArrangement = Arrangement.End,
                     verticalAlignment = Alignment.CenterVertically
                 ) {
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = onEdit,
-                        imageVector = Icons.Default.Edit
+                        icon = Icons.Default.Edit
                     )
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = onExport,
-                        imageVector = Icons.Default.Share
+                        icon = Icons.Default.Share
                     )
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = onDelete,
-                        imageVector = Icons.Default.Delete
+                        icon = Icons.Default.Delete
                     )
                 }
             }

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

@@ -38,7 +38,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 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
@@ -315,9 +315,9 @@ fun DictRuleScreen(
                         onEnabledChange = { enabled -> viewModel.update(item.rule.copy(enabled = enabled)) },
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         trailingAction = {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                imageVector = AppIcons.Delete
+                                icon = AppIcons.Delete
                             )
                         }
                     )

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

@@ -111,7 +111,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
             context: Context,
             exploreName: String? = null,
             sourceUrl: String,
-            exploreUrl: String? = null
+            exploreUrl: String? = null,
         ): Intent = MainIntent.createExploreShowIntent(context, exploreName, sourceUrl, exploreUrl)
     }
 

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/MainIntent.kt

@@ -134,7 +134,7 @@ object MainIntent {
         context: Context,
         exploreName: String? = null,
         sourceUrl: String,
-        exploreUrl: String? = null
+        exploreUrl: String? = null,
     ): Intent {
         return createLauncherIntent(context).apply {
             putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_EXPLORE_SHOW)

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

@@ -19,7 +19,9 @@ import io.legado.app.ui.about.AboutEffect
 import io.legado.app.ui.about.AboutScreen
 import io.legado.app.ui.about.AboutViewModel
 import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen
+import io.legado.app.ui.book.explore.ExploreShowIntent
 import io.legado.app.ui.book.explore.ExploreShowScreen
+import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.import.local.ImportBookScreen
 import io.legado.app.ui.book.import.remote.RemoteBookScreen
 import io.legado.app.ui.book.info.BookInfoRouteScreen
@@ -39,8 +41,8 @@ import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigScreen
 import io.legado.app.ui.config.otherConfig.OtherConfigScreen
 import io.legado.app.ui.config.readConfig.ReadConfigScreen
 import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
-import io.legado.app.ui.config.translation.TranslationConfigScreen
 import io.legado.app.ui.config.themeManage.ThemeManageScreen
+import io.legado.app.ui.config.translation.TranslationConfigScreen
 import io.legado.app.ui.rss.article.MainRouteRssSort
 import io.legado.app.ui.rss.article.RssSortRouteScreen
 import io.legado.app.ui.rss.favorites.RssFavoritesScreen
@@ -436,10 +438,17 @@ fun MainActivity.mainEntryProvider(
     }
 
     entry<MainRouteExploreShow> { route ->
+        val exploreViewModel = koinViewModel<ExploreShowViewModel>()
+
+        LaunchedEffect(route.sourceUrl, route.exploreUrl, exploreViewModel) {
+            exploreViewModel.onIntent(
+                ExploreShowIntent.InitData(route.sourceUrl, route.exploreUrl)
+            )
+        }
+
         ExploreShowScreen(
+            viewModel = exploreViewModel,
             title = route.title ?: "探索",
-            sourceUrl = route.sourceUrl,
-            exploreUrl = route.exploreUrl,
             onBack = { onNavigateBack() },
             onBookClick = { book, sharedCoverKey ->
                 onNavigateToRoute(

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/MainNavigator.kt

@@ -268,7 +268,7 @@ object MainNavigator {
                     MainRouteExploreShow(
                         title = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_NAME),
                         sourceUrl = sourceUrl,
-                        exploreUrl = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_URL)
+                        exploreUrl = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_URL),
                     )
                 } ?: MainRouteHome
 

+ 10 - 6
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -114,7 +114,9 @@ import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SearchBar
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallToggleButton
+import io.legado.app.ui.widget.components.button.series.ToggleStyle
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.divider.PillHeaderDivider
@@ -557,7 +559,7 @@ fun BookshelfScreen(
                             val showExpandButton by BookshelfConfig.shouldShowExpandButtonState
                             if (showExpandButton) {
                                 Box(modifier = Modifier) {
-                                    SmallOutlinedIconToggleButton(
+                                    SmallToggleButton(
                                         checked = showGroupMenu,
                                         onCheckedChange = {
                                             if (it) {
@@ -566,7 +568,8 @@ fun BookshelfScreen(
                                                 viewModel.dismissOverlay()
                                             }
                                         },
-                                        imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
+                                        style = ToggleStyle.Outlined,
+                                        icon = Icons.AutoMirrored.Filled.FormatListBulleted,
                                         contentDescription = stringResource(R.string.group_manage)
                                     )
                                     RoundDropdownMenu(
@@ -837,7 +840,7 @@ fun BookshelfScreen(
                                         if (this != null) Modifier.skipToLookaheadSize() else Modifier
                                     }
                                 ),
-                            beyondViewportPageCount = 1,
+                            beyondViewportPageCount = 0,
                             key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
                         ) { pageIndex ->
                             val group = uiState.groups.getOrNull(pageIndex)
@@ -1058,9 +1061,10 @@ private fun BookshelfTopBar(
                     },
                     trailingIcon = {
                         if (uiState.searchKey.isNotEmpty()) {
-                            TopBarActionButton(
+                            SmallPlainButton(
+                                modifier = Modifier.padding(start = 12.dp),
                                 onClick = onClearSearch,
-                                imageVector = AppIcons.Close,
+                                icon = AppIcons.Close,
                                 contentDescription = stringResource(R.string.clear)
                             )
                         }

+ 47 - 74
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -41,7 +41,6 @@ import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.ImmutableMap
 import kotlinx.collections.immutable.persistentMapOf
 import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toImmutableMap
 import kotlinx.collections.immutable.toImmutableSet
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
@@ -196,34 +195,31 @@ class BookshelfViewModel(
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
 
     @OptIn(ExperimentalCoroutinesApi::class)
-    private val allGroupBooksFlow: StateFlow<Map<Long, List<BookUiItem>>> = combine(
-        groupsFlow, sortConfigFlow
-    ) { groups, sortConfig ->
-        groups to sortConfig
-    }.flatMapLatest { (groups, sortConfig) ->
-        if (groups.isEmpty()) {
-            flowOf(emptyMap())
-        } else {
-            val flows = groups.map { group ->
-                bookRepository.flowBookShelfByGroup(group.groupId).map { books ->
-                    group.groupId to bookshelfRepository.sortBooks(
-                        books,
-                        group,
-                        sortConfig.sort,
-                        sortConfig.sortOrder
-                    ).map { it.toUiItem() }
+    private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
+        combine(groupsFlow, sortConfigFlow) { groups, sortConfig ->
+            groups to sortConfig
+        }.flatMapLatest { (groups, sortConfig) ->
+            if (groups.isEmpty()) {
+                flowOf(persistentMapOf())
+            } else {
+                val flows = groups.map { group ->
+                    bookRepository.flowBookShelfByGroup(group.groupId).map { books ->
+                        group.groupId to bookshelfRepository.sortBooks(
+                            books,
+                            group,
+                            sortConfig.sort,
+                            sortConfig.sortOrder
+                        ).map { it.toUiItem() }.toImmutableList()
+                    }
+                }
+                combine(flows) { results ->
+                    results.fold(persistentMapOf<Long, ImmutableList<BookUiItem>>()) { acc, (id, list) ->
+                        acc.put(id, list)
+                    }
                 }
             }
-            combine(flows) { it.toMap() }
-        }
-    }.distinctUntilChanged()
-        .flowOn(Dispatchers.Default)
-        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
-
-    private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
-        allGroupBooksFlow.map { map ->
-            map.mapValues { it.value.toImmutableList() }.toImmutableMap()
-        }.flowOn(Dispatchers.Default)
+        }.distinctUntilChanged()
+            .flowOn(Dispatchers.Default)
             .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentMapOf())
 
     private val visibleBooksFlow: Flow<List<BookUiItem>> = combine(
@@ -301,30 +297,21 @@ class BookshelfViewModel(
         }
     }.distinctUntilChanged().flowOn(Dispatchers.Default)
 
-    private val coreInternalStateFlow = combine(
+    private val internalStateFlow = combine(
         groupIdFlow,
         searchKeyFlow,
         searchModeFlow,
         loadingTextFlow,
-        updatingBooksFlow
-    ) { groupId, searchKey, isSearchMode, loadingText, updatingBooks ->
+        combine(updatingBooksFlow, upBooksCountFlow, sortConfigFlow) { a, b, c ->
+            Triple(a, b, c)
+        }
+    ) { groupId, searchKey, isSearchMode, loadingText, (updatingBooks, upBooksCount, sortConfig) ->
         InternalState(
             groupId = groupId,
             searchKey = searchKey,
             isSearchMode = isSearchMode,
             loadingText = loadingText,
             updatingBooks = updatingBooks,
-            upBooksCount = 0,
-            sortConfig = readSortConfig()
-        )
-    }
-
-    private val internalStateFlow = combine(
-        coreInternalStateFlow,
-        upBooksCountFlow,
-        sortConfigFlow
-    ) { core, upBooksCount, sortConfig ->
-        core.copy(
             upBooksCount = upBooksCount,
             sortConfig = sortConfig
         )
@@ -351,35 +338,29 @@ class BookshelfViewModel(
         val pendingSavedBooks: List<BookUiItem>?
     )
 
-    private val editStateFlow = combine(
+    private val interactionStateFlow = combine(
         activeOverlayFlow,
         isEditModeFlow,
         selectedVisibleBookUrlsFlow,
-        isInFolderRootFlow
-    ) { activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot ->
-        EditState(activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot)
-    }
-
-    private data class EditState(
-        val activeOverlay: BookshelfOverlay?,
-        val isEditMode: Boolean,
-        val selectedBookUrls: Set<String>,
-        val isInFolderRoot: Boolean
-    )
-
-    private val interactionStateFlow = combine(
-        editStateFlow,
-        isRefreshingFlow,
-        bookGroupStyleFlow,
-        draggingBooksFlow,
-        pendingSavedBooksFlow
-    ) { editState, isRefreshing, bookGroupStyle, draggingBooks, pendingSavedBooks ->
+        isInFolderRootFlow,
+        isRefreshingFlow
+    ) { activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot, isRefreshing ->
         BookshelfInteractionState(
-            activeOverlay = editState.activeOverlay,
-            isEditMode = editState.isEditMode,
-            selectedBookUrls = editState.selectedBookUrls,
-            isInFolderRoot = editState.isInFolderRoot,
+            activeOverlay = activeOverlay,
+            isEditMode = isEditMode,
+            selectedBookUrls = selectedBookUrls,
+            isInFolderRoot = isInFolderRoot,
             isRefreshing = isRefreshing,
+            bookGroupStyle = 0,
+            draggingBooks = null,
+            pendingSavedBooks = null
+        )
+    }.combine(
+        combine(bookGroupStyleFlow, draggingBooksFlow, pendingSavedBooksFlow) { a, b, c ->
+            Triple(a, b, c)
+        }
+    ) { interaction, (bookGroupStyle, draggingBooks, pendingSavedBooks) ->
+        interaction.copy(
             bookGroupStyle = bookGroupStyle,
             draggingBooks = draggingBooks,
             pendingSavedBooks = pendingSavedBooks
@@ -390,11 +371,6 @@ class BookshelfViewModel(
         GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)
     )
 
-    private val allGroupBooksStateFlow =
-        MutableStateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>>(
-        persistentMapOf()
-    )
-
     private val dataStateFlow = combine(
         booksFlow,
         groupsFlow,
@@ -403,7 +379,7 @@ class BookshelfViewModel(
         internalStateFlow
     ) { books, groups, allGroups, previews, internal ->
         BookshelfDataCore(books, groups, allGroups, previews, internal)
-    }.combine(allGroupBooksStateFlow) { core, allGroupBooks ->
+    }.combine(allGroupBooksImmutableFlow) { core, allGroupBooks ->
         BookshelfDataState(
             books = core.books,
             groups = core.groups.map { it.toBookGroupUi() },
@@ -529,9 +505,6 @@ class BookshelfViewModel(
         viewModelScope.launch {
             groupPreviewsFlow.collect { groupPreviewsStateFlow.value = it }
         }
-        viewModelScope.launch {
-            allGroupBooksImmutableFlow.collect { allGroupBooksStateFlow.value = it }
-        }
         viewModelScope.launch {
             combine(booksFlow, selectedGroupCanReorderFlow) { books, canReorderBooks ->
                 books to canReorderBooks

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

@@ -26,12 +26,12 @@ 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.ui.book.group.GroupEditContent
 import io.legado.app.ui.book.group.GroupDeleteAction
+import io.legado.app.ui.book.group.GroupEditContent
 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
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.utils.move
@@ -95,13 +95,13 @@ fun GroupManageSheet(
         },
         endAction = {
             if (!isEditing) {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = {
                         editingGroup = null
                         coverPath = null
                         isEditing = true
                     },
-                    imageVector = Icons.Default.Add
+                    icon = Icons.Default.Add
                 )
             } else {
                 GroupResetCoverAction(

+ 7 - 4
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -92,6 +92,9 @@ fun ExploreScreen(
     val context = LocalContext.current
     val activity = context as? AppCompatActivity
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+    val listItems by remember(uiState.items, uiState.expandedId, uiState.exploreKinds) {
+        derivedStateOf { viewModel.buildExploreListItems(uiState) }
+    }
     var sourceToDeleteUrl by rememberSaveable { mutableStateOf<String?>(null) }
     val sourceToDelete = remember(sourceToDeleteUrl, uiState.items) {
         uiState.items.firstOrNull { it.bookSourceUrl == sourceToDeleteUrl }
@@ -118,10 +121,10 @@ fun ExploreScreen(
         }
     }
 
-    val stickyHeaderSource by remember(uiState.listItems, uiState.items) {
+    val stickyHeaderSource by remember(listItems, uiState.items) {
         derivedStateOf {
             val firstIndex = listState.firstVisibleItemIndex
-            val item = uiState.listItems.getOrNull(firstIndex)
+            val item = listItems.getOrNull(firstIndex)
             if (item is ExploreListItem.KindRow) {
                 uiState.items.find { it.bookSourceUrl == item.sourceUrl }
             } else {
@@ -178,7 +181,7 @@ fun ExploreScreen(
                 )
             ) {
                 items(
-                    items = uiState.listItems,
+                    items = listItems,
                     key = { it.key }
                 ) { listItem ->
                     when (listItem) {
@@ -268,7 +271,7 @@ fun ExploreScreen(
                     verticalPadding = 8.dp,
                     onClick = {
                         scope.launch {
-                            val index = uiState.listItems.indexOfFirst {
+                            val index = listItems.indexOfFirst {
                                 it is ExploreListItem.Header && it.source.bookSourceUrl == item.bookSourceUrl
                             }
                             if (index >= 0) listState.animateScrollToItem(index)

+ 2 - 5
app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt

@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
@@ -42,7 +41,6 @@ class ExploreViewModel(
 
     private val _uiState = MutableStateFlow(ExploreUiState())
     val uiState: StateFlow<ExploreUiState> = _uiState
-        .map { state -> state.copy(listItems = buildExploreListItems(state)) }
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ExploreUiState())
     private val _effects = MutableSharedFlow<ExploreEffect>(extraBufferCapacity = 8)
     val effects = _effects.asSharedFlow()
@@ -200,11 +198,10 @@ class ExploreViewModel(
         val exploreKinds: ImmutableList<ExploreKind> = persistentListOf(),
         val kindDisplayNames: ImmutableMap<String, String> = persistentMapOf(),
         val kindValues: ImmutableMap<String, String> = persistentMapOf(),
-        val loadingKinds: Boolean = false,
-        val listItems: ImmutableList<ExploreListItem> = persistentListOf()
+        val loadingKinds: Boolean = false
     ) : ListUiState<BookSourcePart>
 
-    private fun buildExploreListItems(state: ExploreUiState): ImmutableList<ExploreListItem> {
+    fun buildExploreListItems(state: ExploreUiState): ImmutableList<ExploreListItem> {
         if (state.items.isEmpty()) return persistentListOf()
         val expandedId = state.expandedId
         val kindRows = if (expandedId != null) {

+ 11 - 11
app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt

@@ -37,7 +37,7 @@ import io.legado.app.ui.main.homepage.manage.SetListPage
 import io.legado.app.ui.main.homepage.manage.SourceBrowseDetailPage
 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.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 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
@@ -127,38 +127,38 @@ fun <T> HomepageModuleManageSheet(
         },
         startAction = {
             if (showCustomSetAddModules) {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = { showCustomSetAddModules = false },
-                    imageVector = Icons.AutoMirrored.Filled.ArrowBack
+                    icon = Icons.AutoMirrored.Filled.ArrowBack
                 )
             } else if (browsingSourceUrl != null || showSourceBrowser) {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = {
                         if (browsingDetail) browsingDetail = false
                         else if (showSourceBrowser) showSourceBrowser = false
                         else browsingSourceUrl = null
                     },
-                    imageVector = Icons.AutoMirrored.Filled.ArrowBack
+                    icon = Icons.AutoMirrored.Filled.ArrowBack
                 )
             } else if (selectingSetUrl != null) {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = { selectingSetUrl = null },
-                    imageVector = Icons.AutoMirrored.Filled.ArrowBack
+                    icon = Icons.AutoMirrored.Filled.ArrowBack
                 )
             }
         },
         endAction = {
             if (browsingDetail && browseTab == 2 && browseModuleType == "buttonGroup" && selectedKindTitles.isNotEmpty()) {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = { showAddButtonGroupDialog = true },
-                    imageVector = Icons.Default.Check
+                    icon = Icons.Default.Check
                 )
             } else if ((showSourceBrowser || browsingSourceUrl != null) && !browsingDetail) {
                 var expanded by remember { mutableStateOf(false) }
                 Box {
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = { expanded = true },
-                        imageVector = Icons.Default.FilterList
+                        icon = Icons.Default.FilterList
                     )
                     RoundDropdownMenu(
                         expanded = expanded,

+ 5 - 4
app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt

@@ -72,7 +72,7 @@ import io.legado.app.ui.widget.components.LoadMoreFooter
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.book.SearchBookGridItem
 import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
-import io.legado.app.ui.widget.components.button.SmallTonalIconButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
@@ -82,6 +82,7 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.utils.sendToClip
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import org.koin.androidx.compose.koinViewModel
 
@@ -132,7 +133,7 @@ fun HomepageScreen(
     }
 
     LaunchedEffect(viewModel) {
-        viewModel.effects.collect { effect ->
+        viewModel.effects.collectLatest { effect ->
             when (effect) {
                 is HomepageEffect.NavigateToBookInfo ->
                     onBookClick(
@@ -717,9 +718,9 @@ private fun ModuleHeader(
             modifier = Modifier.weight(1f),
         )
         if (onNavigate != null) {
-            SmallTonalIconButton(
+            SmallTonalButton(
                 onClick = onNavigate,
-                imageVector = Icons.AutoMirrored.Filled.ArrowForward
+                icon = Icons.AutoMirrored.Filled.ArrowForward
             )
         }
     }

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

@@ -338,6 +338,12 @@ class HomepageViewModel(
         }
     }
 
+    override fun onCleared() {
+        super.onCleared()
+        loadJobs.values.forEach { it.cancel() }
+        loadJobs.clear()
+    }
+
     private suspend fun syncModulesFromSource(source: BookSource) {
         val json = source.homepageModules ?: return
         ensureSetForSource(source.bookSourceUrl, source.bookSourceName)

+ 9 - 9
app/src/main/java/io/legado/app/ui/main/homepage/manage/SetDetailPage.kt

@@ -26,7 +26,7 @@ import io.legado.app.ui.main.homepage.HomepageModuleManageUi
 import io.legado.app.ui.main.homepage.HomepageViewModel
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.SecondaryButton
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.divider.PillDivider
@@ -129,13 +129,13 @@ fun SetDetailPage(
                             }
                         },
                         trailingAction = {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { onEditModule(module) },
-                                imageVector = Icons.Default.Edit
+                                icon = Icons.Default.Edit
                             )
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { onRequestDeleteModule(module.id) },
-                                imageVector = Icons.Default.Delete
+                                icon = Icons.Default.Delete
                             )
                         }
                     )
@@ -160,13 +160,13 @@ fun SetDetailPage(
                         containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
                         onEnabledChange = { onToggleModule(module.id, it) },
                         trailingAction = {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { onEditModule(module) },
-                                imageVector = Icons.Default.Edit
+                                icon = Icons.Default.Edit
                             )
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { onRequestDeleteModule(module.id) },
-                                imageVector = Icons.Default.Delete
+                                icon = Icons.Default.Delete
                             )
                         }
                     )

+ 5 - 5
app/src/main/java/io/legado/app/ui/main/homepage/manage/SetListPage.kt

@@ -22,7 +22,7 @@ import io.legado.app.R
 import io.legado.app.ui.main.homepage.HomepageSourceManageUi
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.SecondaryButton
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.divider.PillDivider
 import io.legado.app.utils.move
@@ -85,13 +85,13 @@ fun SetListPage(
                     }
                 },
                 trailingAction = {
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = { onRenameSet(set.sourceUrl) },
-                        imageVector = Icons.Default.DriveFileRenameOutline
+                        icon = Icons.Default.DriveFileRenameOutline
                     )
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = { onDeleteSet(set.sourceUrl) },
-                        imageVector = Icons.Default.Delete
+                        icon = Icons.Default.Delete
                     )
                 }
             )

+ 11 - 11
app/src/main/java/io/legado/app/ui/main/homepage/manage/SourceBrowseDetailPage.kt

@@ -32,7 +32,7 @@ import io.legado.app.ui.main.homepage.HomepageModuleManageUi
 import io.legado.app.ui.main.homepage.HomepageViewModel
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.SecondaryButton
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -170,13 +170,13 @@ fun SourceBrowseDetailPage(
                                     }
                                 },
                                 trailingAction = {
-                                    SmallIconButton(
+                                    SmallPlainButton(
                                         onClick = { onEditModule(module) },
-                                        imageVector = Icons.Default.Edit
+                                        icon = Icons.Default.Edit
                                     )
-                                    SmallIconButton(
+                                    SmallPlainButton(
                                         onClick = { onRequestDeleteModule(module.id) },
-                                        imageVector = Icons.Default.Delete
+                                        icon = Icons.Default.Delete
                                     )
                                 }
                             )
@@ -212,13 +212,13 @@ fun SourceBrowseDetailPage(
                                     containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
                                     onEnabledChange = { onToggleModule(module.id, it) },
                                     trailingAction = {
-                                        SmallIconButton(
+                                        SmallPlainButton(
                                             onClick = { onEditModule(module) },
-                                            imageVector = Icons.Default.Edit
+                                            icon = Icons.Default.Edit
                                         )
-                                        SmallIconButton(
+                                        SmallPlainButton(
                                             onClick = { onRequestDeleteModule(module.id) },
-                                            imageVector = Icons.Default.Delete
+                                            icon = Icons.Default.Delete
                                         )
                                     }
                                 )
@@ -334,9 +334,9 @@ fun SourceBrowseDetailPage(
                         onToggleSelection = { showKindSelect = true },
                         trailingAction = {
                             if (isButtonGroup && selectedKindTitles.isNotEmpty()) {
-                                SmallIconButton(
+                                SmallPlainButton(
                                     onClick = { onShowAddButtonGroupDialog() },
-                                    imageVector = Icons.Default.Check
+                                    icon = Icons.Default.Check
                                 )
                             }
                         }

+ 15 - 16
app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt

@@ -35,8 +35,6 @@ import androidx.compose.material.icons.filled.Source
 import androidx.compose.material.icons.filled.Web
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
@@ -54,11 +52,12 @@ import io.legado.app.ui.replace.ReplaceRuleActivity
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.SplicedColumnGroup
-import io.legado.app.ui.widget.components.button.SmallTextButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import org.koin.androidx.compose.koinViewModel
 
 
@@ -81,7 +80,7 @@ fun MyScreen(
             GlassMediumFlexibleTopAppBar(
                 title = stringResource(R.string.my),
                 actions = {
-                    IconButton(
+                    TopBarActionButton(
                         onClick = {
                             onNavigate(
                                 PrefClickEvent.ShowMd(
@@ -89,10 +88,10 @@ fun MyScreen(
                                     path = "appHelp"
                                 )
                             )
-                        }
-                    ) {Icon(
-                        Icons.AutoMirrored.Filled.HelpOutline, null)
-                    }
+                        },
+                        imageVector = Icons.AutoMirrored.Filled.HelpOutline,
+                        contentDescription = null
+                    )
                 },
                 scrollBehavior = scrollBehavior
             )
@@ -251,22 +250,22 @@ fun WebServiceSettingBlock(
                     .padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 12.dp),
                 horizontalArrangement = Arrangement.End
             ) {
-                SmallTextButton(
-                    text = stringResource(R.string.copy_url),
-                    imageVector = Icons.Default.ContentCopy,
+                SmallPlainButton(
                     onClick = {
                         onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress))
-                    }
+                    },
+                    icon = Icons.Default.ContentCopy,
+                    text = stringResource(R.string.copy_url)
                 )
 
                 Spacer(modifier = Modifier.width(12.dp))
 
-                SmallTextButton(
-                    text = stringResource(R.string.open_in_browser),
-                    imageVector = Icons.Default.OpenInBrowser,
+                SmallPlainButton(
                     onClick = {
                         onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress))
-                    }
+                    },
+                    icon = Icons.Default.OpenInBrowser,
+                    text = stringResource(R.string.open_in_browser)
                 )
             }
         }

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

@@ -5,8 +5,6 @@ import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.slideInVertically
 import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -19,71 +17,54 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.ime
-import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 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.Check
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.AssistChip
 import androidx.compose.material3.BottomAppBar
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExposedDropdownMenuAnchorType
 import androidx.compose.material3.ExposedDropdownMenuBox
 import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.ui.res.stringResource
-import io.legado.app.R
-import io.legado.app.ui.widget.components.AppFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
 import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateMapOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
 import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
+import io.legado.app.R
+import io.legado.app.ui.widget.components.AppFloatingActionButton
 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.MediumIconButton
 import io.legado.app.ui.widget.components.button.ToggleChip
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
-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.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarActionButton
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import org.koin.androidx.compose.koinViewModel
 
 @Composable
@@ -369,9 +350,9 @@ fun GroupSelector(
                 }
             }
         }
-        MediumIconButton(
+        MediumPlainButton(
             onClick = onManageClick,
-            imageVector = Icons.Default.Settings
+            icon = Icons.Default.Settings
         )
     }
 }

+ 13 - 14
app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt

@@ -1,7 +1,6 @@
 package io.legado.app.ui.rss.article
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
@@ -11,13 +10,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
 import androidx.compose.material.icons.automirrored.filled.Login
 import androidx.compose.material.icons.filled.Check
@@ -25,7 +22,6 @@ import androidx.compose.material.icons.filled.CleaningServices
 import androidx.compose.material.icons.filled.Dataset
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.History
-import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Style
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -39,6 +35,7 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
@@ -51,10 +48,9 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.EmptyMessage
-import io.legado.app.ui.widget.components.button.SmallIconButton
-import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallToggleButton
+import io.legado.app.ui.widget.components.button.series.ToggleStyle
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
@@ -65,6 +61,8 @@ import io.legado.app.ui.widget.components.tabRow.AppTabRow
 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
+import io.legado.app.ui.widget.components.topbar.TopBarActionButton
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalMaterial3Api::class)
@@ -242,10 +240,11 @@ fun RssSortScreen(
 
                             if (BookshelfConfig.shouldShowExpandButton) {
                                 Box {
-                                    SmallOutlinedIconToggleButton(
+                                    SmallToggleButton(
                                         checked = showGroupMenu,
                                         onCheckedChange = { showGroupMenu = it },
-                                        imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
+                                        style = ToggleStyle.Outlined,
+                                        icon = Icons.AutoMirrored.Filled.FormatListBulleted,
                                         contentDescription = stringResource(R.string.group_manage)
                                     )
                                     RoundDropdownMenu(
@@ -321,9 +320,9 @@ private fun RssReadRecordSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.read_record),
         endAction = {
-            SmallIconButton(
+            SmallPlainButton(
                 onClick = onClear,
-                imageVector = Icons.Default.CleaningServices,
+                icon = Icons.Default.CleaningServices,
                 contentDescription = stringResource(R.string.clear)
             )
         }

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

@@ -33,7 +33,7 @@ import io.legado.app.data.entities.RssStar
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.EmptyMessage
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.dialog.TextListInputDialog
 import io.legado.app.ui.widget.components.divider.PillDivider
@@ -229,9 +229,9 @@ fun RssFavoritesScreen(
                             val openAction = {
                                 onOpenRead(rssStar.title, rssStar.origin, rssStar.link, null)
                             }
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = openAction,
-                                imageVector = Icons.AutoMirrored.Filled.OpenInNew,
+                                icon = Icons.AutoMirrored.Filled.OpenInNew,
                                 contentDescription = "Open"
                             )
                         },

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

@@ -58,7 +58,7 @@ import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -422,9 +422,9 @@ private fun FavoriteEditSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.favorite),
         endAction = {
-            SmallIconButton(
+            SmallPlainButton(
                 onClick = onDelete,
-                imageVector = Icons.Default.CleaningServices,
+                icon = Icons.Default.CleaningServices,
                 contentDescription = stringResource(R.string.delete)
             )
         }

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

@@ -5,7 +5,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.width
@@ -13,13 +12,10 @@ import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
@@ -45,7 +41,7 @@ import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.GroupManageBottomSheet
 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.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.dialog.TextListInputDialog
 import io.legado.app.ui.widget.components.divider.PillDivider
@@ -364,9 +360,9 @@ fun RssSourceScreen(
                         onEnabledChange = { enabled -> viewModel.update(item.source.copy(enabled = enabled)) },
                         onClickEdit = { onEditSource(item.source) },
                         trailingAction = {
-                            SmallIconButton(
+                            SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.source },
-                                imageVector = Icons.Default.Delete
+                                icon = Icons.Default.Delete
                             )
                         }
                     )

+ 6 - 4
app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt

@@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
@@ -46,6 +45,7 @@ import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
@@ -165,9 +165,11 @@ fun RuleSubScreen(
                             }
                         },
                         trailingAction = {
-                            IconButton(onClick = { showEditDialog = ruleSub }) {
-                                Icon(Icons.Default.Edit, contentDescription = "Edit")
-                            }
+                            SmallPlainButton(
+                                onClick = { showEditDialog = ruleSub },
+                                icon = Icons.Default.Edit,
+                                contentDescription = "Edit"
+                            )
                         },
                         dropdownContent = { dismiss ->
                             RoundDropdownMenuItem(

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

@@ -35,6 +35,14 @@ fun Modifier.adaptiveVerticalPadding(): Modifier {
     return this.padding(horizontal = horizontal)
 }
 
+@Composable
+fun adaptiveHorizonalPadding(): PaddingValues {
+    val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
+    return PaddingValues(
+        horizontal = horizontal
+    )
+}
+
 @Composable
 fun adaptiveContentPaddingOnlyVertical(
     top: Dp,

+ 41 - 0
app/src/main/java/io/legado/app/ui/widget/components/AppSlider.kt

@@ -0,0 +1,41 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.material3.Slider
+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
+import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
+
+@Composable
+fun AppSlider(
+    value: Float,
+    onValueChange: (Float) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    steps: Int = 0,
+    onValueChangeFinished: (() -> Unit)? = null,
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        MiuixSlider(
+            value = value,
+            onValueChange = onValueChange,
+            modifier = modifier,
+            enabled = enabled,
+            valueRange = valueRange,
+            steps = steps,
+            onValueChangeFinished = onValueChangeFinished
+        )
+    } else {
+        Slider(
+            value = value,
+            onValueChange = onValueChange,
+            modifier = modifier,
+            enabled = enabled,
+            valueRange = valueRange,
+            steps = steps,
+            onValueChangeFinished = onValueChangeFinished
+        )
+    }
+}

+ 4 - 4
app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt

@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.layout.wrapContentSize
-import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -24,8 +23,9 @@ import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.icon.AppIcons
-import io.legado.app.ui.widget.components.button.SmallTonalTextButton
+import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
 
 @Composable
@@ -84,10 +84,10 @@ fun EmptyMessage(
 
         if (buttonText != null && onButtonClick != null) {
             Spacer(modifier = Modifier.height(8.dp))
-            SmallTonalTextButton(
+            SmallTonalButton(
                 onClick = onButtonClick,
                 text = buttonText,
-                imageVector = buttonImageVector
+                icon = buttonImageVector
             )
         }
     }

+ 5 - 5
app/src/main/java/io/legado/app/ui/widget/components/GroupManageBottomSheet.kt

@@ -26,7 +26,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
-import io.legado.app.ui.widget.components.button.SmallTextButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.settingItem.SettingItem
 
@@ -118,13 +118,13 @@ private fun GroupItem(
                 modifier = Modifier.fillMaxWidth(),
                 horizontalArrangement = Arrangement.End
             ) {
-                SmallTextButton(
-                    text = stringResource(id = R.string.ok),
-                    imageVector = Icons.Default.Check,
+                SmallPlainButton(
                     onClick = {
                         onUpdateGroup(group, state.text.toString())
                         expanded = false
-                    }
+                    },
+                    icon = Icons.Default.Check,
+                    text = stringResource(id = R.string.ok)
                 )
             }
         }

+ 5 - 5
app/src/main/java/io/legado/app/ui/widget/components/JsonRawEditor.kt

@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp
 import com.google.gson.GsonBuilder
 import com.google.gson.JsonParser
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.GSON
 
@@ -37,17 +37,17 @@ fun JsonRawEditor(
                 style = LegadoTheme.typography.labelMediumEmphasized
             )
             Row {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = {
                         runCatching {
                             val jsonElement = JsonParser.parseString(value)
                             onValueChange(GSON.toJson(jsonElement))
                         }
                     },
-                    imageVector = Icons.Default.AutoFixHigh,
+                    icon = Icons.Default.AutoFixHigh,
                     contentDescription = "格式化"
                 )
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = {
                         runCatching {
                             val jsonElement = JsonParser.parseString(value)
@@ -55,7 +55,7 @@ fun JsonRawEditor(
                             onValueChange(compactGson.toJson(jsonElement))
                         }
                     },
-                    imageVector = Icons.Default.Compress,
+                    icon = Icons.Default.Compress,
                     contentDescription = "压缩"
                 )
             }

+ 5 - 5
app/src/main/java/io/legado/app/ui/widget/components/ValueStepper.kt

@@ -10,7 +10,7 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.SmallOutlinedIconButton
+import io.legado.app.ui.widget.components.button.series.SmallOutlinedButton
 import io.legado.app.ui.widget.components.card.TextCard
 
 @Composable
@@ -26,12 +26,12 @@ fun ValueStepper(
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(8.dp)
     ) {
-        SmallOutlinedIconButton(
+        SmallOutlinedButton(
             onClick = {
                 val newValue = (value.toInt() - 1).toFloat().coerceIn(valueRange)
                 onValueChange(newValue)
             },
-            imageVector = Icons.Default.Remove
+            icon = Icons.Default.Remove
         )
         TextCard(
             cornerRadius = 8.dp,
@@ -41,12 +41,12 @@ fun ValueStepper(
             backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
             contentColor = LegadoTheme.colorScheme.onSurface
         )
-        SmallOutlinedIconButton(
+        SmallOutlinedButton(
             onClick = {
                 val newValue = (value.toInt() + 1).toFloat().coerceIn(valueRange)
                 onValueChange(newValue)
             },
-            imageVector = Icons.Default.Add
+            icon = Icons.Default.Add
         )
     }
 }

+ 0 - 43
app/src/main/java/io/legado/app/ui/widget/components/button/AnimatedTextButton.kt

@@ -1,43 +0,0 @@
-package io.legado.app.ui.widget.components.button
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.animateContentSize
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.material3.TextButton
-import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import io.legado.app.ui.widget.components.text.AppText
-
-@Composable
-fun AnimatedTextButton(
-    isLoading: Boolean,
-    onClick: () -> Unit,
-    text: String,
-    modifier: Modifier = Modifier
-) {
-    TextButton(
-        onClick = onClick,
-        enabled = !isLoading,
-        modifier = modifier.animateContentSize()
-    ) {
-        AnimatedContent(
-            targetState = isLoading,
-            contentAlignment = Alignment.Center,
-            transitionSpec = {
-                fadeIn(tween(200)) togetherWith fadeOut(tween(200))
-            },
-            label = "ButtonLoading"
-        ) { loading ->
-            if (loading) {
-                AppContainedLoadingIndicator()
-            } else {
-                AppText(text)
-            }
-        }
-    }
-}

+ 0 - 217
app/src/main/java/io/legado/app/ui/widget/components/button/MediumIconButton.kt

@@ -1,217 +0,0 @@
-package io.legado.app.ui.widget.components.button
-
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.OutlinedIconButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TonalToggleButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.dp
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.LegadoTheme.composeEngine
-import io.legado.app.ui.theme.ThemeResolver
-import top.yukonga.miuix.kmp.theme.MiuixTheme
-import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
-import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
-import top.yukonga.miuix.kmp.basic.Text as MiuixText
-
-@Composable
-fun MediumIconButton(
-    onClick: () -> Unit,
-    imageVector: ImageVector,
-    tint: Color = LegadoTheme.colorScheme.onSurface,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixIconButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled
-        ) {
-            MiuixIcon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                tint = tint
-            )
-        }
-    } else {
-        IconButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled
-        ) {
-            Icon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                tint = tint
-            )
-        }
-    }
-}
-
-@Composable
-fun MediumOutlinedIconButton(
-    onClick: () -> Unit,
-    imageVector: ImageVector,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixIconButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
-        ) {
-            MiuixIcon(
-                imageVector = imageVector,
-                contentDescription = contentDescription
-            )
-        }
-    } else {
-        OutlinedIconButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            border = ButtonDefaults.outlinedButtonBorder()
-        ) {
-            Icon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                tint = LegadoTheme.colorScheme.onSurface
-            )
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun MediumAnimatedActionButton(
-    checked: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
-    iconChecked: ImageVector,
-    iconUnchecked: ImageVector,
-    activeText: String,
-    inactiveText: String,
-    modifier: Modifier = Modifier
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        val containerColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
-            animationSpec = tween(150),
-            label = "MiuixActionButtonContainer"
-        )
-
-        val contentColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
-            animationSpec = tween(150),
-            label = "MiuixActionButtonContent"
-        )
-
-        AnimatedActionButtonCore(
-            checked = checked,
-            onCheckedChange = onCheckedChange,
-            iconChecked = iconChecked,
-            iconUnchecked = iconUnchecked,
-            activeText = activeText,
-            inactiveText = inactiveText,
-            modifier = modifier,
-            iconSize = 24.dp,
-            textStyle = LegadoTheme.typography.labelMedium,
-            textStartPadding = 8.dp,
-            contentColor = contentColor,
-            button = { buttonModifier, onToggle, content ->
-                MiuixIconButton(
-                    onClick = { onToggle(!checked) },
-                    modifier = buttonModifier,
-                    backgroundColor = containerColor
-                ) {
-                    Row(
-                        verticalAlignment = Alignment.CenterVertically,
-                        horizontalArrangement = Arrangement.Center,
-                        modifier = Modifier.padding(horizontal = 8.dp),
-                        content = content
-                    )
-                }
-            },
-            icon = { imageVector, iconModifier, tint ->
-                MiuixIcon(
-                    imageVector = imageVector,
-                    contentDescription = null,
-                    modifier = iconModifier,
-                    tint = tint ?: Color.Unspecified
-                )
-            },
-            text = { label, textModifier, style, color ->
-                MiuixText(
-                    text = label,
-                    color = color ?: Color.Unspecified,
-                    style = style,
-                    modifier = textModifier,
-                    maxLines = 1,
-                    softWrap = false
-                )
-            }
-        )
-    } else {
-        AnimatedActionButtonCore(
-            checked = checked,
-            onCheckedChange = onCheckedChange,
-            iconChecked = iconChecked,
-            iconUnchecked = iconUnchecked,
-            activeText = activeText,
-            inactiveText = inactiveText,
-            modifier = modifier,
-            iconSize = 24.dp,
-            textStyle = LegadoTheme.typography.labelMedium,
-            textStartPadding = 8.dp,
-            button = { buttonModifier, onToggle, content ->
-                TonalToggleButton(
-                    checked = checked,
-                    onCheckedChange = onToggle,
-                    modifier = buttonModifier,
-                    contentPadding = PaddingValues(horizontal = 8.dp)
-                ) {
-                    Row(
-                        verticalAlignment = Alignment.CenterVertically,
-                        horizontalArrangement = Arrangement.Center,
-                        content = content
-                    )
-                }
-            },
-            icon = { imageVector, iconModifier, _ ->
-                AnimatedIcon(
-                    imageVector = imageVector,
-                    contentDescription = null,
-                    modifier = iconModifier
-                )
-            },
-            text = { label, textModifier, style, color ->
-                Text(
-                    text = label,
-                    modifier = textModifier,
-                    style = style,
-                    color = color ?: Color.Unspecified,
-                    maxLines = 1,
-                    softWrap = false
-                )
-            }
-        )
-    }
-}

+ 0 - 66
app/src/main/java/io/legado/app/ui/widget/components/button/MediumTextButton.kt

@@ -1,66 +0,0 @@
-package io.legado.app.ui.widget.components.button
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Text
-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
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.LegadoTheme.composeEngine
-import io.legado.app.ui.theme.ThemeResolver
-import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
-import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
-import top.yukonga.miuix.kmp.basic.Text as MiuixText
-
-@Composable
-fun MediumOutlinedButton(
-    onClick: () -> Unit,
-    imageVector: ImageVector,
-    text: String,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixIconButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
-        ) {
-            Row(
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.spacedBy(8.dp)
-            ) {
-                MiuixIcon(
-                    imageVector = imageVector,
-                    contentDescription = contentDescription
-                )
-                MiuixText(text = text)
-            }
-        }
-    } else {
-        OutlinedButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            border = ButtonDefaults.outlinedButtonBorder()
-        ) {
-            Icon(
-                imageVector = imageVector,
-                contentDescription = contentDescription
-            )
-            Text(
-                text = text,
-                modifier = Modifier.padding(start = 8.dp)
-            )
-        }
-    }
-}

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

@@ -1,356 +0,0 @@
-package io.legado.app.ui.widget.components.button
-
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.FilledTonalIconButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.IconToggleButtonShapes
-import androidx.compose.material3.LocalMinimumInteractiveComponentSize
-import androidx.compose.material3.OutlinedIconButton
-import androidx.compose.material3.OutlinedIconToggleButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TonalToggleButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.dp
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.LegadoTheme.composeEngine
-import io.legado.app.ui.theme.ThemeResolver
-import top.yukonga.miuix.kmp.theme.MiuixTheme
-import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
-import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
-import top.yukonga.miuix.kmp.basic.Text as MiuixText
-
-private val SmallMiuixButtonSize = 32.dp
-private val SmallMiuixIconSize = 18.dp
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-private fun smallContainerSize() = IconButtonDefaults.extraSmallContainerSize(
-    IconButtonDefaults.IconButtonWidthOption.Uniform
-)
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-private val smallIconSize: androidx.compose.ui.unit.Dp
-    get() = IconButtonDefaults.extraSmallIconSize
-
-@Composable
-private fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
-    CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
-        content()
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun SmallIconButton(
-    onClick: () -> Unit,
-    imageVector: ImageVector,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixIconButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled
-        ) {
-            MiuixIcon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                modifier = Modifier.size(16.dp),
-            )
-        }
-    } else {
-        SmallNoMinTouchTarget {
-            IconButton(
-                onClick = onClick,
-                modifier = modifier.size(smallContainerSize()),
-                enabled = enabled,
-                shape = IconButtonDefaults.extraSmallRoundShape,
-            ) {
-                Icon(
-                    imageVector = imageVector,
-                    contentDescription = contentDescription,
-                    modifier = Modifier.size(smallIconSize),
-                )
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun SmallOutlinedIconButton(
-    onClick: () -> Unit,
-    imageVector: ImageVector,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixIconButton(
-            onClick = onClick,
-            modifier = modifier.size(SmallMiuixButtonSize),
-            enabled = enabled,
-            backgroundColor = LegadoTheme.colorScheme.surfaceContainer
-        ) {
-            MiuixIcon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                modifier = Modifier.size(SmallMiuixIconSize)
-            )
-        }
-    } else {
-        SmallNoMinTouchTarget {
-            OutlinedIconButton(
-                onClick = onClick,
-                modifier = modifier.size(smallContainerSize()),
-                enabled = enabled,
-                shapes = IconButtonDefaults.shapes(),
-                border = ButtonDefaults.outlinedButtonBorder()
-            ) {
-                Icon(
-                    imageVector = imageVector,
-                    contentDescription = contentDescription,
-                    modifier = Modifier.size(smallIconSize)
-                )
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun SmallTonalIconButton(
-    onClick: () -> Unit,
-    imageVector: ImageVector,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        MiuixIconButton(
-            onClick = onClick,
-            modifier = modifier.size(SmallMiuixButtonSize),
-            enabled = enabled,
-            backgroundColor = LegadoTheme.colorScheme.surfaceContainer
-        ) {
-            MiuixIcon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                modifier = Modifier.size(SmallMiuixIconSize)
-            )
-        }
-    } else {
-        SmallNoMinTouchTarget {
-            FilledTonalIconButton(
-                onClick = onClick,
-                modifier = modifier.size(smallContainerSize()),
-                enabled = enabled,
-                shapes = IconButtonDefaults.shapes(),
-                colors = IconButtonDefaults.filledTonalIconButtonColors()
-            ) {
-                Icon(
-                    imageVector = imageVector,
-                    contentDescription = contentDescription,
-                    modifier = Modifier.size(smallIconSize)
-                )
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun SmallOutlinedIconToggleButton(
-    checked: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
-    imageVector: ImageVector,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    contentDescription: String? = null
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        val containerColor by animateColorAsState(
-            targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
-            animationSpec = tween(150),
-            label = "MiuixToggleContainerColor"
-        )
-
-        val iconTint by animateColorAsState(
-            targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
-            animationSpec = tween(150),
-            label = "MiuixToggleIconTint"
-        )
-
-        MiuixIconButton(
-            onClick = { onCheckedChange(!checked) },
-            modifier = modifier.size(SmallMiuixButtonSize),
-            enabled = enabled,
-            backgroundColor = containerColor
-        ) {
-            MiuixIcon(
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                tint = iconTint,
-                modifier = Modifier.size(SmallMiuixIconSize)
-            )
-        }
-    } else {
-        val defaultShape = IconButtonDefaults.extraSmallRoundShape
-        val pressedShape = IconButtonDefaults.extraSmallPressedShape
-        val checkedShape = IconButtonDefaults.extraSmallSelectedRoundShape
-
-        val toggleShapes = remember(defaultShape, checkedShape) {
-            IconToggleButtonShapes(
-                shape = defaultShape,
-                pressedShape = pressedShape,
-                checkedShape = checkedShape
-            )
-        }
-
-        SmallNoMinTouchTarget {
-            OutlinedIconToggleButton(
-                checked = checked,
-                onCheckedChange = onCheckedChange,
-                modifier = modifier.size(smallContainerSize()),
-                enabled = enabled,
-                shapes = toggleShapes
-            ) {
-                Icon(
-                    imageVector = imageVector,
-                    contentDescription = contentDescription,
-                    modifier = Modifier.size(smallIconSize),
-                )
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
-@Composable
-fun SmallAnimatedActionButton(
-    checked: Boolean,
-    onCheckedChange: (Boolean) -> Unit,
-    iconChecked: ImageVector,
-    iconUnchecked: ImageVector,
-    activeText: String,
-    inactiveText: String,
-    modifier: Modifier = Modifier
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        val containerColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainer,
-            animationSpec = tween(150),
-            label = "MiuixActionButtonContainer"
-        )
-
-        AnimatedActionButtonCore(
-            checked = checked,
-            onCheckedChange = onCheckedChange,
-            iconChecked = iconChecked,
-            iconUnchecked = iconUnchecked,
-            activeText = activeText,
-            inactiveText = inactiveText,
-            modifier = modifier,
-            iconSize = 18.dp,
-            textStyle = LegadoTheme.typography.labelSmall,
-            textStartPadding = 6.dp,
-            button = { buttonModifier, onToggle, content ->
-                MiuixIconButton(
-                    onClick = { onToggle(!checked) },
-                    modifier = buttonModifier,
-                    backgroundColor = containerColor
-                ) {
-                    Row(
-                        verticalAlignment = Alignment.CenterVertically,
-                        horizontalArrangement = Arrangement.Center,
-                        modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
-                        content = content
-                    )
-                }
-            },
-            icon = { imageVector, iconModifier, tint ->
-                MiuixIcon(
-                    imageVector = imageVector,
-                    contentDescription = null,
-                    modifier = iconModifier,
-                    tint = tint ?: Color.Unspecified
-                )
-            },
-            text = { label, textModifier, style, color ->
-                MiuixText(
-                    text = label,
-                    style = style,
-                    color = color ?: Color.Unspecified,
-                    modifier = textModifier,
-                    maxLines = 1,
-                    softWrap = false
-                )
-            }
-        )
-    } else {
-        SmallNoMinTouchTarget {
-            AnimatedActionButtonCore(
-                checked = checked,
-                onCheckedChange = onCheckedChange,
-                iconChecked = iconChecked,
-                iconUnchecked = iconUnchecked,
-                activeText = activeText,
-                inactiveText = inactiveText,
-                modifier = modifier.height(36.dp),
-                iconSize = 16.dp,
-                textStyle = LegadoTheme.typography.labelSmall,
-                textStartPadding = 6.dp,
-                button = { buttonModifier, onToggle, content ->
-                    TonalToggleButton(
-                        checked = checked,
-                        onCheckedChange = onToggle,
-                        modifier = buttonModifier,
-                        contentPadding = PaddingValues(horizontal = 8.dp)
-                    ) {
-                        Row(
-                            verticalAlignment = Alignment.CenterVertically,
-                            horizontalArrangement = Arrangement.Center,
-                            content = content
-                        )
-                    }
-                },
-                icon = { imageVector, iconModifier, _ ->
-                    AnimatedIcon(
-                        imageVector = imageVector,
-                        contentDescription = null,
-                        modifier = iconModifier
-                    )
-                },
-                text = { label, textModifier, style, color ->
-                    Text(
-                        text = label,
-                        style = style,
-                        color = color ?: Color.Unspecified,
-                        modifier = textModifier,
-                        maxLines = 1,
-                        softWrap = false
-                    )
-                }
-            )
-        }
-    }
-}

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

@@ -1,142 +0,0 @@
-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.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.FilledTonalButton
-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
-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.Icon as MiuixIcon
-
-@Composable
-fun SmallTextButton(
-    text: String,
-    imageVector: ImageVector,
-    modifier: Modifier = Modifier,
-    onClick: () -> Unit
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        Card(
-            onClick = onClick,
-            modifier = modifier,
-            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(
-            onClick = onClick,
-            modifier = modifier,
-            contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
-            shape = MaterialTheme.shapes.small
-        ) {
-            Icon(
-                imageVector = imageVector,
-                contentDescription = null,
-                modifier = Modifier.size(16.dp)
-            )
-            Spacer(Modifier.width(4.dp))
-            AppText(
-                text = text,
-                style = LegadoTheme.typography.labelMedium
-            )
-        }
-    }
-}
-
-@Composable
-fun SmallTonalTextButton(
-    text: String? = null,
-    imageVector: ImageVector? = null,
-    modifier: Modifier = Modifier,
-    onClick: () -> Unit
-) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        Card(
-            onClick = onClick,
-            modifier = modifier,
-            showIndication = true,
-            colors = CardDefaults.defaultColors(
-                color = LegadoTheme.colorScheme.surfaceContainer,
-                contentColor = LegadoTheme.colorScheme.onSurfaceVariant
-            )
-        ) {
-            Row(
-                modifier = Modifier
-                    .padding(horizontal = 12.dp, vertical = 8.dp),
-                horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
-            ) {
-                if (imageVector != null) {
-                    MiuixIcon(
-                        imageVector = imageVector,
-                        contentDescription = null,
-                        modifier = Modifier.size(16.dp)
-                    )
-                }
-                if (text != null) {
-                    AppText(
-                        text = text,
-                        style = LegadoTheme.typography.labelMedium
-                    )
-                }
-            }
-        }
-    } else {
-        FilledTonalButton(
-            onClick = onClick,
-            modifier = modifier,
-            contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
-        ) {
-            if (imageVector != null) {
-                Icon(
-                    imageVector = imageVector,
-                    contentDescription = null,
-                    modifier = Modifier.size(16.dp)
-                )
-            }
-            Spacer(Modifier.width(4.dp))
-            if (text != null) {
-                AppText(
-                    text = text,
-                    style = LegadoTheme.typography.labelMedium
-                )
-            }
-        }
-    }
-}

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/button/AnimatedActionButtonCore.kt → app/src/main/java/io/legado/app/ui/widget/components/button/series/AnimatedActionButtonCore.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.components.button
+package io.legado.app.ui.widget.components.button.series
 
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.layout.RowScope

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/button/AnimatedIcon.kt → app/src/main/java/io/legado/app/ui/widget/components/button/series/AnimatedIcon.kt

@@ -1,4 +1,4 @@
-package io.legado.app.ui.widget.components.button
+package io.legado.app.ui.widget.components.button.series
 
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.fadeIn

+ 173 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumAnimatedButton.kt

@@ -0,0 +1,173 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TonalToggleButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.theme.MiuixTheme
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun MediumAnimatedButton(
+    checked: Boolean,
+    onCheckedChange: (Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    iconChecked: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        val containerColor by animateColorAsState(
+            targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainer,
+            animationSpec = tween(150),
+            label = "MiuixAnimatedContainerColor"
+        )
+
+        val contentColor by animateColorAsState(
+            targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
+            animationSpec = tween(150),
+            label = "MiuixAnimatedContentColor"
+        )
+
+        if (text != null) {
+            AnimatedActionButtonCore(
+                checked = checked,
+                onCheckedChange = onCheckedChange,
+                iconChecked = iconChecked ?: icon!!,
+                iconUnchecked = icon ?: iconChecked!!,
+                activeText = text,
+                inactiveText = text,
+                modifier = modifier,
+                iconSize = 24.dp,
+                textStyle = LegadoTheme.typography.labelMedium,
+                textStartPadding = 8.dp,
+                contentColor = contentColor,
+                button = { buttonModifier, onToggle, content ->
+                    MiuixIconButton(
+                        onClick = { onToggle(!checked) },
+                        modifier = buttonModifier,
+                        backgroundColor = containerColor
+                    ) {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically,
+                            horizontalArrangement = Arrangement.Center,
+                            modifier = Modifier.padding(horizontal = 8.dp),
+                            content = content
+                        )
+                    }
+                },
+                icon = { imageVector, iconModifier, tint ->
+                    MiuixIcon(
+                        tint = tint ?: Color.Unspecified,
+                        imageVector = imageVector,
+                        contentDescription = null,
+                        modifier = iconModifier
+                    )
+                },
+                text = { label, textModifier, textStyle, color ->
+                    MiuixText(
+                        text = label,
+                        color = color ?: Color.Unspecified,
+                        style = textStyle,
+                        modifier = textModifier,
+                        maxLines = 1,
+                        softWrap = false
+                    )
+                }
+            )
+        } else {
+            MiuixIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier,
+                enabled = enabled,
+                backgroundColor = containerColor
+            ) {
+                MiuixIcon(
+                    imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                    contentDescription = contentDescription,
+                    tint = contentColor
+                )
+            }
+        }
+    } else {
+        if (text != null) {
+            AnimatedActionButtonCore(
+                checked = checked,
+                onCheckedChange = onCheckedChange,
+                iconChecked = iconChecked ?: icon!!,
+                iconUnchecked = icon ?: iconChecked!!,
+                activeText = text,
+                inactiveText = text,
+                modifier = modifier.height(36.dp),
+                iconSize = 20.dp,
+                textStyle = LegadoTheme.typography.labelMedium,
+                textStartPadding = 8.dp,
+                button = { buttonModifier, onToggle, content ->
+                    TonalToggleButton(
+                        modifier = buttonModifier,
+                        contentPadding = PaddingValues(horizontal = 8.dp),
+                        checked = checked,
+                        onCheckedChange = onToggle
+                    ) {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically,
+                            horizontalArrangement = Arrangement.Center,
+                            content = content
+                        )
+                    }
+                },
+                icon = { imageVector, iconModifier, _ ->
+                    AnimatedIcon(
+                        imageVector = imageVector,
+                        contentDescription = null,
+                        modifier = iconModifier
+                    )
+                },
+                text = { label, textModifier, textStyle, color ->
+                    Text(
+                        text = label,
+                        style = textStyle,
+                        color = color ?: Color.Unspecified,
+                        modifier = textModifier,
+                        maxLines = 1,
+                        softWrap = false
+                    )
+                }
+            )
+        } else {
+            FilledTonalIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier,
+                enabled = enabled,
+            ) {
+                Icon(
+                    imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                    contentDescription = contentDescription,
+                )
+            }
+        }
+    }
+}

+ 78 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumOutlinedButton.kt

@@ -0,0 +1,78 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedIconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+
+@Composable
+fun MediumOutlinedButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (icon != null && text == null) {
+            MiuixIconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled,
+                backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
+            ) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceContainerHigh,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                MediumButtonContent(icon, text, contentDescription)
+            }
+        }
+    } else {
+        if (icon != null && text == null) {
+            OutlinedIconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled,
+                border = ButtonDefaults.outlinedButtonBorder()
+            ) {
+                Icon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    tint = LegadoTheme.colorScheme.onSurface
+                )
+            }
+        } else {
+            OutlinedButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled,
+                border = ButtonDefaults.outlinedButtonBorder()
+            ) {
+                MediumButtonContent(icon, text, contentDescription)
+            }
+        }
+    }
+}

+ 121 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumPlainButton.kt

@@ -0,0 +1,121 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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
+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.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
+
+@Composable
+internal fun MediumButtonContent(
+    icon: ImageVector?,
+    text: String?,
+    contentDescription: String?
+) {
+    val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
+    Row(
+        modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        if (icon != null) {
+            if (isMiuix) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription
+                )
+            } else {
+                Icon(
+                    imageVector = icon,
+                    contentDescription = contentDescription
+                )
+            }
+        }
+        if (text != null) {
+            if (isMiuix) {
+                MiuixText(text = text)
+            } else {
+                AppText(text = text)
+            }
+        }
+    }
+}
+
+@Composable
+fun MediumPlainButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    text: String? = null,
+    tint: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.onSurface,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (icon != null && text == null) {
+            MiuixIconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled
+            ) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    tint = tint
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceVariant,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                MediumButtonContent(icon, text, contentDescription)
+            }
+        }
+    } else {
+        if (icon != null && text == null) {
+            IconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled
+            ) {
+                Icon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    tint = tint
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceVariant,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                MediumButtonContent(icon, text, contentDescription)
+            }
+        }
+    }
+}

+ 152 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumToggleButton.kt

@@ -0,0 +1,152 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.ToggleButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.theme.MiuixTheme
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun MediumToggleButton(
+    checked: Boolean,
+    onCheckedChange: (Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    style: ToggleStyle = ToggleStyle.Outlined,
+    icon: ImageVector? = null,
+    iconChecked: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        val containerColor by animateColorAsState(
+            targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
+            animationSpec = tween(150),
+            label = "MiuixToggleContainerColor"
+        )
+
+        val contentColor by animateColorAsState(
+            targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
+            animationSpec = tween(150),
+            label = "MiuixToggleContentColor"
+        )
+
+        if (text != null) {
+            MiuixIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier,
+                enabled = enabled,
+                backgroundColor = containerColor
+            ) {
+                Row(
+                    verticalAlignment = Alignment.CenterVertically,
+                    horizontalArrangement = Arrangement.Center,
+                    modifier = Modifier.padding(horizontal = 8.dp)
+                ) {
+                    MiuixIcon(
+                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                        contentDescription = null,
+                        tint = contentColor
+                    )
+                    MiuixText(
+                        text = text,
+                        color = contentColor,
+                        style = LegadoTheme.typography.labelMedium,
+                        modifier = Modifier.padding(start = 8.dp),
+                        maxLines = 1,
+                        softWrap = false
+                    )
+                }
+            }
+        } else {
+            MiuixIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier,
+                enabled = enabled,
+                backgroundColor = containerColor
+            ) {
+                MiuixIcon(
+                    imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                    contentDescription = contentDescription,
+                    tint = contentColor
+                )
+            }
+        }
+    } else {
+        if (text != null) {
+            ToggleButton(
+                checked = checked,
+                onCheckedChange = onCheckedChange,
+                modifier = modifier,
+                enabled = enabled,
+                contentPadding = PaddingValues(horizontal = 8.dp)
+            ) {
+                Row(
+                    verticalAlignment = Alignment.CenterVertically,
+                    horizontalArrangement = Arrangement.Center
+                ) {
+                    Icon(
+                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                        contentDescription = null
+                    )
+                    Text(
+                        text = text,
+                        style = LegadoTheme.typography.labelMedium,
+                        modifier = Modifier.padding(start = 8.dp),
+                        maxLines = 1,
+                        softWrap = false
+                    )
+                }
+            }
+        } else {
+            when (style) {
+                ToggleStyle.Outlined -> {
+                    FilledTonalButton(
+                        onClick = { onCheckedChange(!checked) },
+                        modifier = modifier,
+                        enabled = enabled,
+                    ) {
+                        Icon(
+                            imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                            contentDescription = contentDescription,
+                        )
+                    }
+                }
+
+                ToggleStyle.Tonal -> {
+                    FilledTonalIconButton(
+                        onClick = { onCheckedChange(!checked) },
+                        modifier = modifier,
+                        enabled = enabled,
+                    ) {
+                        Icon(
+                            imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                            contentDescription = contentDescription,
+                        )
+                    }
+                }
+            }
+        }
+    }
+}

+ 74 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumTonalButton.kt

@@ -0,0 +1,74 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+
+@Composable
+fun MediumTonalButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (icon != null && text == null) {
+            MiuixIconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled,
+                backgroundColor = LegadoTheme.colorScheme.surfaceContainer
+            ) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceContainer,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                MediumButtonContent(icon, text, contentDescription)
+            }
+        }
+    } else {
+        if (icon != null && text == null) {
+            FilledTonalIconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled
+            ) {
+                Icon(
+                    imageVector = icon,
+                    contentDescription = contentDescription
+                )
+            }
+        } else {
+            FilledTonalButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled
+            ) {
+                MediumButtonContent(icon, text, contentDescription)
+            }
+        }
+    }
+}

+ 176 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallAnimatedButton.kt

@@ -0,0 +1,176 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.TonalToggleButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun SmallAnimatedButton(
+    checked: Boolean,
+    onCheckedChange: (Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    iconChecked: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        val containerColor by animateColorAsState(
+            targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
+            animationSpec = tween(150),
+            label = "MiuixAnimatedContainerColor"
+        )
+
+        val iconTint by animateColorAsState(
+            targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
+            animationSpec = tween(150),
+            label = "MiuixAnimatedIconTint"
+        )
+
+        if (text != null) {
+            AnimatedActionButtonCore(
+                checked = checked,
+                onCheckedChange = onCheckedChange,
+                iconChecked = iconChecked ?: icon!!,
+                iconUnchecked = icon ?: iconChecked!!,
+                activeText = text,
+                inactiveText = text,
+                modifier = modifier,
+                iconSize = 18.dp,
+                textStyle = LegadoTheme.typography.labelSmall,
+                textStartPadding = 6.dp,
+                contentColor = iconTint,
+                button = { buttonModifier, onToggle, content ->
+                    MiuixIconButton(
+                        onClick = { onToggle(!checked) },
+                        modifier = buttonModifier,
+                        backgroundColor = containerColor
+                    ) {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically,
+                            horizontalArrangement = Arrangement.Center,
+                            modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
+                            content = content
+                        )
+                    }
+                },
+                icon = { imageVector, iconModifier, tint ->
+                    MiuixIcon(
+                        imageVector = imageVector,
+                        contentDescription = null,
+                        modifier = iconModifier,
+                        tint = tint ?: Color.Unspecified
+                    )
+                },
+                text = { label, textModifier, textStyle, color ->
+                    MiuixText(
+                        text = label,
+                        style = textStyle,
+                        color = color ?: Color.Unspecified,
+                        modifier = textModifier,
+                        maxLines = 1,
+                        softWrap = false
+                    )
+                }
+            )
+        } else {
+            MiuixIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier.size(SmallMiuixButtonSize),
+                enabled = enabled,
+                backgroundColor = containerColor
+            ) {
+                MiuixIcon(
+                    imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                    contentDescription = contentDescription,
+                    tint = iconTint,
+                    modifier = Modifier.size(SmallMiuixIconSize)
+                )
+            }
+        }
+    } else {
+        SmallNoMinTouchTarget {
+            if (text != null) {
+                AnimatedActionButtonCore(
+                    checked = checked,
+                    onCheckedChange = onCheckedChange,
+                    iconChecked = iconChecked ?: icon!!,
+                    iconUnchecked = icon ?: iconChecked!!,
+                    activeText = text,
+                    inactiveText = text,
+                    modifier = modifier.height(36.dp),
+                    iconSize = 16.dp,
+                    textStyle = LegadoTheme.typography.labelSmall,
+                    textStartPadding = 6.dp,
+                    button = { buttonModifier, onToggle, content ->
+                        TonalToggleButton(
+                            checked = checked,
+                            onCheckedChange = onToggle,
+                            modifier = buttonModifier,
+                            contentPadding = PaddingValues(horizontal = 8.dp)
+                        ) {
+                            Row(
+                                verticalAlignment = Alignment.CenterVertically,
+                                horizontalArrangement = Arrangement.Center,
+                                content = content
+                            )
+                        }
+                    },
+                    icon = { imageVector, iconModifier, _ ->
+                        AnimatedIcon(
+                            imageVector = imageVector,
+                            contentDescription = null,
+                            modifier = iconModifier
+                        )
+                    },
+                    text = { label, textModifier, textStyle, color ->
+                        androidx.compose.material3.Text(
+                            text = label,
+                            style = textStyle,
+                            color = color ?: Color.Unspecified,
+                            modifier = textModifier,
+                            maxLines = 1,
+                            softWrap = false
+                        )
+                    }
+                )
+            } else {
+                FilledTonalIconButton(
+                    onClick = { onCheckedChange(!checked) },
+                    modifier = modifier.size(smallContainerSize()),
+                    enabled = enabled,
+                ) {
+                    Icon(
+                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                        contentDescription = contentDescription,
+                        modifier = Modifier.size(smallIconSize),
+                    )
+                }
+            }
+        }
+    }
+}

+ 93 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallOutlinedButton.kt

@@ -0,0 +1,93 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedIconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun SmallOutlinedButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (icon != null && text == null) {
+            MiuixIconButton(
+                onClick = onClick,
+                modifier = modifier.size(SmallMiuixButtonSize),
+                enabled = enabled,
+                backgroundColor = LegadoTheme.colorScheme.surfaceContainer
+            ) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    modifier = Modifier.size(SmallMiuixIconSize)
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceContainer,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                SmallButtonContent(icon, text, contentDescription)
+            }
+        }
+    } else {
+        SmallNoMinTouchTarget {
+            when {
+                icon != null && text == null -> {
+                    OutlinedIconButton(
+                        onClick = onClick,
+                        modifier = modifier.size(smallContainerSize()),
+                        enabled = enabled,
+                        shapes = IconButtonDefaults.shapes(),
+                        border = ButtonDefaults.outlinedButtonBorder()
+                    ) {
+                        Icon(
+                            imageVector = icon,
+                            contentDescription = contentDescription,
+                            modifier = Modifier.size(smallIconSize)
+                        )
+                    }
+                }
+
+                else -> {
+                    OutlinedButton(
+                        onClick = onClick,
+                        modifier = modifier,
+                        enabled = enabled,
+                        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
+                        border = ButtonDefaults.outlinedButtonBorder()
+                    ) {
+                        SmallButtonContent(icon, text, contentDescription)
+                    }
+                }
+            }
+        }
+    }
+}

+ 161 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallPlainButton.kt

@@ -0,0 +1,161 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.LocalMinimumInteractiveComponentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+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.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
+
+internal val SmallMiuixButtonSize = 32.dp
+internal val SmallMiuixIconSize = 18.dp
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal fun smallContainerSize() = IconButtonDefaults.extraSmallContainerSize(
+    IconButtonDefaults.IconButtonWidthOption.Uniform
+)
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal val smallIconSize: Dp
+    get() = IconButtonDefaults.extraSmallIconSize
+
+@Composable
+internal fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
+    CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
+        content()
+    }
+}
+
+@Composable
+internal fun SmallButtonContent(
+    icon: ImageVector?,
+    text: String?,
+    contentDescription: String?
+) {
+    val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
+    Row(
+        modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
+        horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        if (icon != null) {
+            if (isMiuix) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    modifier = Modifier.size(16.dp)
+                )
+            } else {
+                Icon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    modifier = Modifier.size(16.dp)
+                )
+            }
+        }
+        if (text != null) {
+            if (isMiuix) {
+                MiuixText(
+                    text = text,
+                    style = LegadoTheme.typography.labelMedium
+                )
+            } else {
+                AppText(
+                    text = text,
+                    style = LegadoTheme.typography.labelMedium
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun SmallPlainButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (icon != null && text == null) {
+            MiuixIconButton(
+                onClick = onClick,
+                modifier = modifier,
+                enabled = enabled
+            ) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    modifier = Modifier.size(SmallMiuixIconSize),
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceVariant,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                SmallButtonContent(icon, text, contentDescription)
+            }
+        }
+    } else {
+        SmallNoMinTouchTarget {
+            when {
+                icon != null && text == null -> {
+                    IconButton(
+                        onClick = onClick,
+                        modifier = modifier.size(smallContainerSize()),
+                        enabled = enabled,
+                        shape = IconButtonDefaults.extraSmallRoundShape,
+                    ) {
+                        Icon(
+                            imageVector = icon,
+                            contentDescription = contentDescription,
+                            modifier = Modifier.size(smallIconSize),
+                        )
+                    }
+                }
+
+                else -> {
+                    TextButton(
+                        onClick = onClick,
+                        modifier = modifier,
+                        enabled = enabled,
+                        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
+                        shape = MaterialTheme.shapes.small
+                    ) {
+                        SmallButtonContent(icon, text, contentDescription)
+                    }
+                }
+            }
+        }
+    }
+}

+ 178 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallToggleButton.kt

@@ -0,0 +1,178 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.IconToggleButtonShapes
+import androidx.compose.material3.OutlinedIconToggleButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TonalToggleButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
+
+enum class ToggleStyle { Outlined, Tonal }
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun SmallToggleButton(
+    checked: Boolean,
+    onCheckedChange: (Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    style: ToggleStyle = ToggleStyle.Outlined,
+    icon: ImageVector? = null,
+    iconChecked: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        val containerColor by animateColorAsState(
+            targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
+            animationSpec = tween(150),
+            label = "MiuixToggleContainerColor"
+        )
+
+        val iconTint by animateColorAsState(
+            targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
+            animationSpec = tween(150),
+            label = "MiuixToggleIconTint"
+        )
+
+        if (text != null) {
+            MiuixIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier,
+                enabled = enabled,
+                backgroundColor = containerColor
+            ) {
+                Row(
+                    verticalAlignment = Alignment.CenterVertically,
+                    horizontalArrangement = Arrangement.Center,
+                    modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
+                ) {
+                    MiuixIcon(
+                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                        contentDescription = null,
+                        modifier = Modifier.size(16.dp),
+                        tint = iconTint
+                    )
+                    MiuixText(
+                        text = text,
+                        style = LegadoTheme.typography.labelSmall,
+                        color = iconTint,
+                        modifier = Modifier.padding(start = 6.dp),
+                        maxLines = 1,
+                        softWrap = false
+                    )
+                }
+            }
+        } else {
+            MiuixIconButton(
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier.size(SmallMiuixButtonSize),
+                enabled = enabled,
+                backgroundColor = containerColor
+            ) {
+                MiuixIcon(
+                    imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                    contentDescription = contentDescription,
+                    tint = iconTint,
+                    modifier = Modifier.size(SmallMiuixIconSize)
+                )
+            }
+        }
+    } else {
+        SmallNoMinTouchTarget {
+            if (text != null) {
+                TonalToggleButton(
+                    checked = checked,
+                    onCheckedChange = onCheckedChange,
+                    modifier = modifier,
+                    enabled = enabled,
+                    contentPadding = PaddingValues(horizontal = 8.dp)
+                ) {
+                    Row(
+                        verticalAlignment = Alignment.CenterVertically,
+                        horizontalArrangement = Arrangement.Center
+                    ) {
+                        Icon(
+                            imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                            contentDescription = null,
+                            modifier = Modifier.size(16.dp)
+                        )
+                        Text(
+                            text = text,
+                            style = LegadoTheme.typography.labelSmall,
+                            modifier = Modifier.padding(start = 6.dp),
+                            maxLines = 1,
+                            softWrap = false
+                        )
+                    }
+                }
+            } else {
+                when (style) {
+                    ToggleStyle.Outlined -> {
+                        val defaultShape = IconButtonDefaults.extraSmallRoundShape
+                        val pressedShape = IconButtonDefaults.extraSmallPressedShape
+                        val checkedShape = IconButtonDefaults.extraSmallSelectedRoundShape
+
+                        val toggleShapes = remember(defaultShape, checkedShape) {
+                            IconToggleButtonShapes(
+                                shape = defaultShape,
+                                pressedShape = pressedShape,
+                                checkedShape = checkedShape
+                            )
+                        }
+
+                        OutlinedIconToggleButton(
+                            checked = checked,
+                            onCheckedChange = onCheckedChange,
+                            modifier = modifier.size(smallContainerSize()),
+                            enabled = enabled,
+                            shapes = toggleShapes
+                        ) {
+                            Icon(
+                                imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                                contentDescription = contentDescription,
+                                modifier = Modifier.size(smallIconSize),
+                            )
+                        }
+                    }
+
+                    ToggleStyle.Tonal -> {
+                        FilledTonalButton(
+                            onClick = { onCheckedChange(!checked) },
+                            modifier = modifier.size(smallContainerSize()),
+                            enabled = enabled,
+                        ) {
+                            Icon(
+                                imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                                contentDescription = contentDescription,
+                                modifier = Modifier.size(smallIconSize),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 91 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallTonalButton.kt

@@ -0,0 +1,91 @@
+package io.legado.app.ui.widget.components.button.series
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.FilledTonalIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun SmallTonalButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    icon: ImageVector? = null,
+    text: String? = null,
+    contentDescription: String? = null
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (icon != null && text == null) {
+            MiuixIconButton(
+                onClick = onClick,
+                modifier = modifier.size(SmallMiuixButtonSize),
+                enabled = enabled,
+                backgroundColor = LegadoTheme.colorScheme.surfaceContainer
+            ) {
+                MiuixIcon(
+                    imageVector = icon,
+                    contentDescription = contentDescription,
+                    modifier = Modifier.size(SmallMiuixIconSize)
+                )
+            }
+        } else {
+            Card(
+                onClick = onClick,
+                modifier = modifier,
+                showIndication = true,
+                colors = CardDefaults.defaultColors(
+                    color = LegadoTheme.colorScheme.surfaceContainer,
+                    contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+                )
+            ) {
+                SmallButtonContent(icon, text, contentDescription)
+            }
+        }
+    } else {
+        SmallNoMinTouchTarget {
+            when {
+                icon != null && text == null -> {
+                    FilledTonalIconButton(
+                        onClick = onClick,
+                        modifier = modifier.size(smallContainerSize()),
+                        enabled = enabled,
+                        shapes = IconButtonDefaults.shapes(),
+                        colors = IconButtonDefaults.filledTonalIconButtonColors()
+                    ) {
+                        Icon(
+                            imageVector = icon,
+                            contentDescription = contentDescription,
+                            modifier = Modifier.size(smallIconSize)
+                        )
+                    }
+                }
+
+                else -> {
+                    FilledTonalButton(
+                        onClick = onClick,
+                        modifier = modifier,
+                        enabled = enabled,
+                        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)
+                    ) {
+                        SmallButtonContent(icon, text, contentDescription)
+                    }
+                }
+            }
+        }
+    }
+}

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

@@ -16,13 +16,8 @@ import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Edit
-import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material3.Checkbox
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.ListItemDefaults
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -42,7 +37,7 @@ 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.AdaptiveSwitch
-import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 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
@@ -50,7 +45,6 @@ import io.legado.app.ui.widget.components.text.AppText
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.ReorderableLazyListState
 import top.yukonga.miuix.kmp.basic.BasicComponent
-import top.yukonga.miuix.kmp.theme.MiuixTheme
 
 @Composable
 fun SelectionItemCard(
@@ -218,9 +212,9 @@ fun SelectionItemCardContent(
             }
 
             if (onClickEdit != null) {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = onClickEdit,
-                    imageVector = AppIcons.Edit,
+                    icon = AppIcons.Edit,
                     contentDescription = "Edit"
                 )
             }
@@ -231,9 +225,9 @@ fun SelectionItemCardContent(
 
             if (dropdownContent != null) {
                 Box {
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = { showMenu = true },
-                        imageVector = AppIcons.MoreVert,
+                        icon = AppIcons.MoreVert,
                         contentDescription = "More"
                     )
                     RoundDropdownMenu(

+ 7 - 8
app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapCalendarComponents.kt

@@ -36,8 +36,8 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.MediumAnimatedActionButton
-import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
+import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
+import io.legado.app.ui.widget.components.button.series.MediumToggleButton
 import io.legado.app.ui.widget.components.text.AppText
 import java.time.LocalDate
 
@@ -54,15 +54,14 @@ fun HeatmapCalendarStartAction(
     currentMode: HeatmapMode,
     onModeChanged: (HeatmapMode) -> Unit,
 ) {
-    MediumAnimatedActionButton(
+    MediumToggleButton(
         checked = currentMode == HeatmapMode.TIME,
         onCheckedChange = {
             onModeChanged(if (it) HeatmapMode.TIME else HeatmapMode.COUNT)
         },
+        icon = Icons.Default.FormatListNumbered,
         iconChecked = Icons.Default.AccessTime,
-        iconUnchecked = Icons.Default.FormatListNumbered,
-        activeText = "按时长",
-        inactiveText = "按次数"
+        text = "按时长"
     )
 }
 
@@ -73,9 +72,9 @@ fun HeatmapCalendarStartAction(
 fun HeatmapCalendarEndAction(
     onClearDate: () -> Unit
 ) {
-    MediumOutlinedIconButton(
+    MediumOutlinedButton(
         onClick = onClearDate,
-        imageVector = Icons.Outlined.Delete,
+        icon = Icons.Outlined.Delete,
     )
 }
 

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

@@ -44,7 +44,7 @@ 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.SmallIconButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -183,9 +183,9 @@ fun <T> BatchImportDialog(
         title = sheetTitle,
         startAction = if (isEditing) {
             {
-                SmallIconButton(
+                SmallPlainButton(
                     onClick = { editingIndex = null },
-                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                    icon = Icons.AutoMirrored.Filled.ArrowBack,
                     contentDescription = "返回"
                 )
             }
@@ -196,9 +196,9 @@ fun <T> BatchImportDialog(
             {
                 Row {
                     topBarActions()
-                    SmallIconButton(
+                    SmallPlainButton(
                         onClick = { onToggleAll(!allSelected) },
-                        imageVector = Icons.Default.SelectAll,
+                        icon = Icons.Default.SelectAll,
                         contentDescription = if (allSelected) "全不选" else "全选"
                     )
                 }
@@ -371,9 +371,9 @@ fun ImportItemRow(
                 modifier = Modifier.padding(end = 4.dp)
             )
 
-            SmallIconButton(
+            SmallPlainButton(
                 onClick = onInfoClick,
-                imageVector = Icons.Default.Info,
+                icon = Icons.Default.Info,
                 contentDescription = "详情"
             )
         }

+ 3 - 3
app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt

@@ -23,7 +23,7 @@ import io.legado.app.R
 import io.legado.app.constant.AppLog
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.EmptyMessage
-import io.legado.app.ui.widget.components.button.MediumIconButton
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.LogUtils
@@ -43,12 +43,12 @@ fun AppLogSheet(
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.log),
         endAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = {
                     AppLog.clear()
                     logs = emptyList()
                 },
-                imageVector = Icons.Default.DeleteSweep
+                icon = Icons.Default.DeleteSweep
             )
         }
     ) {

+ 1 - 3
app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/OptionSheet.kt

@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -65,7 +63,7 @@ fun RowScope.OptionCard(
             .weight(1f)
             .height(100.dp),
         cornerRadius = 12.dp,
-        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+        containerColor = LegadoTheme.colorScheme.onSheetContent,
         elevation = 2.dp
     ) {
         Column(

+ 5 - 15
app/src/main/java/io/legado/app/ui/widget/components/rules/RuleEditSheet.kt

@@ -13,15 +13,8 @@ import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.ContentPaste
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Save
-import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -30,19 +23,16 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.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.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.button.MediumIconButton
-import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 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.text.AppText
 import kotlinx.coroutines.launch
 
 /**
@@ -84,17 +74,17 @@ fun <T> RuleEditSheet(
     AppModalBottomSheet(
         title = title,
         startAction = {
-            MediumIconButton(
+            MediumPlainButton(
                 onClick = onDismissRequest,
-                imageVector = Icons.Default.Close,
+                icon = Icons.Default.Close,
                 contentDescription = "Close",
             )
         },
         endAction = {
             Box{
-                MediumIconButton(
+                MediumPlainButton(
                     onClick = { showMenu = true },
-                    imageVector = Icons.Default.MoreVert,
+                    icon = Icons.Default.MoreVert,
                     contentDescription = "More"
                 )
                 RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {

+ 25 - 4
app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassMediumFlexibleTopAppBar.kt

@@ -1,9 +1,13 @@
 package io.legado.app.ui.widget.components.topbar
 
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.MaterialTheme
@@ -16,6 +20,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LocalHazeState
@@ -81,7 +86,9 @@ fun GlassMediumFlexibleTopAppBar(
     }
 
     val finalModifier = if (hazeState != null) {
-        modifier.background(color = animatedColor).responsiveHazeEffect(state = hazeState)
+        modifier
+            .background(color = animatedColor)
+            .responsiveHazeEffect(state = hazeState)
     } else {
         modifier.background(color = animatedColor)
     }
@@ -116,7 +123,7 @@ fun GlassMediumFlexibleTopAppBar(
                             AdaptiveAnimatedText(
                                 text = title,
                                 useCharMode = useCharMode,
-                                maxLines = 1,
+                                maxLines = 2,
                                 overflow = TextOverflow.Ellipsis
                             )
                         },
@@ -126,7 +133,13 @@ fun GlassMediumFlexibleTopAppBar(
                             }
                         },
                         navigationIcon = navigationIcon,
-                        actions = actions,
+                        actions = {
+                            Box(modifier = Modifier.padding(end = 12.dp)) {
+                                Row(
+                                    horizontalArrangement = Arrangement.spacedBy(8.dp)
+                                ) { actions() }
+                            }
+                        },
                         scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
                         colors = transparentColors
                     )
@@ -153,7 +166,15 @@ fun GlassMediumFlexibleTopAppBar(
                             }
                         },
                         navigationIcon = navigationIcon,
-                        actions = actions,
+                        actions = {
+                            Box(modifier = Modifier.padding(end = 12.dp)) {
+                                Row(
+                                    horizontalArrangement = Arrangement.spacedBy(8.dp)
+                                ) {
+                                    actions()
+                                }
+                            }
+                        },
                         scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
                         colors = transparentColors
                     )

+ 7 - 14
app/src/main/java/io/legado/app/ui/widget/components/topbar/TopBarButton.kt

@@ -30,13 +30,13 @@ import io.legado.app.R
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
-import io.legado.app.ui.widget.components.button.AnimatedActionButtonCore
-import io.legado.app.ui.widget.components.button.AnimatedIcon
+import io.legado.app.ui.widget.components.button.series.AnimatedActionButtonCore
+import io.legado.app.ui.widget.components.button.series.AnimatedIcon
 import io.legado.app.ui.widget.components.icon.AppIcons
 import top.yukonga.miuix.kmp.theme.MiuixTheme
-import top.yukonga.miuix.kmp.basic.Text as MiuixText
 import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
 import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.basic.Text as MiuixText
 
 @Composable
 private fun TopBarButton(
@@ -112,7 +112,7 @@ fun TopBarActionButton(
                 onClick = onClick,
                 imageVector = imageVector,
                 contentDescription = contentDescription,
-                modifier = modifier.padding(end = 12.dp)
+                modifier = modifier
             )
         } else {
             IconButton(
@@ -140,16 +140,10 @@ fun TopBarAnimatedActionButton(
     modifier: Modifier = Modifier
 ) {
     if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
-        val containerColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
-            animationSpec = tween(150),
-            label = "MiuixActionButtonContainer"
-        )
-
         val contentColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
+            targetValue = if (checked) MiuixTheme.colorScheme.primary else MiuixTheme.colorScheme.onSurface,
             animationSpec = tween(150),
-            label = "MiuixActionButtonContainer"
+            label = "MiuixActionButtonContent"
         )
 
         AnimatedActionButtonCore(
@@ -167,8 +161,7 @@ fun TopBarAnimatedActionButton(
             button = { buttonModifier, onToggle, content ->
                 MiuixIconButton(
                     onClick = { onToggle(!checked) },
-                    modifier = buttonModifier,
-                    backgroundColor = containerColor
+                    modifier = buttonModifier
                 ) {
                     Row(
                         verticalAlignment = Alignment.CenterVertically,

+ 30 - 6
app/src/main/java/io/legado/app/web/socket/BookSearchWebSocket.kt

@@ -1,24 +1,41 @@
 package io.legado.app.web.socket
 
-import io.ktor.server.websocket.*
-import io.ktor.websocket.*
+import io.ktor.server.websocket.DefaultWebSocketServerSession
+import io.ktor.websocket.CloseReason
+import io.ktor.websocket.Frame
+import io.ktor.websocket.close
+import io.ktor.websocket.readText
+import io.ktor.websocket.send
 import io.legado.app.R
-import io.legado.app.constant.PreferKey
+import io.legado.app.data.local.preferences.LocalPreferencesKeys
+import io.legado.app.data.local.preferences.LocalPreferencesRepository
 import io.legado.app.domain.model.BookSearchScope
+import io.legado.app.domain.model.MatchMode
 import io.legado.app.domain.usecase.BookSearchControl
 import io.legado.app.domain.usecase.BookSearchRequest
 import io.legado.app.domain.usecase.SearchBooksUseCase
 import io.legado.app.domain.usecase.SearchRunEvent
 import io.legado.app.help.config.AppConfig
 import io.legado.app.ui.config.otherConfig.OtherConfig
-import io.legado.app.utils.*
-import kotlinx.coroutines.*
+import io.legado.app.utils.GSON
+import io.legado.app.utils.fromJsonObject
+import io.legado.app.utils.isJson
+import io.legado.app.utils.printOnDebug
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
 import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 
 class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) : CoroutineScope by session {
 
     private val searchBooksUseCase: SearchBooksUseCase by lazy { GlobalContext.get().get() }
+    private val localPreferencesRepository: LocalPreferencesRepository by lazy {
+        GlobalContext.get().get()
+    }
     private val searchControl = BookSearchControl()
     private val sentBookUrls = linkedSetOf<String>()
     private var searchJob: Job? = null
@@ -66,7 +83,14 @@ class BookSearchWebSocket(private val session: DefaultWebSocketServerSession) :
                             keyword = key,
                             page = 1,
                             scope = BookSearchScope(AppConfig.searchScope),
-                            precision = appCtx.getPrefBoolean(PreferKey.precisionSearch),
+                            matchMode = MatchMode.of(
+                                localPreferencesRepository
+                                    .getPreference(
+                                        LocalPreferencesKeys.MATCH_MODE,
+                                        MatchMode.DEFAULT.value
+                                    )
+                                    .first()
+                            ),
                             concurrency = OtherConfig.threadCount,
                         ),
                         searchControl

+ 4 - 2
app/src/main/res/values-zh-rCN/strings.xml

@@ -24,7 +24,7 @@
     <string name="dialog_setting">去设置</string>
     <string name="tip_cannot_jump_setting_page">无法跳转至设置界面</string>
 
-    <string name="manual_input">手动输入</string>
+    <string name="manual_input">输入URL</string>
     <string name="preferred_contrast">偏好对比度</string>
     <string name="contrast_source_system_summary">来源:系统对比度</string>
     <string name="contrast_source_manual_summary">来源:手动输入</string>
@@ -897,7 +897,7 @@
     <string name="body_title">正文标题</string>
     <string name="show_hide">显示/隐藏</string>
     <string name="header_footer">页眉与页脚</string>
-    <string name="rule_subscription">规则订阅</string>
+    <string name="rule_subscription">规则</string>
     <string name="rule_sub_empty_msg">添加大佬们提供的规则导入地址\n添加后点击可导入规则</string>
     <string name="get_book_progress">拉取云端进度</string>
     <string name="cover_book_progress">覆盖云端进度</string>
@@ -1643,6 +1643,8 @@
     <string name="search_source_progress"> · 进度 %1$d/%2$d</string>
     <string name="search_empty_scope_disable_precision">%1$s分组搜索结果为空,是否关闭精准搜索?</string>
     <string name="search_empty_scope_switch_all">%1$s分组搜索结果为空,是否切换到全部分组?</string>
+    <string name="search_layout_list">列表布局</string>
+    <string name="search_layout_source_grouped">来源分组</string>
     <string name="download_cache_config">下载与缓存</string>
     <string name="http_cache">HTTP缓存</string>
     <string name="cover_cache">封面缓存</string>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1679,6 +1679,8 @@
     <string name="search_source_progress"> · Progress %1$d/%2$d</string>
     <string name="search_empty_scope_disable_precision">%1$s group search returned no results. Disable precise search?</string>
     <string name="search_empty_scope_switch_all">%1$s group search returned no results. Switch to all groups?</string>
+    <string name="search_layout_list">List layout</string>
+    <string name="search_layout_source_grouped">Source grouped</string>
     <string name="download_cache_config">Download &amp; Cache</string>
     <string name="http_cache">HTTP Cache</string>
     <string name="cover_cache">Cover Cache</string>