HapeLee 6 miesięcy temu
rodzic
commit
7a6515d600

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

@@ -69,7 +69,10 @@
       "Bash(Select-String -Pattern \"FAILED|error:\")",
       "Bash(Select-Object -First 5)",
       "Bash(Select-String -NotMatch \"^\\\\s*$\")",
-      "Bash(Select-Object -Last 20)"
+      "Bash(Select-Object -Last 20)",
+      "WebFetch(domain:compose-miuix-ui.github.io)",
+      "Bash(Select-Object -Last 60)",
+      "PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 60)"
     ]
   }
 }

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -307,6 +307,7 @@ object PreferKey {
     const val showBookIntro = "showBookIntro"
     const val bookshelfShowIntro = "bookshelfShowIntro"
     const val bookshelfShowTag = "bookshelfShowTag"
+    const val bookshelfShowLatestChapter = "bookshelfShowLatestChapter"
     const val bookshelfIntroMaxLines = "bookshelfIntroMaxLines"
     const val sliderVibrator = "sliderVibrator"
     const val selectVibrator = "selectVibrator"

+ 2 - 15
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -42,9 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
 import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Slider
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import io.legado.app.ui.widget.components.AppPullToRefresh
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
@@ -136,7 +134,6 @@ fun ExploreShowScreen(
     }
 
     val isRefreshing by viewModel.isRefreshing.collectAsState()
-    val pullToRefreshState = rememberPullToRefreshState()
 
     val hazeState = remember { HazeState() }
     val shouldLoadMoreList = remember {
@@ -409,20 +406,10 @@ fun ExploreShowScreen(
             )
         }
     ) { paddingValues ->
-        PullToRefreshBox(
+        AppPullToRefresh(
             modifier = Modifier.fillMaxSize(),
             isRefreshing = isRefreshing,
-            state = pullToRefreshState,
             onRefresh = { viewModel.loadMore(isRefresh = true) },
-            indicator = {
-                PullToRefreshDefaults.LoadingIndicator(
-                    state = pullToRefreshState,
-                    isRefreshing = isRefreshing,
-                    modifier = Modifier
-                        .align(Alignment.TopCenter)
-                        .padding(top = paddingValues.calculateTopPadding())
-                )
-            }
         ) {
             Crossfade(
                 targetState = isGridMode,

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

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
@@ -35,8 +36,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import io.legado.app.ui.widget.components.AppPullToRefresh
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -178,31 +178,29 @@ private fun ImportBookContent(
             onSelectInvert = onSelectInvert,
             primaryAction = ActionItem(
                 text = stringResource(R.string.add_to_bookshelf),
-                icon = { Icon(Icons.Default.CloudDownload, null) },
+                icon = Icons.Default.CloudDownload,
                 onClick = onAddToBookshelf
             ),
             secondaryActions = listOf(
                 ActionItem(
                     text = stringResource(R.string.delete),
-                    icon = { Icon(Icons.Default.Delete, null) },
+                    icon = Icons.Default.Delete,
                     onClick = onDeleteSelection
                 )
             )
         ),
         onAddClick = null
     ) { paddingValues ->
-        val refreshState = rememberPullToRefreshState()
-        PullToRefreshBox(
+        AppPullToRefresh(
             modifier = Modifier
                 .fillMaxSize()
                 .padding(paddingValues),
             isRefreshing = state.isLoading,
-            state = refreshState,
-            onRefresh = onScanFolder
+            onRefresh = onScanFolder,
         ) {
             when {
                 state.items.isEmpty() && state.isLoading -> {
-                    CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+                    CircularProgressIndicator(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center))
                 }
 
                 state.items.isEmpty() -> {

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

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.padding
@@ -44,8 +45,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import io.legado.app.ui.widget.components.AppPullToRefresh
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -84,6 +84,7 @@ import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -303,7 +304,7 @@ fun RemoteBookScreen(
             onSelectInvert = { viewModel.dispatch(RemoteBookIntent.SelectInvert) },
             primaryAction = ActionItem(
                 text = "添加至书架",
-                icon = { Icon(Icons.Default.CloudDownload, null) },
+                icon = Icons.Default.CloudDownload,
                 onClick = {
                     val selectedBooks = uiState.items
                         .filter { it.id in uiState.selectedIds }
@@ -316,18 +317,16 @@ fun RemoteBookScreen(
         ),
         onAddClick = null,
     ) { paddingValues ->
-        val pullToRefreshState = rememberPullToRefreshState()
-        PullToRefreshBox(
+        AppPullToRefresh(
             modifier = Modifier
                 .fillMaxSize()
                 .padding(paddingValues),
             isRefreshing = uiState.isLoading,
-            state = pullToRefreshState,
-            onRefresh = { viewModel.dispatch(RemoteBookIntent.Refresh) }
+            onRefresh = { viewModel.dispatch(RemoteBookIntent.Refresh) },
         ) {
             if (uiState.items.isEmpty()) {
                 if (uiState.isLoading) {
-                    CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+                    CircularProgressIndicator(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center))
                 } else {
                     EmptyMessage(
                         modifier = Modifier

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

@@ -45,9 +45,7 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.MediumFlexibleTopAppBar
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import io.legado.app.ui.widget.components.AppPullToRefresh
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -155,7 +153,6 @@ private fun BookInfoScreenContent(
         M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior())
     }
     val listState = rememberLazyListState()
-    val pullState = rememberPullToRefreshState()
     var showMenu by rememberSaveable { mutableStateOf(false) }
 
     AppScaffold(
@@ -194,22 +191,10 @@ private fun BookInfoScreenContent(
         } else {
             Box(modifier = Modifier.fillMaxSize()) {
                 BookInfoBackdrop(book)
-                PullToRefreshBox(
+                AppPullToRefresh(
                     modifier = Modifier.fillMaxSize(),
-                    state = pullState,
                     isRefreshing = state.isTocLoading,
                     onRefresh = { onIntent(BookInfoIntent.MenuAction(BookInfoMenuAction.Refresh)) },
-                    indicator = {
-                        PullToRefreshDefaults.LoadingIndicator(
-                            state = pullState,
-                            isRefreshing = state.isTocLoading,
-                            containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
-                            color = LegadoTheme.colorScheme.primary,
-                            modifier = Modifier
-                                .align(Alignment.TopCenter)
-                                .padding(top = paddingValues.calculateTopPadding())
-                        )
-                    }
                 ) {
                     LazyColumn(
                         state = listState,

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

@@ -108,6 +108,7 @@ 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
 import io.legado.app.ui.widget.components.divider.PillHeaderDivider
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -244,17 +245,17 @@ fun TocScreen(
         listOf(
             ActionItem(
                 text = "反选",
-                icon = { Icon(Icons.Default.Refresh, contentDescription = null) },
+                icon = Icons.Default.Refresh,
                 onClick = { viewModel.invertSelection() }
             ),
             ActionItem(
                 text = "选择后续",
-                icon = { Icon(Icons.Default.ExpandMore, contentDescription = null) },
+                icon = Icons.Default.ExpandMore,
                 onClick = { viewModel.selectFromLast() }
             ),
             ActionItem(
                 text = "添加书签",
-                icon = { Icon(Icons.Default.BookmarkAdd, contentDescription = null) },
+                icon = Icons.Default.BookmarkAdd,
                 onClick = { viewModel.addBookmarksForSelected() }
             )
         )
@@ -577,7 +578,7 @@ fun TocScreen(
                     onSelectInvert = { viewModel.invertSelection() },
                     primaryAction = ActionItem(
                         text = "下载已选 (${state.selectedIds.size})",
-                        icon = { Icon(Icons.Default.Download, null) },
+                        icon = Icons.Default.Download,
                         onClick = { viewModel.downloadSelected() }
                     ),
                     secondaryActions = selectionSecondaryActions

+ 7 - 0
app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt

@@ -91,6 +91,13 @@ object BookshelfConfig {
     var bookshelfShowTag by _bookshelfShowTag
     val bookshelfShowTagState: State<Boolean> get() = _bookshelfShowTag.state
 
+    /**
+     * 是否显示最新章节
+     */
+    private val _bookshelfShowLatestChapter = prefStateDelegate(PreferKey.bookshelfShowLatestChapter, true)
+    var bookshelfShowLatestChapter by _bookshelfShowLatestChapter
+    val bookshelfShowLatestChapterState: State<Boolean> get() = _bookshelfShowLatestChapter.state
+
     /**
      * 列表模式下简介显示行数 (0为显示全部)
      */

+ 50 - 43
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -7,10 +7,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
@@ -77,6 +79,7 @@ fun BookshelfItem(
     desc: String? = null,
     descMaxLines: Int = 1,
     extra: @Composable (RowScope.() -> Unit)? = null,
+    columnContent: @Composable (ColumnScope.() -> Unit)? = null,
     bottomContent: @Composable (() -> Unit)? = null,
     titleSmallFont: Boolean = false,
     titleCenter: Boolean = true,
@@ -185,6 +188,7 @@ fun BookshelfItem(
                 ) {
                     Box(
                         modifier = Modifier
+                            .align(Alignment.Top)
                             .width(if (!isCompact) 80.dp else 56.dp)
                             .padding(end = 8.dp)
                     ) {
@@ -205,7 +209,7 @@ fun BookshelfItem(
                         }
                     }
                     Column(
-                        modifier = Modifier.weight(1f).padding(top = 4.dp, bottom = 4.dp, end = 8.dp),
+                        modifier = Modifier.weight(1f).padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
                         verticalArrangement = Arrangement.Center
                     ) {
                     Row(
@@ -257,6 +261,7 @@ fun BookshelfItem(
                                 content = it
                             )
                         }
+                        columnContent?.invoke(this)
                     }
                 }
                 bottomContent?.invoke()
@@ -438,12 +443,11 @@ fun BookGroupItemList(
         return
     }
     val firstBookName = previewBooks.firstOrNull()?.name
-    val primaryColor = MaterialTheme.colorScheme.primary
     val descAnnotated = if (firstBookName != null) {
         buildAnnotatedString {
             append("最近阅读:")
-            withStyle(SpanStyle(color = primaryColor, fontWeight = FontWeight.Medium)) {
-                append("《$firstBookName》")
+            withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
+                append(firstBookName)
             }
         }
     } else {
@@ -455,7 +459,6 @@ fun BookGroupItemList(
         isCompact = BookshelfConfig.bookshelfGroupListStyle == 1 || isCompact,
         cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
         title = group.groupName,
-        titleColor = primaryColor,
         subTitle = countText,
         descAnnotated = descAnnotated,
         titleSmallFont = titleSmallFont,
@@ -649,37 +652,38 @@ fun BookItem(
             book.author
         },
         desc = book.durChapterTitle ?: "",
-        bottomContent = if (layoutMode == 0 && BookshelfConfig.showBookIntro) {
+        columnContent = if (layoutMode == 0 && !isCompact && BookshelfConfig.showBookIntro) {
             {
                 val kindList = book.kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }
                 val intro = book.intro?.takeIf { it.isNotBlank() }
                 val customTagColors = if (ThemeConfig.enableCustomTagColors) ThemeConfig.getCustomTagColors() else emptyList()
                 if (BookshelfConfig.bookshelfShowTag && !kindList.isNullOrEmpty()) {
-                        FlowRow(
-                            modifier = Modifier
-                                .fillMaxWidth()
-                                .padding(horizontal = 4.dp, vertical = 4.dp),
-                            horizontalArrangement = Arrangement.spacedBy(4.dp),
-                            verticalArrangement = Arrangement.spacedBy(4.dp)
-                        ) {
-                            kindList.forEachIndexed { index, label ->
-                                val colorPair = if (customTagColors.isNotEmpty()) {
-                                    customTagColors[index % customTagColors.size]
-                                } else {
-                                    null
-                                }
-                                TextCard(
-                                    text = label,
-                                    backgroundColor = if (colorPair != null && colorPair.bgColor != 0) Color(colorPair.bgColor) else LegadoTheme.colorScheme.surfaceContainerHighest,
-                                    contentColor = if (colorPair != null && colorPair.textColor != 0) Color(colorPair.textColor) else LegadoTheme.colorScheme.primary,
-                                    cornerRadius = 4.dp,
-                                    horizontalPadding = 6.dp,
-                                    verticalPadding = 2.dp,
-                                    textStyle = LegadoTheme.typography.labelSmall
-                                )
+                    Row(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(vertical = 4.dp)
+                            .horizontalScroll(rememberScrollState()),
+                        horizontalArrangement = Arrangement.spacedBy(4.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        kindList.forEachIndexed { index, label ->
+                            val colorPair = if (customTagColors.isNotEmpty()) {
+                                customTagColors[index % customTagColors.size]
+                            } else {
+                                null
                             }
+                            TextCard(
+                                text = label,
+                                backgroundColor = if (colorPair != null && colorPair.bgColor != 0) Color(colorPair.bgColor) else LegadoTheme.colorScheme.surfaceContainerHigh,
+                                contentColor = if (colorPair != null && colorPair.textColor != 0) Color(colorPair.textColor) else LegadoTheme.colorScheme.primary.copy(alpha = 0.8f),
+                                cornerRadius = 4.dp,
+                                horizontalPadding = 4.dp,
+                                verticalPadding = 2.dp,
+                                textStyle = LegadoTheme.typography.labelSmallEmphasized
+                            )
                         }
                     }
+                }
                 if (BookshelfConfig.bookshelfShowIntro && intro != null) {
                     val maxLines = if (BookshelfConfig.bookshelfIntroMaxLines == 0) Int.MAX_VALUE else BookshelfConfig.bookshelfIntroMaxLines
                     AppText(
@@ -690,29 +694,32 @@ fun BookItem(
                         overflow = TextOverflow.Ellipsis,
                         modifier = Modifier
                             .fillMaxWidth()
-                            .padding(horizontal = 4.dp, vertical = 4.dp)
+                            .padding(vertical = 4.dp)
                     )
                 }
             }
         } else null,
-        extra = {
-            if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
+        bottomContent = null,
+        extra = if (layoutMode == 0 && !isCompact && BookshelfConfig.showBookIntro && BookshelfConfig.bookshelfShowLatestChapter) {
+            {
+                if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
+                    AppText(
+                        text = book.latestChapterTime.toTimeAgo(),
+                        style = LegadoTheme.typography.labelSmallEmphasized,
+                        color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
+                        modifier = Modifier.padding(end = 4.dp)
+                    )
+                }
                 AppText(
-                    text = book.latestChapterTime.toTimeAgo(),
+                    text = book.latestChapterTitle ?: "",
                     style = LegadoTheme.typography.labelSmallEmphasized,
                     color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
-                    modifier = Modifier.padding(end = 4.dp)
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                    modifier = Modifier.weight(1f)
                 )
             }
-            AppText(
-                text = book.latestChapterTitle ?: "",
-                style = LegadoTheme.typography.labelSmallEmphasized,
-                color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-                modifier = Modifier.weight(1f)
-            )
-        },
+        } else null,
         titleSmallFont = titleSmallFont,
         titleCenter = titleCenter,
         titleMaxLines = titleMaxLines,

+ 7 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt

@@ -288,6 +288,13 @@ fun BookshelfConfigSheet(
                                 Column(
                                     verticalArrangement = Arrangement.spacedBy(8.dp)
                                 ) {
+                                    CompactSwitchSettingItem(
+                                        title = "显示最新章节",
+                                        checked = BookshelfConfig.bookshelfShowLatestChapter,
+                                        color = LegadoTheme.colorScheme.surface,
+                                        onCheckedChange = { BookshelfConfig.bookshelfShowLatestChapter = it }
+                                    )
+
                                     CompactSwitchSettingItem(
                                         title = "显示简介",
                                         checked = BookshelfConfig.bookshelfShowIntro,

+ 3 - 2
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -132,6 +132,7 @@ 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
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
@@ -1061,7 +1062,7 @@ fun BookshelfScreen(
                     onSelectInvert = { viewModel.invertVisibleSelection() },
                     primaryAction = ActionItem(
                         text = stringResource(R.string.action_download),
-                        icon = { Icon(Icons.Default.Download, contentDescription = null) },
+                        icon = Icons.Default.Download,
                         onClick = {
                             if (selectedBookUrls.isNotEmpty()) {
                                 viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
@@ -1071,7 +1072,7 @@ fun BookshelfScreen(
                     secondaryActions = listOf(
                         ActionItem(
                             text = stringResource(R.string.move_to_group),
-                            icon = { Icon(Icons.Default.Bookmarks, contentDescription = null) },
+                            icon = Icons.Default.Bookmarks,
                             onClick = {
                                 if (selectedBookUrls.isNotEmpty()) {
                                     viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)

+ 6 - 2
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -710,7 +710,9 @@ class BookshelfViewModel(
     fun refreshBooks(books: List<BookShelfItem>) {
         if (isRefreshingFlow.value) return
         isRefreshingFlow.value = true
-        enqueueTocUpdate(books, resetRefreshWhenIdle = true)
+        val limit = BookshelfConfig.bookshelfRefreshingLimit
+        val list = if (limit > 0) books.take(limit) else books
+        enqueueTocUpdate(list, resetRefreshWhenIdle = true)
     }
 
     fun startDraggingBooks(books: List<BookShelfItem>) {
@@ -761,7 +763,9 @@ class BookshelfViewModel(
     }
 
     fun upToc(books: List<BookShelfItem>) {
-        enqueueTocUpdate(books, resetRefreshWhenIdle = false)
+        val limit = BookshelfConfig.bookshelfRefreshingLimit
+        val list = if (limit > 0) books.take(limit) else books
+        enqueueTocUpdate(list, resetRefreshWhenIdle = false)
     }
 
     private fun enqueueTocUpdate(

+ 8 - 37
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -118,37 +118,12 @@ fun ExploreScreen(
         }
     }
 
-    val expandedHeader = remember(uiState.expandedId, uiState.listItems) {
-        val expandedId = uiState.expandedId ?: return@remember null
-        val headerIndex = uiState.listItems.indexOfFirst {
-            it is ExploreListItem.Header && it.source.bookSourceUrl == expandedId
-        }
-        val headerItem = uiState.listItems.getOrNull(headerIndex) as? ExploreListItem.Header
-        if (headerItem != null) {
-            ExpandedExploreHeader(
-                source = headerItem.source,
-                headerIndex = headerIndex,
-                contentRowCount = uiState.listItems.count {
-                    it is ExploreListItem.KindRow && it.sourceUrl == expandedId
-                }
-            )
-        } else {
-            null
-        }
-    }
-
-    LaunchedEffect(expandedHeader?.headerIndex) {
-        expandedHeader?.let { listState.animateScrollToItem(it.headerIndex) }
-    }
-
-    val stickyHeaderSource by remember(expandedHeader) {
+    val stickyHeaderSource by remember(uiState.listItems, uiState.items) {
         derivedStateOf {
-            val header = expandedHeader ?: return@derivedStateOf null
-            val lastContentIndex = header.headerIndex + header.contentRowCount
-            val firstVisible = listState.firstVisibleItemIndex
-
-            if (firstVisible in (header.headerIndex + 1)..lastContentIndex) {
-                header.source
+            val firstIndex = listState.firstVisibleItemIndex
+            val item = uiState.listItems.getOrNull(firstIndex)
+            if (item is ExploreListItem.KindRow) {
+                uiState.items.find { it.bookSourceUrl == item.sourceUrl }
             } else {
                 null
             }
@@ -292,8 +267,9 @@ fun ExploreScreen(
                     verticalPadding = 8.dp,
                     onClick = {
                         scope.launch {
-                            val index =
-                                uiState.items.indexOfFirst { it.bookSourceUrl == item.bookSourceUrl }
+                            val index = uiState.listItems.indexOfFirst {
+                                it is ExploreListItem.Header && it.source.bookSourceUrl == item.bookSourceUrl
+                            }
                             if (index >= 0) listState.animateScrollToItem(index)
                         }
                     }
@@ -317,11 +293,6 @@ fun ExploreScreen(
     )
 }
 
-private data class ExpandedExploreHeader(
-    val source: BookSourcePart,
-    val headerIndex: Int,
-    val contentRowCount: Int
-)
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable

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

@@ -33,8 +33,7 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Text
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import io.legado.app.ui.widget.components.AppPullToRefresh
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -132,17 +131,15 @@ fun RssArticlesPage(
         loadState.errorMessage?.takeIf { it.isNotBlank() }?.let(context::toastOnUi)
     }
 
-    val refreshState = rememberPullToRefreshState()
     val contentPadding = adaptiveContentPadding(
         top = paddingValues.calculateTopPadding(),
         bottom = 120.dp
     )
 
-    PullToRefreshBox(
+    AppPullToRefresh(
         isRefreshing = loadState.isRefreshing,
         onRefresh = { rssSource?.let(viewModel::loadArticles) },
-        state = refreshState,
-        modifier = modifier.fillMaxSize()
+        modifier = modifier.fillMaxSize(),
     ) {
         when (layout) {
             RssArticleLayout.List, RssArticleLayout.LargeCard -> {

+ 58 - 0
app/src/main/java/io/legado/app/ui/widget/components/AppPullToRefresh.kt

@@ -0,0 +1,58 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.PullToRefresh as MiuixPullToRefresh
+import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState as miuixRememberPullToRefreshState
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AppPullToRefresh(
+    isRefreshing: Boolean,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    content: @Composable () -> Unit,
+) {
+    if (ThemeResolver.isMiuixEngine(composeEngine)) {
+        if (enabled) {
+            MiuixPullToRefresh(
+                isRefreshing = isRefreshing,
+                onRefresh = onRefresh,
+                modifier = modifier,
+                pullToRefreshState = miuixRememberPullToRefreshState(),
+                refreshTexts = listOf(
+                    "下拉刷新",
+                    "松开刷新",
+                    "正在刷新",
+                    "刷新成功"
+                )
+            ) {
+                content()
+            }
+        } else {
+            Box(modifier) {
+                content()
+            }
+        }
+    } else {
+        if (enabled) {
+            androidx.compose.material3.pulltorefresh.PullToRefreshBox(
+                isRefreshing = isRefreshing,
+                onRefresh = onRefresh,
+                modifier = modifier,
+                state = androidx.compose.material3.pulltorefresh.rememberPullToRefreshState(),
+            ) {
+                content()
+            }
+        } else {
+            Box(modifier) {
+                content()
+            }
+        }
+    }
+}

+ 132 - 55
app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt

@@ -1,8 +1,12 @@
 package io.legado.app.ui.widget.components
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ExpandMore
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.SelectAll
@@ -12,7 +16,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.FilledIconButton
 import androidx.compose.material3.HorizontalFloatingToolbar
-import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.PlainTooltip
 import androidx.compose.material3.TooltipAnchorPosition
@@ -24,9 +27,20 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+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.ThemeResolver
+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 top.yukonga.miuix.kmp.basic.FloatingToolbar as MiuixFloatingToolbar
+import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
+import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
+import top.yukonga.miuix.kmp.theme.MiuixTheme
 
 data class SelectionActions(
     val primaryAction: ActionItem,
@@ -38,7 +52,7 @@ data class SelectionActions(
 
 data class ActionItem(
     val text: String,
-    val icon: @Composable (() -> Unit)? = null,
+    val icon: ImageVector = Icons.Default.ExpandMore,
     val onClick: () -> Unit
 )
 
@@ -52,66 +66,129 @@ fun SelectionBottomBar(
     secondaryActions: List<ActionItem>
 ) {
     var showMenu by remember { mutableStateOf(false) }
+    val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
 
-    HorizontalFloatingToolbar(
-        modifier = modifier,
-        expanded = true,
-        leadingContent = {
-            IconButton(onClick = onSelectAll) {
-                Icon(
-                    imageVector = Icons.Default.SelectAll,
-                    contentDescription = "Select All"
-                )
-            }
-            IconButton(onClick = onSelectInvert) {
-                Icon(
-                    imageVector = Icons.Default.Refresh,
-                    contentDescription = "Invert Selection"
-                )
-            }
-        },
-        trailingContent = {
-            if (secondaryActions.isNotEmpty()) {
-                Box {
-                    IconButton(onClick = { showMenu = true }) {
-                        Icon(
-                            imageVector = Icons.Default.MoreVert,
-                            contentDescription = "More actions"
-                        )
-                    }
-                    DropdownMenu(
-                        expanded = showMenu,
-                        onDismissRequest = { showMenu = false }
-                    ) {
-                        secondaryActions.forEach { action ->
-                            DropdownMenuItem(
-                                text = { AppText(action.text) },
-                                leadingIcon = action.icon,
-                                onClick = {
-                                    action.onClick()
-                                    showMenu = false
-                                }
+    if (isMiuix) {
+        MiuixFloatingToolbar(
+            modifier = modifier
+        ) {
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement = Arrangement.spacedBy(8.dp),
+                modifier = Modifier.padding(8.dp)
+            ) {
+                MiuixIconButton(onClick = onSelectAll) {
+                    MiuixIcon(
+                        imageVector = Icons.Default.SelectAll,
+                        contentDescription = "Select All"
+                    )
+                }
+                MiuixIconButton(onClick = onSelectInvert) {
+                    MiuixIcon(
+                        imageVector = Icons.Default.Refresh,
+                        contentDescription = "Invert Selection"
+                    )
+                }
+
+                MiuixIconButton(
+                    onClick = primaryAction.onClick,
+                    backgroundColor = MiuixTheme.colorScheme.primary,
+                    minWidth = 64.dp
+                ) {
+                    MiuixIcon(
+                        imageVector = primaryAction.icon,
+                        contentDescription = null,
+                        tint = MiuixTheme.colorScheme.onPrimary
+                    )
+                }
+
+                if (secondaryActions.isNotEmpty()) {
+                    Box {
+                        MiuixIconButton(onClick = { showMenu = true }) {
+                            MiuixIcon(
+                                imageVector = Icons.Default.MoreVert,
+                                contentDescription = "More actions"
                             )
                         }
+                        RoundDropdownMenu(
+                            expanded = showMenu,
+                            onDismissRequest = { showMenu = false }
+                        ) {
+                            secondaryActions.forEach { action ->
+                                RoundDropdownMenuItem(
+                                    text = action.text,
+                                    onClick = {
+                                        action.onClick()
+                                        showMenu = false
+                                    }
+                                )
+                            }
+                        }
                     }
                 }
             }
-        },
-        content = {
-            TooltipBox(
-                positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
-                    TooltipAnchorPosition.Above
-                ),
-                tooltip = { PlainTooltip { AppText(primaryAction.text) } },
-                state = rememberTooltipState(),
-            ) {
-                FilledIconButton(
-                    modifier = Modifier.width(64.dp),
-                    onClick = primaryAction.onClick,
+        }
+    } else {
+        HorizontalFloatingToolbar(
+            modifier = modifier,
+            expanded = true,
+            leadingContent = {
+                IconButton(onClick = onSelectAll) {
+                    AppIcon(
+                        imageVector = Icons.Default.SelectAll,
+                        contentDescription = "Select All"
+                    )
+                }
+                IconButton(onClick = onSelectInvert) {
+                    AppIcon(
+                        imageVector = Icons.Default.Refresh,
+                        contentDescription = "Invert Selection"
+                    )
+                }
+            },
+            trailingContent = {
+                if (secondaryActions.isNotEmpty()) {
+                    Box {
+                        IconButton(onClick = { showMenu = true }) {
+                            AppIcon(
+                                imageVector = Icons.Default.MoreVert,
+                                contentDescription = "More actions"
+                            )
+                        }
+                        DropdownMenu(
+                            expanded = showMenu,
+                            onDismissRequest = { showMenu = false }
+                        ) {
+                            secondaryActions.forEach { action ->
+                                DropdownMenuItem(
+                                    text = { AppText(action.text) },
+                                    leadingIcon = action.icon?.let { { AppIcon(imageVector = it, contentDescription = null) } },
+                                    onClick = {
+                                        action.onClick()
+                                        showMenu = false
+                                    }
+                                )
+                            }
+                        }
+                    }
+                }
+            },
+            content = {
+                TooltipBox(
+                    positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
+                        TooltipAnchorPosition.Above
+                    ),
+                    tooltip = { PlainTooltip { AppText(primaryAction.text) } },
+                    state = rememberTooltipState(),
                 ) {
-                    primaryAction.icon?.invoke()
+                    FilledIconButton(
+                        modifier = Modifier.width(64.dp),
+                        onClick = primaryAction.onClick,
+                    ) {
+                        AppIcon(imageVector = primaryAction.icon, contentDescription = null)
+                    }
                 }
             }
-        }
-    )
+        )
+    }
 }

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

@@ -106,7 +106,7 @@ fun <T> RuleListScaffold(
             onSelectInvert = onSelectInvert,
             primaryAction = ActionItem(
                 text = stringResource(R.string.delete),
-                icon = { Icon(Icons.Default.Delete, null) },
+                icon = Icons.Default.Delete,
                 onClick = { showDeleteConfirmDialog = true }
             ),
             secondaryActions = selectionSecondaryActions