[优化] 优化书签显示

This commit is contained in:
HapeLee
2025-12-06 15:33:45 +08:00
parent 7a1ade76e2
commit 443380f61a
4 changed files with 143 additions and 88 deletions
@@ -34,7 +34,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.Bookmark
import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.SearchBarSection
import org.koin.androidx.compose.koinViewModel
@@ -57,7 +56,7 @@ fun AllBookmarkScreen(
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val allKeys = bookmarksGrouped.keys
val isAllCollapsed = allKeys.isNotEmpty() && collapsedGroups.containsAll(allKeys)
val isAllCollapsed = allKeys.isNotEmpty() && allKeys.all { collapsedGroups.contains(it.toString()) }
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
@@ -74,9 +73,8 @@ fun AllBookmarkScreen(
Column {
MediumTopAppBar(
title = {
val text = if (showSearch) "搜索" else "所有书签"
AnimatedTextLine(
text = text,
Text(
text = "所有书签",
style = MaterialTheme.typography.titleLarge
)
},
@@ -149,39 +147,40 @@ fun AllBookmarkScreen(
.fillMaxSize()
.padding(paddingValues)
) {
bookmarksGrouped.forEach { (headerTitle, bookmarks) ->
bookmarksGrouped.forEach { (headerKey, bookmarks) ->
val isCollapsed = collapsedGroups.contains(headerTitle)
val isCollapsed = collapsedGroups.contains(headerKey.toString())
item(key = headerTitle) {
Column {
BookmarkHeader(
text = headerTitle,
isCollapsed = isCollapsed,
onToggle = { viewModel.toggleGroupCollapse(headerTitle) }
)
AnimatedVisibility(
visible = !isCollapsed,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
stickyHeader(key = headerKey.toString()) {
BookAuthorHeader(
bookTitle = headerKey.bookName,
bookAuthor = headerKey.bookAuthor,
isCollapsed = isCollapsed,
onToggle = { viewModel.toggleGroupCollapse(headerKey) }
)
}
item(key = "content_${headerKey}") {
AnimatedVisibility(
visible = !isCollapsed,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier
.animateContentSize()
) {
Column(
modifier = Modifier
.animateContentSize()
) {
bookmarks.forEach { bookmark ->
BookmarkItem(
bookmark = bookmark,
modifier = Modifier
.animateItem()
.fillMaxWidth(),
onClick = {
editingBookmark = bookmark
showBottomSheet = true
}
)
}
bookmarks.forEach { bookmark ->
BookmarkItem(
bookmark = bookmark,
modifier = Modifier
.animateItem()
.fillMaxWidth(),
onClick = {
editingBookmark = bookmark
showBottomSheet = true
}
)
}
}
}
@@ -210,8 +209,9 @@ fun AllBookmarkScreen(
}
@Composable
fun BookmarkHeader(
text: String,
fun BookAuthorHeader(
bookTitle: String,
bookAuthor: String,
isCollapsed: Boolean,
onToggle: () -> Unit
) {
@@ -226,15 +226,30 @@ fun BookmarkHeader(
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 标题
Text(
text = text,
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
),
Column(
modifier = Modifier.weight(1f)
)
) {
Text(
text = bookTitle,
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = bookAuthor,
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
val rotation by animateFloatAsState(
targetValue = if (isCollapsed) 0f else 180f,
@@ -243,7 +258,7 @@ fun BookmarkHeader(
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
contentDescription = if (isCollapsed) "展开书签" else "折叠书签",
modifier = Modifier.rotate(rotation),
tint = MaterialTheme.colorScheme.primary
)
@@ -35,6 +35,13 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
data class BookmarkGroupHeader(
val bookName: String,
val bookAuthor: String
) {
override fun toString(): String = "$bookName|${bookAuthor}"
}
class AllBookmarkViewModel(
application: Application,
private val bookmarkDao: BookmarkDao
@@ -46,16 +53,9 @@ class AllBookmarkViewModel(
private val _collapsedGroups = MutableStateFlow<Set<String>>(emptySet())
val collapsedGroups = _collapsedGroups.asStateFlow()
private val refreshTrigger = MutableSharedFlow<Unit>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) // FlowPreview 用于 debounce
val bookmarksState: StateFlow<Map<String, List<Bookmark>>> = combine(
refreshTrigger.onStart { emit(Unit) },
_searchQuery
) { _, query -> query }
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
val bookmarksState: StateFlow<Map<BookmarkGroupHeader, List<Bookmark>>> = _searchQuery
.debounce(300L)
.flatMapLatest { query ->
if (query.isBlank()) {
@@ -65,7 +65,7 @@ class AllBookmarkViewModel(
}
}
.map { list ->
list.groupBy { "${it.bookName}(${it.bookAuthor})" }
list.groupBy { BookmarkGroupHeader(it.bookName, it.bookAuthor) }
}
.catch { e -> e.printStackTrace() }
.flowOn(Dispatchers.IO)
@@ -75,27 +75,23 @@ class AllBookmarkViewModel(
emptyMap()
)
private fun refreshBookmarks() {
viewModelScope.launch {
refreshTrigger.emit(Unit)
}
}
fun toggleGroupCollapse(groupKey: String) {
fun toggleGroupCollapse(groupKey: BookmarkGroupHeader) {
val stringKey = groupKey.toString()
val current = _collapsedGroups.value
if (current.contains(groupKey)) {
_collapsedGroups.value = current - groupKey
if (current.contains(stringKey)) {
_collapsedGroups.value = current - stringKey
} else {
_collapsedGroups.value = current + groupKey // 折叠
_collapsedGroups.value = current + stringKey
}
}
fun toggleAllCollapse(currentKeys: Set<String>) {
fun toggleAllCollapse(currentKeys: Set<BookmarkGroupHeader>) {
val stringKeys = currentKeys.map { it.toString() }.toSet()
val currentCollapsed = _collapsedGroups.value
if (currentCollapsed.containsAll(currentKeys) && currentKeys.isNotEmpty()) {
if (currentCollapsed.containsAll(stringKeys) && currentKeys.isNotEmpty()) {
_collapsedGroups.value = emptySet()
} else {
_collapsedGroups.value = currentKeys
_collapsedGroups.value = stringKeys
}
}
@@ -108,7 +104,6 @@ class AllBookmarkViewModel(
withContext(Dispatchers.IO) {
bookmarkDao.insert(bookmark)
}
refreshBookmarks()
}
}
@@ -117,7 +112,6 @@ class AllBookmarkViewModel(
withContext(Dispatchers.IO) {
bookmarkDao.delete(bookmark)
}
refreshBookmarks()
}
}
@@ -63,6 +63,7 @@ import io.legado.app.ui.widget.components.Calendar
import io.legado.app.ui.widget.components.Cover
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SectionHeader
import io.legado.app.utils.StringUtils.formatFriendlyDate
import kotlinx.coroutines.delay
import java.time.LocalDate
@@ -494,26 +495,28 @@ fun DateHeader(
dailyTotalTime: Long? = null
) {
val dateText = formatFriendlyDate(date)
Surface(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Text(
text = dateText,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.secondary
)
dailyTotalTime?.let { total ->
SectionHeader(
titleContent = {
Column {
Text(
text = "已读 ${formatDuring(total)}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
text = dateText,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.secondary
)
dailyTotalTime?.let { total ->
Text(
text = "已读 ${formatDuring(total)}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
},
detailContent = null,
horizontalArrangement = Arrangement.Start
)
}
@Composable
@@ -0,0 +1,43 @@
package io.legado.app.ui.widget.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SectionHeader(
// 左侧主标题内容
titleContent: @Composable () -> Unit,
// 右侧或副标题内容
detailContent: @Composable (() -> Unit)? = null,
// 主要内容的排列方式
verticalArrangement: Arrangement.Vertical = Arrangement.Center,
// 定义水平对齐方式
horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween,
// 根容器的 Padding
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
Surface(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = horizontalArrangement
) {
titleContent()
detailContent?.let {
it()
}
}
}
}