组件优化
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user