组件优化

This commit is contained in:
HapeLee
2026-05-15 01:22:14 +08:00
parent 3247d48256
commit 7a6515d600
17 changed files with 303 additions and 200 deletions
+4 -1
View File
@@ -69,7 +69,10 @@
"Bash(Select-String -Pattern \"FAILED|error:\")", "Bash(Select-String -Pattern \"FAILED|error:\")",
"Bash(Select-Object -First 5)", "Bash(Select-Object -First 5)",
"Bash(Select-String -NotMatch \"^\\\\s*$\")", "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)"
] ]
} }
} }
@@ -307,6 +307,7 @@ object PreferKey {
const val showBookIntro = "showBookIntro" const val showBookIntro = "showBookIntro"
const val bookshelfShowIntro = "bookshelfShowIntro" const val bookshelfShowIntro = "bookshelfShowIntro"
const val bookshelfShowTag = "bookshelfShowTag" const val bookshelfShowTag = "bookshelfShowTag"
const val bookshelfShowLatestChapter = "bookshelfShowLatestChapter"
const val bookshelfIntroMaxLines = "bookshelfIntroMaxLines" const val bookshelfIntroMaxLines = "bookshelfIntroMaxLines"
const val sliderVibrator = "sliderVibrator" const val sliderVibrator = "sliderVibrator"
const val selectVibrator = "selectVibrator" const val selectVibrator = "selectVibrator"
@@ -42,9 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import io.legado.app.ui.widget.components.AppPullToRefresh
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -136,7 +134,6 @@ fun ExploreShowScreen(
} }
val isRefreshing by viewModel.isRefreshing.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState()
val pullToRefreshState = rememberPullToRefreshState()
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val shouldLoadMoreList = remember { val shouldLoadMoreList = remember {
@@ -409,20 +406,10 @@ fun ExploreShowScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
PullToRefreshBox( AppPullToRefresh(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
state = pullToRefreshState,
onRefresh = { viewModel.loadMore(isRefresh = true) }, onRefresh = { viewModel.loadMore(isRefresh = true) },
indicator = {
PullToRefreshDefaults.LoadingIndicator(
state = pullToRefreshState,
isRefreshing = isRefreshing,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = paddingValues.calculateTopPadding())
)
}
) { ) {
Crossfade( Crossfade(
targetState = isGridMode, targetState = isGridMode,
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -35,8 +36,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import io.legado.app.ui.widget.components.AppPullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -178,31 +178,29 @@ private fun ImportBookContent(
onSelectInvert = onSelectInvert, onSelectInvert = onSelectInvert,
primaryAction = ActionItem( primaryAction = ActionItem(
text = stringResource(R.string.add_to_bookshelf), text = stringResource(R.string.add_to_bookshelf),
icon = { Icon(Icons.Default.CloudDownload, null) }, icon = Icons.Default.CloudDownload,
onClick = onAddToBookshelf onClick = onAddToBookshelf
), ),
secondaryActions = listOf( secondaryActions = listOf(
ActionItem( ActionItem(
text = stringResource(R.string.delete), text = stringResource(R.string.delete),
icon = { Icon(Icons.Default.Delete, null) }, icon = Icons.Default.Delete,
onClick = onDeleteSelection onClick = onDeleteSelection
) )
) )
), ),
onAddClick = null onAddClick = null
) { paddingValues -> ) { paddingValues ->
val refreshState = rememberPullToRefreshState() AppPullToRefresh(
PullToRefreshBox(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
isRefreshing = state.isLoading, isRefreshing = state.isLoading,
state = refreshState, onRefresh = onScanFolder,
onRefresh = onScanFolder
) { ) {
when { when {
state.items.isEmpty() && state.isLoading -> { state.items.isEmpty() && state.isLoading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) CircularProgressIndicator(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center))
} }
state.items.isEmpty() -> { state.items.isEmpty() -> {
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -44,8 +45,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import io.legado.app.ui.widget.components.AppPullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.GlassCard
import io.legado.app.ui.widget.components.card.SelectionItemCard 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.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.list.ListScaffold
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem 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.modalBottomSheet.AppModalBottomSheet
@@ -303,7 +304,7 @@ fun RemoteBookScreen(
onSelectInvert = { viewModel.dispatch(RemoteBookIntent.SelectInvert) }, onSelectInvert = { viewModel.dispatch(RemoteBookIntent.SelectInvert) },
primaryAction = ActionItem( primaryAction = ActionItem(
text = "添加至书架", text = "添加至书架",
icon = { Icon(Icons.Default.CloudDownload, null) }, icon = Icons.Default.CloudDownload,
onClick = { onClick = {
val selectedBooks = uiState.items val selectedBooks = uiState.items
.filter { it.id in uiState.selectedIds } .filter { it.id in uiState.selectedIds }
@@ -316,18 +317,16 @@ fun RemoteBookScreen(
), ),
onAddClick = null, onAddClick = null,
) { paddingValues -> ) { paddingValues ->
val pullToRefreshState = rememberPullToRefreshState() AppPullToRefresh(
PullToRefreshBox(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
isRefreshing = uiState.isLoading, isRefreshing = uiState.isLoading,
state = pullToRefreshState, onRefresh = { viewModel.dispatch(RemoteBookIntent.Refresh) },
onRefresh = { viewModel.dispatch(RemoteBookIntent.Refresh) }
) { ) {
if (uiState.items.isEmpty()) { if (uiState.items.isEmpty()) {
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) CircularProgressIndicator(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center))
} else { } else {
EmptyMessage( EmptyMessage(
modifier = Modifier modifier = Modifier
@@ -45,9 +45,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MediumFlexibleTopAppBar import androidx.compose.material3.MediumFlexibleTopAppBar
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import io.legado.app.ui.widget.components.AppPullToRefresh
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -155,7 +153,6 @@ private fun BookInfoScreenContent(
M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior()) M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior())
} }
val listState = rememberLazyListState() val listState = rememberLazyListState()
val pullState = rememberPullToRefreshState()
var showMenu by rememberSaveable { mutableStateOf(false) } var showMenu by rememberSaveable { mutableStateOf(false) }
AppScaffold( AppScaffold(
@@ -194,22 +191,10 @@ private fun BookInfoScreenContent(
} else { } else {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
BookInfoBackdrop(book) BookInfoBackdrop(book)
PullToRefreshBox( AppPullToRefresh(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = pullState,
isRefreshing = state.isTocLoading, isRefreshing = state.isTocLoading,
onRefresh = { onIntent(BookInfoIntent.MenuAction(BookInfoMenuAction.Refresh)) }, 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( LazyColumn(
state = listState, state = listState,
@@ -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.card.TextCard
import io.legado.app.ui.widget.components.divider.PillDivider 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.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.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -244,17 +245,17 @@ fun TocScreen(
listOf( listOf(
ActionItem( ActionItem(
text = "反选", text = "反选",
icon = { Icon(Icons.Default.Refresh, contentDescription = null) }, icon = Icons.Default.Refresh,
onClick = { viewModel.invertSelection() } onClick = { viewModel.invertSelection() }
), ),
ActionItem( ActionItem(
text = "选择后续", text = "选择后续",
icon = { Icon(Icons.Default.ExpandMore, contentDescription = null) }, icon = Icons.Default.ExpandMore,
onClick = { viewModel.selectFromLast() } onClick = { viewModel.selectFromLast() }
), ),
ActionItem( ActionItem(
text = "添加书签", text = "添加书签",
icon = { Icon(Icons.Default.BookmarkAdd, contentDescription = null) }, icon = Icons.Default.BookmarkAdd,
onClick = { viewModel.addBookmarksForSelected() } onClick = { viewModel.addBookmarksForSelected() }
) )
) )
@@ -577,7 +578,7 @@ fun TocScreen(
onSelectInvert = { viewModel.invertSelection() }, onSelectInvert = { viewModel.invertSelection() },
primaryAction = ActionItem( primaryAction = ActionItem(
text = "下载已选 (${state.selectedIds.size})", text = "下载已选 (${state.selectedIds.size})",
icon = { Icon(Icons.Default.Download, null) }, icon = Icons.Default.Download,
onClick = { viewModel.downloadSelected() } onClick = { viewModel.downloadSelected() }
), ),
secondaryActions = selectionSecondaryActions secondaryActions = selectionSecondaryActions
@@ -91,6 +91,13 @@ object BookshelfConfig {
var bookshelfShowTag by _bookshelfShowTag var bookshelfShowTag by _bookshelfShowTag
val bookshelfShowTagState: State<Boolean> get() = _bookshelfShowTag.state 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为显示全部) * 列表模式下简介显示行数 (0为显示全部)
*/ */
@@ -7,10 +7,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
@@ -77,6 +79,7 @@ fun BookshelfItem(
desc: String? = null, desc: String? = null,
descMaxLines: Int = 1, descMaxLines: Int = 1,
extra: @Composable (RowScope.() -> Unit)? = null, extra: @Composable (RowScope.() -> Unit)? = null,
columnContent: @Composable (ColumnScope.() -> Unit)? = null,
bottomContent: @Composable (() -> Unit)? = null, bottomContent: @Composable (() -> Unit)? = null,
titleSmallFont: Boolean = false, titleSmallFont: Boolean = false,
titleCenter: Boolean = true, titleCenter: Boolean = true,
@@ -185,6 +188,7 @@ fun BookshelfItem(
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.Top)
.width(if (!isCompact) 80.dp else 56.dp) .width(if (!isCompact) 80.dp else 56.dp)
.padding(end = 8.dp) .padding(end = 8.dp)
) { ) {
@@ -205,7 +209,7 @@ fun BookshelfItem(
} }
} }
Column( 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 verticalArrangement = Arrangement.Center
) { ) {
Row( Row(
@@ -257,6 +261,7 @@ fun BookshelfItem(
content = it content = it
) )
} }
columnContent?.invoke(this)
} }
} }
bottomContent?.invoke() bottomContent?.invoke()
@@ -438,12 +443,11 @@ fun BookGroupItemList(
return return
} }
val firstBookName = previewBooks.firstOrNull()?.name val firstBookName = previewBooks.firstOrNull()?.name
val primaryColor = MaterialTheme.colorScheme.primary
val descAnnotated = if (firstBookName != null) { val descAnnotated = if (firstBookName != null) {
buildAnnotatedString { buildAnnotatedString {
append("最近阅读:") append("最近阅读:")
withStyle(SpanStyle(color = primaryColor, fontWeight = FontWeight.Medium)) { withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
append("$firstBookName") append(firstBookName)
} }
} }
} else { } else {
@@ -455,7 +459,6 @@ fun BookGroupItemList(
isCompact = BookshelfConfig.bookshelfGroupListStyle == 1 || isCompact, isCompact = BookshelfConfig.bookshelfGroupListStyle == 1 || isCompact,
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) }, cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
title = group.groupName, title = group.groupName,
titleColor = primaryColor,
subTitle = countText, subTitle = countText,
descAnnotated = descAnnotated, descAnnotated = descAnnotated,
titleSmallFont = titleSmallFont, titleSmallFont = titleSmallFont,
@@ -649,37 +652,38 @@ fun BookItem(
book.author book.author
}, },
desc = book.durChapterTitle ?: "", 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 kindList = book.kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }
val intro = book.intro?.takeIf { it.isNotBlank() } val intro = book.intro?.takeIf { it.isNotBlank() }
val customTagColors = if (ThemeConfig.enableCustomTagColors) ThemeConfig.getCustomTagColors() else emptyList() val customTagColors = if (ThemeConfig.enableCustomTagColors) ThemeConfig.getCustomTagColors() else emptyList()
if (BookshelfConfig.bookshelfShowTag && !kindList.isNullOrEmpty()) { if (BookshelfConfig.bookshelfShowTag && !kindList.isNullOrEmpty()) {
FlowRow( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 4.dp), .padding(vertical = 4.dp)
horizontalArrangement = Arrangement.spacedBy(4.dp), .horizontalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp),
) { verticalAlignment = Alignment.CenterVertically
kindList.forEachIndexed { index, label -> ) {
val colorPair = if (customTagColors.isNotEmpty()) { kindList.forEachIndexed { index, label ->
customTagColors[index % customTagColors.size] val colorPair = if (customTagColors.isNotEmpty()) {
} else { customTagColors[index % customTagColors.size]
null } 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
)
} }
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) { if (BookshelfConfig.bookshelfShowIntro && intro != null) {
val maxLines = if (BookshelfConfig.bookshelfIntroMaxLines == 0) Int.MAX_VALUE else BookshelfConfig.bookshelfIntroMaxLines val maxLines = if (BookshelfConfig.bookshelfIntroMaxLines == 0) Int.MAX_VALUE else BookshelfConfig.bookshelfIntroMaxLines
AppText( AppText(
@@ -690,29 +694,32 @@ fun BookItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 4.dp) .padding(vertical = 4.dp)
) )
} }
} }
} else null, } else null,
extra = { bottomContent = null,
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) { 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( AppText(
text = book.latestChapterTime.toTimeAgo(), text = book.latestChapterTitle ?: "",
style = LegadoTheme.typography.labelSmallEmphasized, style = LegadoTheme.typography.labelSmallEmphasized,
color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f), color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier.padding(end = 4.dp) maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
) )
} }
AppText( } else null,
text = book.latestChapterTitle ?: "",
style = LegadoTheme.typography.labelSmallEmphasized,
color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
},
titleSmallFont = titleSmallFont, titleSmallFont = titleSmallFont,
titleCenter = titleCenter, titleCenter = titleCenter,
titleMaxLines = titleMaxLines, titleMaxLines = titleMaxLines,
@@ -288,6 +288,13 @@ fun BookshelfConfigSheet(
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
CompactSwitchSettingItem(
title = "显示最新章节",
checked = BookshelfConfig.bookshelfShowLatestChapter,
color = LegadoTheme.colorScheme.surface,
onCheckedChange = { BookshelfConfig.bookshelfShowLatestChapter = it }
)
CompactSwitchSettingItem( CompactSwitchSettingItem(
title = "显示简介", title = "显示简介",
checked = BookshelfConfig.bookshelfShowIntro, checked = BookshelfConfig.bookshelfShowIntro,
@@ -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.card.TextCard
import io.legado.app.ui.widget.components.divider.PillHeaderDivider 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.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.icon.AppIcons
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
@@ -1061,7 +1062,7 @@ fun BookshelfScreen(
onSelectInvert = { viewModel.invertVisibleSelection() }, onSelectInvert = { viewModel.invertVisibleSelection() },
primaryAction = ActionItem( primaryAction = ActionItem(
text = stringResource(R.string.action_download), text = stringResource(R.string.action_download),
icon = { Icon(Icons.Default.Download, contentDescription = null) }, icon = Icons.Default.Download,
onClick = { onClick = {
if (selectedBookUrls.isNotEmpty()) { if (selectedBookUrls.isNotEmpty()) {
viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog) viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
@@ -1071,7 +1072,7 @@ fun BookshelfScreen(
secondaryActions = listOf( secondaryActions = listOf(
ActionItem( ActionItem(
text = stringResource(R.string.move_to_group), text = stringResource(R.string.move_to_group),
icon = { Icon(Icons.Default.Bookmarks, contentDescription = null) }, icon = Icons.Default.Bookmarks,
onClick = { onClick = {
if (selectedBookUrls.isNotEmpty()) { if (selectedBookUrls.isNotEmpty()) {
viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet) viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
@@ -710,7 +710,9 @@ class BookshelfViewModel(
fun refreshBooks(books: List<BookShelfItem>) { fun refreshBooks(books: List<BookShelfItem>) {
if (isRefreshingFlow.value) return if (isRefreshingFlow.value) return
isRefreshingFlow.value = true 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>) { fun startDraggingBooks(books: List<BookShelfItem>) {
@@ -761,7 +763,9 @@ class BookshelfViewModel(
} }
fun upToc(books: List<BookShelfItem>) { 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( private fun enqueueTocUpdate(
@@ -118,37 +118,12 @@ fun ExploreScreen(
} }
} }
val expandedHeader = remember(uiState.expandedId, uiState.listItems) { val stickyHeaderSource by remember(uiState.listItems, uiState.items) {
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) {
derivedStateOf { derivedStateOf {
val header = expandedHeader ?: return@derivedStateOf null val firstIndex = listState.firstVisibleItemIndex
val lastContentIndex = header.headerIndex + header.contentRowCount val item = uiState.listItems.getOrNull(firstIndex)
val firstVisible = listState.firstVisibleItemIndex if (item is ExploreListItem.KindRow) {
uiState.items.find { it.bookSourceUrl == item.sourceUrl }
if (firstVisible in (header.headerIndex + 1)..lastContentIndex) {
header.source
} else { } else {
null null
} }
@@ -292,8 +267,9 @@ fun ExploreScreen(
verticalPadding = 8.dp, verticalPadding = 8.dp,
onClick = { onClick = {
scope.launch { scope.launch {
val index = val index = uiState.listItems.indexOfFirst {
uiState.items.indexOfFirst { it.bookSourceUrl == item.bookSourceUrl } it is ExploreListItem.Header && it.source.bookSourceUrl == item.bookSourceUrl
}
if (index >= 0) listState.animateScrollToItem(index) 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -33,8 +33,7 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import io.legado.app.ui.widget.components.AppPullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -132,17 +131,15 @@ fun RssArticlesPage(
loadState.errorMessage?.takeIf { it.isNotBlank() }?.let(context::toastOnUi) loadState.errorMessage?.takeIf { it.isNotBlank() }?.let(context::toastOnUi)
} }
val refreshState = rememberPullToRefreshState()
val contentPadding = adaptiveContentPadding( val contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(), top = paddingValues.calculateTopPadding(),
bottom = 120.dp bottom = 120.dp
) )
PullToRefreshBox( AppPullToRefresh(
isRefreshing = loadState.isRefreshing, isRefreshing = loadState.isRefreshing,
onRefresh = { rssSource?.let(viewModel::loadArticles) }, onRefresh = { rssSource?.let(viewModel::loadArticles) },
state = refreshState, modifier = modifier.fillMaxSize(),
modifier = modifier.fillMaxSize()
) { ) {
when (layout) { when (layout) {
RssArticleLayout.List, RssArticleLayout.LargeCard -> { RssArticleLayout.List, RssArticleLayout.LargeCard -> {
@@ -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()
}
}
}
}
@@ -1,8 +1,12 @@
package io.legado.app.ui.widget.components package io.legado.app.ui.widget.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.foundation.layout.width
import androidx.compose.material.icons.Icons 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.MoreVert
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SelectAll 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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.PlainTooltip import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipAnchorPosition
@@ -24,9 +27,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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 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( data class SelectionActions(
val primaryAction: ActionItem, val primaryAction: ActionItem,
@@ -38,7 +52,7 @@ data class SelectionActions(
data class ActionItem( data class ActionItem(
val text: String, val text: String,
val icon: @Composable (() -> Unit)? = null, val icon: ImageVector = Icons.Default.ExpandMore,
val onClick: () -> Unit val onClick: () -> Unit
) )
@@ -52,66 +66,129 @@ fun SelectionBottomBar(
secondaryActions: List<ActionItem> secondaryActions: List<ActionItem>
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
HorizontalFloatingToolbar( if (isMiuix) {
modifier = modifier, MiuixFloatingToolbar(
expanded = true, modifier = modifier
leadingContent = { ) {
IconButton(onClick = onSelectAll) { Row(
Icon( verticalAlignment = Alignment.CenterVertically,
imageVector = Icons.Default.SelectAll, horizontalArrangement = Arrangement.spacedBy(8.dp),
contentDescription = "Select All" modifier = Modifier.padding(8.dp)
) ) {
} MiuixIconButton(onClick = onSelectAll) {
IconButton(onClick = onSelectInvert) { MiuixIcon(
Icon( imageVector = Icons.Default.SelectAll,
imageVector = Icons.Default.Refresh, contentDescription = "Select All"
contentDescription = "Invert Selection" )
) }
} MiuixIconButton(onClick = onSelectInvert) {
}, MiuixIcon(
trailingContent = { imageVector = Icons.Default.Refresh,
if (secondaryActions.isNotEmpty()) { contentDescription = "Invert Selection"
Box { )
IconButton(onClick = { showMenu = true }) { }
Icon(
imageVector = Icons.Default.MoreVert, MiuixIconButton(
contentDescription = "More actions" onClick = primaryAction.onClick,
) backgroundColor = MiuixTheme.colorScheme.primary,
} minWidth = 64.dp
DropdownMenu( ) {
expanded = showMenu, MiuixIcon(
onDismissRequest = { showMenu = false } imageVector = primaryAction.icon,
) { contentDescription = null,
secondaryActions.forEach { action -> tint = MiuixTheme.colorScheme.onPrimary
DropdownMenuItem( )
text = { AppText(action.text) }, }
leadingIcon = action.icon,
onClick = { if (secondaryActions.isNotEmpty()) {
action.onClick() Box {
showMenu = false 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 = { } else {
TooltipBox( HorizontalFloatingToolbar(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider( modifier = modifier,
TooltipAnchorPosition.Above expanded = true,
), leadingContent = {
tooltip = { PlainTooltip { AppText(primaryAction.text) } }, IconButton(onClick = onSelectAll) {
state = rememberTooltipState(), AppIcon(
) { imageVector = Icons.Default.SelectAll,
FilledIconButton( contentDescription = "Select All"
modifier = Modifier.width(64.dp), )
onClick = primaryAction.onClick, }
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)
}
} }
} }
} )
) }
} }
@@ -106,7 +106,7 @@ fun <T> RuleListScaffold(
onSelectInvert = onSelectInvert, onSelectInvert = onSelectInvert,
primaryAction = ActionItem( primaryAction = ActionItem(
text = stringResource(R.string.delete), text = stringResource(R.string.delete),
icon = { Icon(Icons.Default.Delete, null) }, icon = Icons.Default.Delete,
onClick = { showDeleteConfirmDialog = true } onClick = { showDeleteConfirmDialog = true }
), ),
secondaryActions = selectionSecondaryActions secondaryActions = selectionSecondaryActions