diff --git a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt index 529606bd1..ac9f68abb 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt +++ b/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() + 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() + 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 { + 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 diff --git a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt index b913ef994..e34fff261 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt +++ b/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(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 { + 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) { diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt index fdfd0ab3a..3befe0974 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt +++ b/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(), 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 { diff --git a/app/src/main/res/layout/activity_chapter_list.xml b/app/src/main/res/layout/activity_chapter_list.xml index 919dc79d4..615f49c38 100644 --- a/app/src/main/res/layout/activity_chapter_list.xml +++ b/app/src/main/res/layout/activity_chapter_list.xml @@ -25,7 +25,8 @@ + android:layout_marginStart="16dp" + android:layout_marginEnd="12dp"> + style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"/> diff --git a/app/src/main/res/menu/menu_chapter_actions.xml b/app/src/main/res/menu/menu_chapter_actions.xml new file mode 100644 index 000000000..bda97031c --- /dev/null +++ b/app/src/main/res/menu/menu_chapter_actions.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 93918c31a..53970edb0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1326,4 +1326,8 @@ 名称升序 名称降序 文字下划虚线 + 跳转到卷 + 取消选择 + 折叠所有卷 + 展开所有卷 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4be9477b6..2f8d39a68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1327,4 +1327,8 @@ 名称升序 名称降序 文字下划虚线 + 跳转到卷 + 取消选择 + 折叠所有卷 + 展开所有卷