diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt index 9a3d4d5c7..cfb9e6dab 100644 --- a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt @@ -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 ) diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt index ae1bb4635..4a4e41eed 100644 --- a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt @@ -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>(emptySet()) val collapsedGroups = _collapsedGroups.asStateFlow() - private val refreshTrigger = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) // FlowPreview 用于 debounce - val bookmarksState: StateFlow>> = combine( - refreshTrigger.onStart { emit(Unit) }, - _searchQuery - ) { _, query -> query } + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + val bookmarksState: StateFlow>> = _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) { + fun toggleAllCollapse(currentKeys: Set) { + 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() } } diff --git a/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt b/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt index a100ef01b..b905cdbc5 100644 --- a/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SectionHeader.kt b/app/src/main/java/io/legado/app/ui/widget/components/SectionHeader.kt new file mode 100644 index 000000000..3b7bb33d1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/SectionHeader.kt @@ -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() + } + } + } +} \ No newline at end of file