[修复] 偶现的无法退出书源管理的问题

This commit is contained in:
HapeLee
2026-03-11 00:20:55 +08:00
parent ca067578a1
commit 50cd2f56fe
3 changed files with 70 additions and 29 deletions
@@ -5,6 +5,7 @@ import android.content.Context
import android.util.SparseArray import android.util.SparseArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.util.size
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -37,6 +38,8 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
private var diffJob: Coroutine<*>? = null private var diffJob: Coroutine<*>? = null
private var isResumed = false
var itemAnimation: ItemAnimation? = null var itemAnimation: ItemAnimation? = null
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
@@ -54,8 +57,8 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
@Synchronized @Synchronized
fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching { kotlin.runCatching {
val index = headerItems.size() val index = headerItems.size
headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header) headerItems.put(TYPE_HEADER_VIEW + headerItems.size, header)
notifyItemInserted(index) notifyItemInserted(index)
} }
} }
@@ -63,8 +66,8 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
@Synchronized @Synchronized
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
kotlin.runCatching { kotlin.runCatching {
val index = getActualItemCount() + footerItems.size() val index = getActualItemCount() + footerItems.size
footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer) footerItems.put(TYPE_FOOTER_VIEW + footerItems.size, footer)
notifyItemInserted(index) notifyItemInserted(index)
} }
} }
@@ -113,6 +116,10 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
skipDiff: Boolean = false skipDiff: Boolean = false
) { ) {
kotlin.runCatching { kotlin.runCatching {
if (!isResumed) { //全量标记更新
setItems(items)
return@runCatching
}
val oldItems = this.items.toList() val oldItems = this.items.toList()
val itemsSize = items?.size ?: 0 val itemsSize = items?.size ?: 0
val headerCount = getHeaderCount() val headerCount = getHeaderCount()
@@ -153,6 +160,10 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
return itemCallback.getChangePayload(oldItem, newItem) return itemCallback.getChangePayload(oldItem, newItem)
} }
} }
if (!isResumed) {
setItems(items)
return@runCatching
}
diffJob?.cancel() diffJob?.cancel()
diffJob = Coroutine.async { diffJob = Coroutine.async {
val diffResult = if (skipDiff) withTimeoutOrNullAsync(500L) { val diffResult = if (skipDiff) withTimeoutOrNullAsync(500L) {
@@ -162,7 +173,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
ensureActive() ensureActive()
handler.post { handler.post {
if (diffResult == null) { if (isResumed || diffResult == null) {
setItems(items) setItems(items)
return@post return@post
} }
@@ -172,6 +183,10 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
if (items != null) { if (items != null) {
this@RecyclerAdapter.items.addAll(items) this@RecyclerAdapter.items.addAll(items)
} }
if (!isResumed) {
return@post
}
ensureActive()
diffResult.dispatchUpdatesTo(this@RecyclerAdapter) diffResult.dispatchUpdatesTo(this@RecyclerAdapter)
onCurrentListChanged() onCurrentListChanged()
} }
@@ -329,10 +344,10 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
fun getActualItemCount() = items.size fun getActualItemCount() = items.size
fun getHeaderCount() = headerItems.size() fun getHeaderCount() = headerItems.size
fun getFooterCount() = footerItems.size() fun getFooterCount() = footerItems.size
fun getItem(position: Int): ITEM? = items.getOrNull(position) fun getItem(position: Int): ITEM? = items.getOrNull(position)
@@ -431,6 +446,15 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
} }
fun upResumed(isResumed: Boolean) {
if (!isResumed) {
diffJob?.cancel()
diffJob = null
handler.removeCallbacksAndMessages(null)
}
this.isResumed = isResumed
}
private fun isHeader(position: Int) = position < getHeaderCount() private fun isHeader(position: Int) = position < getHeaderCount()
private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount() private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount()
@@ -479,8 +503,4 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
private val handler by lazy { buildMainHandler() } private val handler by lazy { buildMainHandler() }
} }
} }
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.SubMenu import android.view.SubMenu
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -31,8 +30,7 @@ import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.DirectLinkUpload import io.legado.app.help.DirectLinkUpload
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
//import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.model.CheckSource import io.legado.app.model.CheckSource
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.ui.association.ImportBookSourceDialog import io.legado.app.ui.association.ImportBookSourceDialog
@@ -53,13 +51,12 @@ import io.legado.app.utils.cnCompare
import io.legado.app.utils.dpToPx import io.legado.app.utils.dpToPx
import io.legado.app.utils.flowWithLifecycleAndDatabaseChange import io.legado.app.utils.flowWithLifecycleAndDatabaseChange
import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst
import io.legado.app.utils.hideSoftInput
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.launch import io.legado.app.utils.launch
import io.legado.app.utils.observeEvent import io.legado.app.utils.observeEvent
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.setEdgeEffectColor
import io.legado.app.utils.share import io.legado.app.utils.share
import io.legado.app.utils.shouldHideSoftInput
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.showHelp import io.legado.app.utils.showHelp
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
@@ -272,7 +269,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
} }
private fun initRecyclerView() { private fun initRecyclerView() {
//binding.recyclerView.setEdgeEffectColor(primaryColor) binding.recyclerView.setEdgeEffectColor(primaryColor)
binding.recyclerView.addItemDecoration(VerticalDivider(this)) binding.recyclerView.addItemDecoration(VerticalDivider(this))
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.recyclerView.recycledViewPool.setMaxRecycledViews(0, 15) binding.recyclerView.recycledViewPool.setMaxRecycledViews(0, 15)
@@ -286,7 +283,6 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
} }
private fun initSearchView() { private fun initSearchView() {
//searchView.applyTint(primaryTextColor)
searchView.queryHint = getString(R.string.search_book_source) searchView.queryHint = getString(R.string.search_book_source)
searchView.setOnQueryTextListener(this) searchView.setOnQueryTextListener(this)
} }
@@ -394,6 +390,16 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
} }
} }
override fun onResume() {
super.onResume()
adapter.upResumed(true)
}
override fun onPause() {
adapter.upResumed(false)
super.onPause()
}
private fun initLiveDataGroup() { private fun initLiveDataGroup() {
lifecycleScope.launch { lifecycleScope.launch {
appDb.bookSourceDao.flowGroups() appDb.bookSourceDao.flowGroups()
@@ -472,11 +478,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
searchView.query?.toString(), searchView.query?.toString(),
sortAscending, sortAscending,
sort sort
) { file -> ) { file, name ->
exportDir.launch { exportDir.launch {
mode = HandleFileContract.EXPORT mode = HandleFileContract.EXPORT
fileData = HandleFileContract.FileData( fileData = HandleFileContract.FileData(
"bookSource.json", name,
file, file,
"application/json" "application/json"
) )
@@ -488,8 +494,8 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
searchView.query?.toString(), searchView.query?.toString(),
sortAscending, sortAscending,
sort sort
) { ) { file, name ->
share(it) share(file)
} }
R.id.menu_check_selected_interval -> adapter.checkSelectedInterval() R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
@@ -594,7 +600,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
?.toMutableList() ?: mutableListOf() ?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_on_line) { alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editLayout.hint = "URL" editView.hint = "url"
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)
editView.delCallBack = { editView.delCallBack = {
cacheUrls.remove(it) cacheUrls.remove(it)
@@ -12,6 +12,7 @@ import io.legado.app.help.source.SourceHelp
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.cnCompare import io.legado.app.utils.cnCompare
import io.legado.app.utils.normalizeFileName
import io.legado.app.utils.outputStream import io.legado.app.utils.outputStream
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.stackTraceStr import io.legado.app.utils.stackTraceStr
@@ -19,6 +20,8 @@ import io.legado.app.utils.toastOnUi
import io.legado.app.utils.writeToOutputStream import io.legado.app.utils.writeToOutputStream
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.util.Date
import java.util.Locale
/** /**
* 书源管理数据修改 * 书源管理数据修改
@@ -123,7 +126,11 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
private fun saveToFile(sources: List<BookSource>, success: (file: File) -> Unit) { private fun saveToFile(
sources: List<BookSource>,
name: String,
success: (file: File, name: String) -> Unit
) {
execute { execute {
val path = "${context.filesDir}/shareBookSource.json" val path = "${context.filesDir}/shareBookSource.json"
FileUtils.delete(path) FileUtils.delete(path)
@@ -133,7 +140,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
file file
}.onSuccess { }.onSuccess {
success.invoke(it) success.invoke(it, name)
}.onError { }.onError {
context.toastOnUi(it.stackTraceStr) context.toastOnUi(it.stackTraceStr)
} }
@@ -144,11 +151,12 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
searchKey: String?, searchKey: String?,
sortAscending: Boolean, sortAscending: Boolean,
sort: BookSourceSort, sort: BookSourceSort,
success: (file: File) -> Unit success: (file: File, name: String) -> Unit
) { ) {
execute { execute {
val selection = adapter.selection val selection = adapter.selection
val selectedRate = selection.size.toFloat() / adapter.itemCount.toFloat() val selectionSize = selection.size
val selectedRate = selectionSize.toFloat() / adapter.itemCount.toFloat()
val sources = if (selectedRate == 1f) { val sources = if (selectedRate == 1f) {
getBookSources(searchKey, sortAscending, sort) getBookSources(searchKey, sortAscending, sort)
} else if (selectedRate < 0.3) { } else if (selectedRate < 0.3) {
@@ -160,7 +168,14 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
keys.contains(it.bookSourceUrl) keys.contains(it.bookSourceUrl)
} }
} }
saveToFile(sources, success) val name = if (selectionSize == 1) {
"bookSource_${selection.first().bookSourceName.normalizeFileName()}.json"
} else {
val timestamp =
java.text.SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault()).format(Date())
"bookSource_$timestamp.json"
}
saveToFile(sources, name, success)
} }
} }