[优化] 优化书签显示
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user