From 7a6515d6001db74308d4e2d19ad5d73303dc6d65 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Thu, 14 May 2026 23:46:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=84=E4=BB=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- .../java/io/legado/app/constant/PreferKey.kt | 1 + .../app/ui/book/explore/ExploreShowScreen.kt | 17 +- .../ui/book/import/local/ImportBookScreen.kt | 16 +- .../ui/book/import/remote/RemoteBookScreen.kt | 15 +- .../legado/app/ui/book/info/BookInfoScreen.kt | 19 +- .../io/legado/app/ui/book/toc/TocScreen.kt | 9 +- .../config/bookshelfConfig/BookshelfConfig.kt | 7 + .../legado/app/ui/main/bookshelf/BookItem.kt | 93 +++++---- .../ui/main/bookshelf/BookshelfConfigSheet.kt | 7 + .../app/ui/main/bookshelf/BookshelfScreen.kt | 5 +- .../ui/main/bookshelf/BookshelfViewModel.kt | 8 +- .../app/ui/main/explore/ExploreScreen.kt | 45 +---- .../app/ui/rss/article/RssArticlesCompose.kt | 9 +- .../ui/widget/components/AppPullToRefresh.kt | 58 ++++++ .../widget/components/SelectionBottomBar.kt | 187 ++++++++++++------ .../components/rules/RuleListScaffold.kt | 2 +- 17 files changed, 303 insertions(+), 200 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/AppPullToRefresh.kt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4965d8755..1e2472279 100644 --- a/.claude/settings.local.json +++ b/.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)" ] } } diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 5246289dd..3b66c94d9 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/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" diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt index 29081fb7b..b4ea46114 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt index b1b4a69f7..b9c9434a0 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt +++ b/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() -> { diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt index 466a421a2..d19be498e 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt index 61e0f0841..e135eab6c 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt index 951539704..ee19e3e7a 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt index 98efa72b1..0faf4ac07 100644 --- a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt +++ b/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 get() = _bookshelfShowTag.state + /** + * 是否显示最新章节 + */ + private val _bookshelfShowLatestChapter = prefStateDelegate(PreferKey.bookshelfShowLatestChapter, true) + var bookshelfShowLatestChapter by _bookshelfShowLatestChapter + val bookshelfShowLatestChapterState: State get() = _bookshelfShowLatestChapter.state + /** * 列表模式下简介显示行数 (0为显示全部) */ diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 46f32a766..e99927cf3 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt index c28068ad8..fc8f5bf3d 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index 146361064..4d7bb130b 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/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) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index f28732827..a5ce2071c 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -710,7 +710,9 @@ class BookshelfViewModel( fun refreshBooks(books: List) { 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) { @@ -761,7 +763,9 @@ class BookshelfViewModel( } fun upToc(books: List) { - 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( diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt index 0ed096f4b..d5e3d9ac4 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt index 7ce8fc8a5..0b97e0144 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt +++ b/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 -> { diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppPullToRefresh.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppPullToRefresh.kt new file mode 100644 index 000000000..544eb1811 --- /dev/null +++ b/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() + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt index 8a189abe1..08ec2af4a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt +++ b/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 ) { 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) + } } } - } - ) + ) + } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt index 15d69cc23..151457308 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt @@ -106,7 +106,7 @@ fun 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