HapeLee 11 месяцев назад
Родитель
Сommit
443380f61a

+ 61 - 46
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()
-                        ) {
-                            Column(
-                                modifier = Modifier
-                                    .animateContentSize()
-                            ) {
-                                bookmarks.forEach { bookmark ->
+                stickyHeader(key = headerKey.toString()) {
+                    BookAuthorHeader(
+                        bookTitle = headerKey.bookName,
+                        bookAuthor = headerKey.bookAuthor,
+                        isCollapsed = isCollapsed,
+                        onToggle = { viewModel.toggleGroupCollapse(headerKey) }
+                    )
+                }
 
-                                    BookmarkItem(
-                                        bookmark = bookmark,
-                                        modifier = Modifier
-                                            .animateItem()
-                                            .fillMaxWidth(),
-                                        onClick = {
-                                            editingBookmark = bookmark
-                                            showBottomSheet = true
-                                        }
-                                    )
-                                }
+                item(key = "content_${headerKey}") {
+                    AnimatedVisibility(
+                        visible = !isCollapsed,
+                        enter = expandVertically() + fadeIn(),
+                        exit = shrinkVertically() + fadeOut()
+                    ) {
+                        Column(
+                            modifier = Modifier
+                                .animateContentSize()
+                        ) {
+                            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
             )

+ 19 - 25
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<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()
         }
     }
 

+ 20 - 17
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

+ 43 - 0
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()
+            }
+        }
+    }
+}