组件优化

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
@@ -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"
@@ -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,
@@ -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() -> {
@@ -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
@@ -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,
@@ -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
@@ -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为显示全部)
*/
@@ -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,
@@ -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,
@@ -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)
@@ -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(
@@ -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
@@ -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 -> {
@@ -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
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)
}
}
}
}
)
)
}
}
@@ -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