HapeLee 1 год назад
Родитель
Сommit
24275949dd

+ 2 - 2
app/src/main/java/io/legato/kazusa/base/adapter/DiffRecyclerAdapter.kt

@@ -108,7 +108,7 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
 
     fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty()
 
-    fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position)
+    open fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position)
 
     fun getItems(): List<ITEM> = asyncListDiffer.currentList
 
@@ -117,7 +117,7 @@ abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context
      */
     protected open fun getSpanSize(viewType: Int, position: Int) = 1
 
-    final override fun getItemCount() = getItems().size
+    override fun getItemCount() = getItems().size
 
     final override fun getItemViewType(position: Int): Int {
         return 0

+ 96 - 16
app/src/main/java/io/legato/kazusa/ui/book/toc/ChapterListAdapter.kt

@@ -38,6 +38,8 @@ class ChapterListAdapter(
 
     private val handler = Handler(Looper.getMainLooper())
     private var upDisplayTileJob: Job? = null
+    private val collapsedVolumes = mutableSetOf<Int>() // 保存折叠的卷index
+    private var visibleItems = mutableListOf<BookChapter>() // 当前显示
 
     override val diffItemCallback: DiffUtil.ItemCallback<BookChapter>
         get() = object : DiffUtil.ItemCallback<BookChapter>() {
@@ -59,6 +61,7 @@ class ChapterListAdapter(
 
     override fun onCurrentListChanged() {
         super.onCurrentListChanged()
+        rebuildVisibleItems()
         callback.onListChanged()
     }
 
@@ -169,9 +172,71 @@ class ChapterListAdapter(
         callback.onSelectionModeChanged(isInSelectionMode())
     }
 
+    private fun rebuildVisibleItems() {
+        val all = getItems()
+        visibleItems.clear()
+        var currentVolumeCollapsed = false
+        for (item in all) {
+            if (item.isVolume) {
+                visibleItems.add(item)
+                currentVolumeCollapsed = collapsedVolumes.contains(item.index)
+            } else if (!currentVolumeCollapsed) {
+                visibleItems.add(item)
+            }
+        }
+    }
+
+    fun toggleVolume(volume: BookChapter) {
+        if (!volume.isVolume) return
+
+        val all = getItems()
+        val startIndex = all.indexOf(volume)
+        if (startIndex == -1) return
+
+        val currentPos = visibleItems.indexOf(volume)
+        if (currentPos == -1) return
+
+        if (collapsedVolumes.contains(volume.index)) {
+            collapsedVolumes.remove(volume.index)
+
+            val toAdd = mutableListOf<BookChapter>()
+            for (i in startIndex + 1 until all.size) {
+                val next = all[i]
+                if (next.isVolume) break
+                toAdd.add(next)
+            }
+
+            val insertPos = currentPos + 1
+            visibleItems.addAll(insertPos, toAdd)
+
+            notifyItemRangeInserted(insertPos, toAdd.size)
+            notifyItemChanged(currentPos)
+
+        } else {
+            collapsedVolumes.add(volume.index)
+            val toRemove = mutableListOf<BookChapter>()
+            for (i in startIndex + 1 until all.size) {
+                val next = all[i]
+                if (next.isVolume) break
+                toRemove.add(next)
+            }
+            if (toRemove.isNotEmpty()) {
+                val removeStart = currentPos + 1
+                visibleItems.removeAll(toRemove)
+                notifyItemRangeRemoved(removeStart, toRemove.size)
+                notifyItemChanged(currentPos)
+            }
+        }
+    }
+
     private fun getDisplayTitle(chapter: BookChapter): String =
         displayTitleMap[chapter.url] ?: chapter.title
 
+    override fun getItemCount(): Int = visibleItems.size
+
+    override fun getItem(position: Int): BookChapter? =
+        visibleItems.getOrNull(position)
+
     override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding =
         ItemChapterListBinding.inflate(inflater, parent, false)
 
@@ -182,8 +247,9 @@ class ChapterListAdapter(
         payloads: MutableList<Any>
     ) {
         val isDur = callback.durChapterIndex() == item.index
-        val cached =
-            callback.isLocalBook || item.isVolume || cacheFileNames.contains(item.getFileName())
+        val cached = callback.isLocalBook || item.isVolume || cacheFileNames.contains(item.getFileName())
+        val isSelected = selectedIndices.contains(item.index)
+        val isCollapsed = collapsedVolumes.contains(item.index)
 
         binding.run {
             if (payloads.isEmpty()) {
@@ -204,32 +270,41 @@ class ChapterListAdapter(
                 if (item.isVip && !item.isPay) ivLocked.visible() else ivLocked.gone()
 
                 upHasCache(binding, cached)
-
             } else {
                 tvChapterName.text = getDisplayTitle(item)
                 upHasCache(binding, cached)
             }
 
-            if (selectedIndices.contains(item.index)) {
-                ivVolume.gone()
-                tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurfaceContainerHighest))
-            } else {
-                if (item.isVolume) {
+            when {
+                item.isVolume -> {
                     ivVolume.visible()
+                    if (isCollapsed) ivColl.visible()
+                    else ivColl.gone()
                     tvChapterName.textSize = 12f
                     tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorTertiary))
                     tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface))
-                } else {
+                }
+
+                isDur -> {
+                    ivColl.gone()
                     ivVolume.gone()
-                    if (isDur) {
-                        tvChapterName.setTextColor(context.themeColor(androidx.appcompat.R.attr.colorPrimary))
-                        tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
-                    } else {
-                        tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorOnSurface))
-                        tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface))
-                    }
+                    tvChapterName.setTextColor(context.themeColor(androidx.appcompat.R.attr.colorPrimary))
+                    tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
+                }
+
+                else -> {
+                    ivColl.gone()
+                    ivVolume.gone()
+                    tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorOnSurface))
+                    tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface))
                 }
             }
+
+            if (isSelected) {
+                tvChapterItem.setBackgroundColor(
+                    context.themeColor(com.google.android.material.R.attr.colorSurfaceContainerHighest)
+                )
+            }
         }
     }
 
@@ -240,6 +315,11 @@ class ChapterListAdapter(
             if (pos == RecyclerView.NO_POSITION) return@setOnClickListener
             val item = getItem(pos) ?: return@setOnClickListener
 
+            if (item.isVolume) {
+                toggleVolume(item)
+                return@setOnClickListener
+            }
+
             if (isInSelectionMode()) {
                 toggleSelection(item, pos)
             } else {

+ 19 - 2
app/src/main/res/layout/item_chapter_list.xml

@@ -38,12 +38,12 @@
     </LinearLayout>
 
     <LinearLayout
+        android:id="@+id/ll_chap"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-
         android:orientation="vertical"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/iv_checked"
+        app:layout_constraintEnd_toStartOf="@id/iv_Coll"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toEndOf="@id/ll_info">
 
@@ -85,6 +85,23 @@
             app:layout_constraintTop_toBottomOf="@id/tv_word_count" />
     </LinearLayout>
 
+    <com.google.android.material.card.MaterialCardView
+        android:id="@+id/iv_Coll"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:strokeWidth="0dp"
+        app:cardBackgroundColor="?attr/colorSurfaceContainerHighest"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/iv_checked">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:visibility="visible"
+            android:src="@drawable/ic_arrow_drop_down" />
+    </com.google.android.material.card.MaterialCardView>
+
     <ImageView
         android:id="@+id/iv_checked"
         android:layout_width="24dp"