فهرست منبع

目录卷快速跳转与展开 #230、#212、#195

HapeLee 1 سال پیش
والد
کامیت
f2d0e149ce

+ 64 - 0
app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt

@@ -229,6 +229,70 @@ class ChapterListAdapter(
         }
     }
 
+    fun expandAllVolumes() {
+        val all = getItems()
+        var offset = 0
+
+        collapsedVolumes.forEach { volumeIndex ->
+            val volume = all.firstOrNull { it.index == volumeIndex } ?: return@forEach
+            val startIndex = all.indexOf(volume)
+            val currentPos = visibleItems.indexOf(volume)
+            if (currentPos == -1) return@forEach
+
+            val toAdd = mutableListOf<BookChapter>()
+            for (i in startIndex + 1 until all.size) {
+                val next = all[i]
+                if (next.isVolume) break
+                toAdd.add(next)
+            }
+
+            if (toAdd.isNotEmpty()) {
+                visibleItems.addAll(currentPos + 1, toAdd)
+                notifyItemRangeInserted(currentPos + 1, toAdd.size)
+                notifyItemChanged(currentPos)
+                offset += toAdd.size
+            }
+        }
+
+        collapsedVolumes.clear()
+    }
+
+    fun collapseAllVolumes() {
+        val all = getItems()
+
+        all.filter { it.isVolume }.forEach { volume ->
+            val startIndex = visibleItems.indexOf(volume)
+            if (startIndex == -1) return@forEach
+
+            val toRemove = mutableListOf<BookChapter>()
+            for (i in startIndex + 1 until visibleItems.size) {
+                val next = visibleItems[i]
+                if (next.isVolume) break
+                toRemove.add(next)
+            }
+
+            if (toRemove.isNotEmpty()) {
+                visibleItems.removeAll(toRemove)
+                notifyItemRangeRemoved(startIndex + 1, toRemove.size)
+                notifyItemChanged(startIndex)
+            }
+
+            collapsedVolumes.add(volume.index)
+        }
+    }
+
+    fun getAllVolumes(): List<String> {
+        return getItems().filter { it.isVolume }.map { it.title }
+    }
+
+    fun getVolumeStartPosition(volumeName: String): Int {
+        return getItems().indexOfFirst { it.isVolume && it.title == volumeName }
+    }
+
+    fun areAllVolumesExpanded(): Boolean {
+        return getItems().filter { it.isVolume }.all { !collapsedVolumes.contains(it.index) }
+    }
+
     private fun getDisplayTitle(chapter: BookChapter): String =
         displayTitleMap[chapter.url] ?: chapter.title
 

+ 38 - 1
app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt

@@ -18,6 +18,7 @@ import androidx.fragment.app.activityViewModels
 import androidx.interpolator.view.animation.FastOutSlowInInterpolator
 import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.transition.platform.MaterialFadeThrough
 import io.legado.app.R
@@ -259,11 +260,47 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
                 }
             }
 
-            // 可选:清空选择状态并提示
             adapter.clearSelection()
         }
     }
 
+    fun areAllVolumesExpanded(): Boolean = adapter.areAllVolumesExpanded()
+
+    fun toggleAllVolumes() {
+        val allExpanded = adapter.areAllVolumesExpanded()
+        if (allExpanded) {
+            adapter.collapseAllVolumes()
+            toastOnUi("已收起所有卷")
+        } else {
+            adapter.expandAllVolumes()
+            toastOnUi("已展开所有卷")
+        }
+    }
+
+    fun isInSelectionMode(): Boolean = adapter.isInSelectionMode()
+
+    fun toggleChapterSelection() {
+        if (adapter.isInSelectionMode()) {
+            adapter.clearSelection()
+            toastOnUi("已取消选择")
+        } else {
+            adapter.selectAll()
+            toastOnUi("已全选 ${adapter.itemCount} 个章节")
+        }
+    }
+
+    fun getAllVolumes(): List<String> {
+        return adapter.getAllVolumes()
+    }
+
+    fun scrollToVolume(volumeName: String) {
+        val position = adapter.getVolumeStartPosition(volumeName)
+        if (position >= 0) {
+            (binding.recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, 0)
+            toastOnUi("已跳转到 $volumeName")
+        }
+    }
+
     fun scrollToChapter(chapterIndex: Int) {
         val pos = adapter.getItems().indexOfFirst { it.index == chapterIndex }
         if (pos != -1) {

+ 93 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt

@@ -4,13 +4,24 @@ package io.legado.app.ui.book.toc
 
 import android.annotation.SuppressLint
 import android.content.Intent
+import android.graphics.Color
+import android.graphics.Typeface
 import android.os.Bundle
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.AbsoluteSizeSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StyleSpan
 import android.view.Menu
 import android.view.MenuItem
 import android.view.MotionEvent
+import android.view.View
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.viewModels
+import androidx.appcompat.widget.PopupMenu
 import androidx.appcompat.widget.SearchView
+import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentPagerAdapter
 import com.google.android.material.tabs.TabLayout
@@ -78,6 +89,88 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
         intent.getStringExtra("bookUrl")?.let {
             viewModel.initBook(it)
         }
+        setupTabListener()
+        setupMoreMenuButton()
+    }
+
+    private fun setupTabListener() {
+        tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+            override fun onTabSelected(tab: TabLayout.Tab) {
+                updateButtonVisibility(tab.position)
+            }
+            override fun onTabUnselected(tab: TabLayout.Tab) {}
+            override fun onTabReselected(tab: TabLayout.Tab) {
+                updateButtonVisibility(tab.position)
+            }
+        })
+    }
+
+    private fun updateButtonVisibility(tabPosition: Int) {
+        binding.btnChapterMenu.isEnabled = tabPosition == 0
+    }
+
+    private fun setupMoreMenuButton() {
+        val tag = "android:switcher:${binding.viewPager.id}:0"
+        binding.viewPager.postDelayed({
+            val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment
+            val volumes = chapterFragment?.getAllVolumes().orEmpty()
+            binding.btnChapterMenu.isVisible = volumes.isNotEmpty()
+        }, 200)
+        binding.btnChapterMenu.setOnClickListener { view ->
+            showChapterActionsMenu(view)
+        }
+    }
+
+    private fun showChapterActionsMenu(anchor: View) {
+        val popup = PopupMenu(this, anchor)
+        popup.menuInflater.inflate(R.menu.menu_chapter_actions, popup.menu)
+        val tag = "android:switcher:${binding.viewPager.id}:0"
+        val chapterFragment = supportFragmentManager.findFragmentByTag(tag) as? ChapterListFragment
+        val volumes = chapterFragment?.getAllVolumes().orEmpty()
+        volumes.forEachIndexed { index, volumeName ->
+            popup.menu.add(R.id.action_jump_to_volume_section, index, Menu.NONE, volumeName)
+        }
+        val titleItem = popup.menu.findItem(R.id.action_jump_to_volume_section)
+        val s = SpannableString(titleItem.title)
+        s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
+        s.setSpan(ForegroundColorSpan(Color.GRAY), 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
+        s.setSpan(AbsoluteSizeSpan(12, true), 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
+        titleItem.title = s
+        val toggleSelectionItem = popup.menu.findItem(R.id.action_toggle_selection)
+        toggleSelectionItem.title = if (chapterFragment?.isInSelectionMode() == true) {
+            getString(R.string.cancel_select)
+        } else {
+            getString(R.string.select_all)
+        }
+
+        val toggleVolumesItem = popup.menu.findItem(R.id.action_toggle_volumes)
+        toggleVolumesItem.title = if (chapterFragment?.areAllVolumesExpanded() == true) {
+            getString(R.string.coll_volume)
+        } else {
+            getString(R.string.expand_volume)
+        }
+        popup.setOnMenuItemClickListener { item ->
+            when (item.itemId) {
+                R.id.action_toggle_volumes -> {
+                    chapterFragment?.toggleAllVolumes()
+                    true
+                }
+                R.id.action_toggle_selection -> {
+                    chapterFragment?.toggleChapterSelection()
+                    true
+                }
+                else -> {
+                    if (item.groupId == R.id.action_jump_to_volume_section && item.itemId in volumes.indices) {
+                        chapterFragment?.scrollToVolume(volumes[item.itemId])
+                        true
+                    } else false
+                }
+            }
+        }
+        popup.setOnDismissListener {
+            binding.btnChapterMenu.isChecked = false
+        }
+        popup.show()
     }
 
     override fun dispatchTouchEvent(ev: MotionEvent): Boolean {

+ 10 - 4
app/src/main/res/layout/activity_chapter_list.xml

@@ -25,7 +25,8 @@
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginHorizontal="16dp">
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="12dp">
 
             <com.google.android.material.tabs.TabLayout
                 android:id="@+id/tab_layout"
@@ -37,12 +38,17 @@
                 app:tabMode="fixed" />
 
             <com.google.android.material.button.MaterialButton
-                android:layout_width="32dp"
-                android:layout_height="32dp"
+                android:id="@+id/btn_chapterMenu"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:layout_marginStart="4dp"
+                android:visibility="gone"
+                app:iconSize="16dp"
                 android:clickable="true"
                 android:checkable="true"
+                android:layout_gravity="center_vertical"
                 app:icon="@drawable/ic_toc"
-                style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"/>
+                style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"/>
 
         </LinearLayout>
 

+ 13 - 0
app/src/main/res/menu/menu_chapter_actions.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/action_toggle_volumes"
+        android:title="" />
+    <item
+        android:id="@+id/action_toggle_selection"
+        android:title="" />
+    <item
+        android:id="@+id/action_jump_to_volume_section"
+        android:title="@string/jump_to_volume"
+        android:enabled="false" />
+</menu>

+ 4 - 0
app/src/main/res/values-zh/strings.xml

@@ -1326,4 +1326,8 @@
     <string name="sort_name_asc">名称升序</string>
     <string name="sort_name_desc">名称降序</string>
     <string name="text_dottedline">文字下划虚线</string>
+    <string name="jump_to_volume">跳转到卷</string>
+    <string name="cancel_select">取消选择</string>
+    <string name="coll_volume">折叠所有卷</string>
+    <string name="expand_volume">展开所有卷</string>
 </resources>

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -1327,4 +1327,8 @@
     <string name="sort_name_asc">名称升序</string>
     <string name="sort_name_desc">名称降序</string>
     <string name="text_dottedline">文字下划虚线</string>
+    <string name="jump_to_volume">跳转到卷</string>
+    <string name="cancel_select">取消选择</string>
+    <string name="coll_volume">折叠所有卷</string>
+    <string name="expand_volume">展开所有卷</string>
 </resources>