|
@@ -6,6 +6,7 @@ import android.os.Handler
|
|
|
import android.os.Looper
|
|
import android.os.Looper
|
|
|
import android.view.ViewGroup
|
|
import android.view.ViewGroup
|
|
|
import androidx.recyclerview.widget.DiffUtil
|
|
import androidx.recyclerview.widget.DiffUtil
|
|
|
|
|
+import androidx.recyclerview.widget.RecyclerView
|
|
|
import io.legato.kazusa.R
|
|
import io.legato.kazusa.R
|
|
|
import io.legato.kazusa.base.adapter.DiffRecyclerAdapter
|
|
import io.legato.kazusa.base.adapter.DiffRecyclerAdapter
|
|
|
import io.legato.kazusa.base.adapter.ItemViewHolder
|
|
import io.legato.kazusa.base.adapter.ItemViewHolder
|
|
@@ -16,7 +17,6 @@ import io.legato.kazusa.help.book.ContentProcessor
|
|
|
import io.legato.kazusa.help.config.AppConfig
|
|
import io.legato.kazusa.help.config.AppConfig
|
|
|
import io.legato.kazusa.lib.theme.ThemeUtils
|
|
import io.legato.kazusa.lib.theme.ThemeUtils
|
|
|
import io.legato.kazusa.utils.gone
|
|
import io.legato.kazusa.utils.gone
|
|
|
-import io.legato.kazusa.utils.longToastOnUi
|
|
|
|
|
import io.legato.kazusa.utils.themeColor
|
|
import io.legato.kazusa.utils.themeColor
|
|
|
import io.legato.kazusa.utils.visible
|
|
import io.legato.kazusa.utils.visible
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -27,28 +27,25 @@ import kotlinx.coroutines.launch
|
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.coroutines.withContext
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
|
|
|
|
|
|
-class ChapterListAdapter(context: Context, val callback: Callback) :
|
|
|
|
|
- DiffRecyclerAdapter<BookChapter, ItemChapterListBinding>(context) {
|
|
|
|
|
|
|
+class ChapterListAdapter(
|
|
|
|
|
+ context: Context,
|
|
|
|
|
+ val callback: Callback
|
|
|
|
|
+) : DiffRecyclerAdapter<BookChapter, ItemChapterListBinding>(context) {
|
|
|
|
|
|
|
|
val cacheFileNames = hashSetOf<String>()
|
|
val cacheFileNames = hashSetOf<String>()
|
|
|
private val displayTitleMap = ConcurrentHashMap<String, String>()
|
|
private val displayTitleMap = ConcurrentHashMap<String, String>()
|
|
|
|
|
+ private val selectedIndices = LinkedHashSet<Int>()
|
|
|
|
|
|
|
|
private val handler = Handler(Looper.getMainLooper())
|
|
private val handler = Handler(Looper.getMainLooper())
|
|
|
|
|
+ private var upDisplayTileJob: Job? = null
|
|
|
|
|
|
|
|
override val diffItemCallback: DiffUtil.ItemCallback<BookChapter>
|
|
override val diffItemCallback: DiffUtil.ItemCallback<BookChapter>
|
|
|
get() = object : DiffUtil.ItemCallback<BookChapter>() {
|
|
get() = object : DiffUtil.ItemCallback<BookChapter>() {
|
|
|
-
|
|
|
|
|
- override fun areItemsTheSame(
|
|
|
|
|
- oldItem: BookChapter,
|
|
|
|
|
- newItem: BookChapter
|
|
|
|
|
- ): Boolean {
|
|
|
|
|
|
|
+ override fun areItemsTheSame(oldItem: BookChapter, newItem: BookChapter): Boolean {
|
|
|
return oldItem.index == newItem.index
|
|
return oldItem.index == newItem.index
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- override fun areContentsTheSame(
|
|
|
|
|
- oldItem: BookChapter,
|
|
|
|
|
- newItem: BookChapter
|
|
|
|
|
- ): Boolean {
|
|
|
|
|
|
|
+ override fun areContentsTheSame(oldItem: BookChapter, newItem: BookChapter): Boolean {
|
|
|
return oldItem.bookUrl == newItem.bookUrl
|
|
return oldItem.bookUrl == newItem.bookUrl
|
|
|
&& oldItem.url == newItem.url
|
|
&& oldItem.url == newItem.url
|
|
|
&& oldItem.isVip == newItem.isVip
|
|
&& oldItem.isVip == newItem.isVip
|
|
@@ -58,11 +55,8 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|
|
&& oldItem.wordCount == newItem.wordCount
|
|
&& oldItem.wordCount == newItem.wordCount
|
|
|
&& oldItem.isVolume == newItem.isVolume
|
|
&& oldItem.isVolume == newItem.isVolume
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private var upDisplayTileJob: Job? = null
|
|
|
|
|
-
|
|
|
|
|
override fun onCurrentListChanged() {
|
|
override fun onCurrentListChanged() {
|
|
|
super.onCurrentListChanged()
|
|
super.onCurrentListChanged()
|
|
|
callback.onListChanged()
|
|
callback.onListChanged()
|
|
@@ -80,7 +74,6 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|
|
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
|
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
|
|
val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule()
|
|
val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule()
|
|
|
val items = getItems()
|
|
val items = getItems()
|
|
|
-
|
|
|
|
|
val indices = ((startIndex until items.size) + (startIndex downTo 0))
|
|
val indices = ((startIndex until items.size) + (startIndex downTo 0))
|
|
|
.filter { it in items.indices }
|
|
.filter { it in items.indices }
|
|
|
|
|
|
|
@@ -100,33 +93,132 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fun isInSelectionMode(): Boolean = selectedIndices.isNotEmpty()
|
|
|
|
|
+
|
|
|
|
|
+ fun toggleSelection(chapter: BookChapter, position: Int? = null) {
|
|
|
|
|
+ val pos = position ?: getItems().indexOfFirst { it.index == chapter.index }
|
|
|
|
|
+ if (pos == -1) return
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedIndices.contains(chapter.index)) {
|
|
|
|
|
+ selectedIndices.remove(chapter.index)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ selectedIndices.add(chapter.index)
|
|
|
|
|
+ }
|
|
|
|
|
+ notifyItemChanged(pos, true)
|
|
|
|
|
+ callback.onSelectionModeChanged(isInSelectionMode())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fun getSelectedChapters(): List<BookChapter> =
|
|
|
|
|
+ getItems().filter { selectedIndices.contains(it.index) }
|
|
|
|
|
+
|
|
|
|
|
+ fun clearSelection() {
|
|
|
|
|
+ if (selectedIndices.isNotEmpty()) {
|
|
|
|
|
+ val prevSelected = selectedIndices.toList()
|
|
|
|
|
+ selectedIndices.clear()
|
|
|
|
|
+ prevSelected.forEach { index ->
|
|
|
|
|
+ val pos = getItems().indexOfFirst { it.index == index }
|
|
|
|
|
+ if (pos != -1) notifyItemChanged(pos, true)
|
|
|
|
|
+ }
|
|
|
|
|
+ callback.onSelectionModeChanged(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- private fun getDisplayTitle(chapter: BookChapter): String {
|
|
|
|
|
- return displayTitleMap[chapter.title] ?: chapter.title
|
|
|
|
|
|
|
+ fun selectAll() {
|
|
|
|
|
+ val items = getItems()
|
|
|
|
|
+ val changedIndices = items.map { it.index }.filter { !selectedIndices.contains(it) }
|
|
|
|
|
+ selectedIndices.clear()
|
|
|
|
|
+ selectedIndices.addAll(items.map { it.index })
|
|
|
|
|
+ changedIndices.forEach { index ->
|
|
|
|
|
+ val pos = items.indexOfFirst { it.index == index }
|
|
|
|
|
+ if (pos != -1) notifyItemChanged(pos, true)
|
|
|
|
|
+ }
|
|
|
|
|
+ callback.onSelectionModeChanged(true)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
|
|
|
|
|
- return ItemChapterListBinding.inflate(inflater, parent, false)
|
|
|
|
|
|
|
+ fun invertSelection() {
|
|
|
|
|
+ val items = getItems()
|
|
|
|
|
+ val newSelected = items.map { it.index }.toSet() - selectedIndices
|
|
|
|
|
+ val changedIndices = (selectedIndices + newSelected) // 之前选中 + 新选中 = 改变的项
|
|
|
|
|
+ selectedIndices.clear()
|
|
|
|
|
+ selectedIndices.addAll(newSelected)
|
|
|
|
|
+ changedIndices.forEach { index ->
|
|
|
|
|
+ val pos = items.indexOfFirst { it.index == index }
|
|
|
|
|
+ if (pos != -1) notifyItemChanged(pos, true)
|
|
|
|
|
+ }
|
|
|
|
|
+ callback.onSelectionModeChanged(selectedIndices.isNotEmpty())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fun selectFrom() {
|
|
|
|
|
+ val items = getItems()
|
|
|
|
|
+ val startPos = selectedIndices.lastOrNull()?.let { lastIndex ->
|
|
|
|
|
+ items.indexOfFirst { it.index == lastIndex }
|
|
|
|
|
+ } ?: return
|
|
|
|
|
+
|
|
|
|
|
+ if (startPos !in items.indices) return
|
|
|
|
|
+
|
|
|
|
|
+ val changedIndices = mutableListOf<Int>()
|
|
|
|
|
+ for (i in startPos until items.size) {
|
|
|
|
|
+ val chapterIndex = items[i].index
|
|
|
|
|
+ if (!selectedIndices.contains(chapterIndex)) {
|
|
|
|
|
+ selectedIndices.add(chapterIndex)
|
|
|
|
|
+ changedIndices.add(i)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ changedIndices.forEach { notifyItemChanged(it, true) }
|
|
|
|
|
+ callback.onSelectionModeChanged(isInSelectionMode())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun getDisplayTitle(chapter: BookChapter): String =
|
|
|
|
|
+ displayTitleMap[chapter.title] ?: chapter.title
|
|
|
|
|
+
|
|
|
|
|
+ override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding =
|
|
|
|
|
+ ItemChapterListBinding.inflate(inflater, parent, false)
|
|
|
|
|
+
|
|
|
override fun convert(
|
|
override fun convert(
|
|
|
holder: ItemViewHolder,
|
|
holder: ItemViewHolder,
|
|
|
binding: ItemChapterListBinding,
|
|
binding: ItemChapterListBinding,
|
|
|
item: BookChapter,
|
|
item: BookChapter,
|
|
|
payloads: MutableList<Any>
|
|
payloads: MutableList<Any>
|
|
|
) {
|
|
) {
|
|
|
|
|
+ val isDur = callback.durChapterIndex() == item.index
|
|
|
|
|
+ val cached =
|
|
|
|
|
+ callback.isLocalBook || item.isVolume || cacheFileNames.contains(item.getFileName())
|
|
|
|
|
+
|
|
|
binding.run {
|
|
binding.run {
|
|
|
- val isDur = callback.durChapterIndex() == item.index
|
|
|
|
|
- val cached = callback.isLocalBook
|
|
|
|
|
- || item.isVolume
|
|
|
|
|
- || cacheFileNames.contains(item.getFileName())
|
|
|
|
|
if (payloads.isEmpty()) {
|
|
if (payloads.isEmpty()) {
|
|
|
|
|
+ tvChapterName.text = getDisplayTitle(item)
|
|
|
|
|
+ tvChapterItem.foreground =
|
|
|
|
|
+ ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground)
|
|
|
|
|
+
|
|
|
|
|
+ if (!item.tag.isNullOrEmpty() && !item.isVolume) {
|
|
|
|
|
+ tvTag.text = item.tag
|
|
|
|
|
+ tvTag.visible()
|
|
|
|
|
+ } else tvTag.gone()
|
|
|
|
|
+
|
|
|
|
|
+ if (AppConfig.tocCountWords && !item.wordCount.isNullOrEmpty() && !item.isVolume) {
|
|
|
|
|
+ tvWordCount.text = item.wordCount
|
|
|
|
|
+ tvWordCount.visible()
|
|
|
|
|
+ } else tvWordCount.gone()
|
|
|
|
|
+
|
|
|
|
|
+ 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.colorSurfaceBright))
|
|
|
|
|
+ } else {
|
|
|
if (item.isVolume) {
|
|
if (item.isVolume) {
|
|
|
ivVolume.visible()
|
|
ivVolume.visible()
|
|
|
- tvChapterName.text = getDisplayTitle(item)
|
|
|
|
|
tvChapterName.textSize = 12f
|
|
tvChapterName.textSize = 12f
|
|
|
tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorTertiary))
|
|
tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorTertiary))
|
|
|
|
|
+ tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface))
|
|
|
} else {
|
|
} else {
|
|
|
ivVolume.gone()
|
|
ivVolume.gone()
|
|
|
if (isDur) {
|
|
if (isDur) {
|
|
@@ -136,70 +228,39 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|
|
tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorOnSurface))
|
|
tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorOnSurface))
|
|
|
tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface))
|
|
tvChapterItem.setBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurface))
|
|
|
}
|
|
}
|
|
|
- tvChapterName.text = getDisplayTitle(item)
|
|
|
|
|
- tvChapterItem.foreground =
|
|
|
|
|
- ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- //卷名不显示
|
|
|
|
|
- if (!item.tag.isNullOrEmpty() && !item.isVolume) {
|
|
|
|
|
- //更新时间规则
|
|
|
|
|
- tvTag.text = item.tag
|
|
|
|
|
- tvTag.visible()
|
|
|
|
|
- } else {
|
|
|
|
|
- tvTag.gone()
|
|
|
|
|
- }
|
|
|
|
|
- if (AppConfig.tocCountWords && !item.wordCount.isNullOrEmpty() && !item.isVolume) {
|
|
|
|
|
- //章节字数
|
|
|
|
|
- tvWordCount.text = item.wordCount
|
|
|
|
|
- tvWordCount.visible()
|
|
|
|
|
- } else {
|
|
|
|
|
- tvWordCount.gone()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (item.isVip && !item.isPay) {
|
|
|
|
|
- ivLocked.visible()
|
|
|
|
|
- } else {
|
|
|
|
|
- ivLocked.gone()
|
|
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- upHasCache(binding, cached)
|
|
|
|
|
- } else {
|
|
|
|
|
- tvChapterName.text = getDisplayTitle(item)
|
|
|
|
|
- upHasCache(binding, cached)
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) {
|
|
override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) {
|
|
|
holder.itemView.setOnClickListener {
|
|
holder.itemView.setOnClickListener {
|
|
|
- getItem(holder.layoutPosition)?.let {
|
|
|
|
|
- callback.openChapter(it)
|
|
|
|
|
|
|
+ val pos = holder.bindingAdapterPosition
|
|
|
|
|
+ if (pos == RecyclerView.NO_POSITION) return@setOnClickListener
|
|
|
|
|
+ val item = getItem(pos) ?: return@setOnClickListener
|
|
|
|
|
+
|
|
|
|
|
+ if (isInSelectionMode()) {
|
|
|
|
|
+ toggleSelection(item, pos)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ callback.openChapter(item)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
holder.itemView.setOnLongClickListener {
|
|
holder.itemView.setOnLongClickListener {
|
|
|
- getItem(holder.layoutPosition)?.let { item ->
|
|
|
|
|
- context.longToastOnUi(getDisplayTitle(item))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ val pos = holder.bindingAdapterPosition
|
|
|
|
|
+ if (pos == RecyclerView.NO_POSITION) return@setOnLongClickListener true
|
|
|
|
|
+ val item = getItem(pos) ?: return@setOnLongClickListener true
|
|
|
|
|
+
|
|
|
|
|
+ toggleSelection(item, pos)
|
|
|
true
|
|
true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun upHasCache(binding: ItemChapterListBinding, cached: Boolean) =
|
|
|
|
|
- binding.apply {
|
|
|
|
|
- when {
|
|
|
|
|
- cached -> {
|
|
|
|
|
- ivChecked.setImageResource(R.drawable.ic_download_done)
|
|
|
|
|
- ivChecked.visible()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- else -> {
|
|
|
|
|
- ivChecked.setImageResource(R.drawable.ic_outline_cloud_24)
|
|
|
|
|
- ivChecked.visible()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ private fun upHasCache(binding: ItemChapterListBinding, cached: Boolean) = binding.apply {
|
|
|
|
|
+ ivChecked.setImageResource(if (cached) R.drawable.ic_download_done else R.drawable.ic_outline_cloud_24)
|
|
|
|
|
+ ivChecked.visible()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
interface Callback {
|
|
interface Callback {
|
|
|
val scope: CoroutineScope
|
|
val scope: CoroutineScope
|
|
@@ -208,6 +269,6 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|
|
fun openChapter(bookChapter: BookChapter)
|
|
fun openChapter(bookChapter: BookChapter)
|
|
|
fun durChapterIndex(): Int
|
|
fun durChapterIndex(): Int
|
|
|
fun onListChanged()
|
|
fun onListChanged()
|
|
|
|
|
+ fun onSelectionModeChanged(enabled: Boolean)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
}
|
|
}
|