This commit is contained in:
Horis
2023-03-23 16:26:33 +08:00
parent 61e146a46f
commit 63e1334de9
14 changed files with 120 additions and 53 deletions
@@ -108,6 +108,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
@Synchronized @Synchronized
fun setItems(items: List<ITEM>?, itemCallback: DiffUtil.ItemCallback<ITEM>) { fun setItems(items: List<ITEM>?, itemCallback: DiffUtil.ItemCallback<ITEM>) {
kotlin.runCatching { kotlin.runCatching {
val oldItems = this.items.toList()
val callback = object : DiffUtil.Callback() { val callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int { override fun getOldListSize(): Int {
return itemCount return itemCount
@@ -118,7 +119,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = getItem(oldItemPosition - getHeaderCount()) val oldItem = oldItems.getOrNull(oldItemPosition - getHeaderCount())
?: return true ?: return true
val newItem = items?.getOrNull(newItemPosition - getHeaderCount()) val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
?: return true ?: return true
@@ -129,7 +130,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
oldItemPosition: Int, oldItemPosition: Int,
newItemPosition: Int newItemPosition: Int
): Boolean { ): Boolean {
val oldItem = getItem(oldItemPosition - getHeaderCount()) val oldItem = oldItems.getOrNull(oldItemPosition - getHeaderCount())
?: return true ?: return true
val newItem = items?.getOrNull(newItemPosition - getHeaderCount()) val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
?: return true ?: return true
@@ -137,7 +138,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = getItem(oldItemPosition - getHeaderCount()) val oldItem = oldItems.getOrNull(oldItemPosition - getHeaderCount())
?: return null ?: return null
val newItem = items?.getOrNull(newItemPosition - getHeaderCount()) val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
?: return null ?: return null
@@ -44,7 +44,7 @@ object BookType {
@Target(AnnotationTarget.VALUE_PARAMETER) @Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@IntDef(text, updateError, audio, image, webFile, local) @IntDef(text, updateError, audio, image, webFile, local, archive)
annotation class Type annotation class Type
@@ -19,6 +19,9 @@ interface TxtTocRuleDao {
@get:Query("select * from txtTocRules where enable != 1 order by serialNumber") @get:Query("select * from txtTocRules where enable != 1 order by serialNumber")
val disabled: List<TxtTocRule> val disabled: List<TxtTocRule>
@get:Query("select count(*) from txtTocRules")
val count: Int
@Query("select * from txtTocRules where id = :id") @Query("select * from txtTocRules where id = :id")
fun get(id: Long): TxtTocRule? fun get(id: Long): TxtTocRule?
@@ -5,6 +5,7 @@ import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.CacheManager import io.legado.app.help.CacheManager
import io.legado.app.help.IntentData
import io.legado.app.service.CheckSourceService import io.legado.app.service.CheckSourceService
import io.legado.app.utils.startService import io.legado.app.utils.startService
import splitties.init.appCtx import splitties.init.appCtx
@@ -26,9 +27,9 @@ object CheckSource {
sources.map { sources.map {
selectedIds.add(it.bookSourceUrl) selectedIds.add(it.bookSourceUrl)
} }
IntentData.put("checkSourceSelectedIds", selectedIds)
context.startService<CheckSourceService> { context.startService<CheckSourceService> {
action = IntentAction.start action = IntentAction.start
putExtra("selectIds", selectedIds)
} }
} }
@@ -212,11 +212,8 @@ object LocalBook {
val files = ArchiveUtils.deCompress(archiveFileDoc, filter = filter) val files = ArchiveUtils.deCompress(archiveFileDoc, filter = filter)
if (files.isEmpty()) throw NoStackTraceException(appCtx.getString(R.string.unsupport_archivefile_entry)) if (files.isEmpty()) throw NoStackTraceException(appCtx.getString(R.string.unsupport_archivefile_entry))
return files.map { return files.map {
saveBookFile( saveBookFile(FileInputStream(it), saveFileName ?: it.name).let { uri ->
FileInputStream(it), importFile(uri).apply {
saveFileName ?: it.name
).let {
importFile(it).apply {
//附加压缩包名称 以便解压文件被删后再解压 //附加压缩包名称 以便解压文件被删后再解压
origin = "${BookType.localTag}::${archiveFileDoc.name}" origin = "${BookType.localTag}::${archiveFileDoc.name}"
addType(BookType.archive) addType(BookType.archive)
@@ -255,8 +252,8 @@ object LocalBook {
importFile(uri) importFile(uri)
} }
}.onFailure { }.onFailure {
AppLog.put("ImportFile Error:\nFile ${fileDoc.toString()}\n${it.localizedMessage}", it) AppLog.put("ImportFile Error:\nFile $fileDoc\n${it.localizedMessage}", it)
errorCount = errorCount + 1 errorCount += 1
} }
} }
if (errorCount == uris.size) throw NoStackTraceException("ImportFiles Error:\nAll input files occur error") if (errorCount == uris.size) throw NoStackTraceException("ImportFiles Error:\nAll input files occur error")
@@ -409,8 +406,8 @@ object LocalBook {
if (localBook.isArchive) { if (localBook.isArchive) {
// 压缩包 // 压缩包
val archiveUri = saveBookFile(it, localBook.archiveName) val archiveUri = saveBookFile(it, localBook.archiveName)
val newBook = importArchiveFile(archiveUri, localBook.originName) { val newBook = importArchiveFile(archiveUri, localBook.originName) { name ->
it.contains(localBook.originName) name.contains(localBook.originName)
}.first() }.first()
localBook.origin = newBook.origin localBook.origin = newBook.origin
localBook.bookUrl = newBook.bookUrl localBook.bookUrl = newBook.bookUrl
@@ -407,7 +407,7 @@ class TextFile(private val book: Book) {
*/ */
private fun getTocRules(): List<TxtTocRule> { private fun getTocRules(): List<TxtTocRule> {
var rules = appDb.txtTocRuleDao.enabled var rules = appDb.txtTocRuleDao.enabled
if (rules.isEmpty()) { if (appDb.txtTocRuleDao.count == 0) {
rules = DefaultData.txtTocRules.apply { rules = DefaultData.txtTocRules.apply {
appDb.txtTocRuleDao.insert(*this.toTypedArray()) appDb.txtTocRuleDao.insert(*this.toTypedArray())
}.filter { }.filter {
@@ -78,6 +78,7 @@ abstract class BaseReadAloudService : BaseService(),
internal var pageIndex = 0 internal var pageIndex = 0
private var needResumeOnAudioFocusGain = false private var needResumeOnAudioFocusGain = false
private var dsJob: Job? = null private var dsJob: Job? = null
var pageChanged = false
private val broadcastReceiver = object : BroadcastReceiver() { private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -157,7 +158,7 @@ abstract class BaseReadAloudService : BaseService(),
contentList.add(text) contentList.add(text)
} }
} }
if (play) play() if (play) play() else pageChanged = true
} }
} }
@@ -15,6 +15,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.exception.ContentEmptyException import io.legado.app.exception.ContentEmptyException
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.exception.TocEmptyException import io.legado.app.exception.TocEmptyException
import io.legado.app.help.IntentData
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.source.exploreKinds import io.legado.app.help.source.exploreKinds
import io.legado.app.model.CheckSource import io.legado.app.model.CheckSource
@@ -65,12 +66,12 @@ class CheckSourceService : BaseService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) { when (intent?.action) {
IntentAction.start -> intent.getStringArrayListExtra("selectIds")?.let { IntentAction.start -> IntentData.get<ArrayList<String>>("checkSourceSelectedIds")?.let {
check(it) check(it)
} }
IntentAction.resume -> upNotification() IntentAction.resume -> upNotification()
else -> stopSelf() IntentAction.stop -> stopSelf()
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
@@ -62,6 +62,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
override fun play() { override fun play() {
pageChanged = false
exoPlayer.stop() exoPlayer.stop()
if (contentList.isEmpty()) { if (contentList.isEmpty()) {
AppLog.putDebug("朗读列表为空") AppLog.putDebug("朗读列表为空")
@@ -285,8 +286,12 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun resumeReadAloud() { override fun resumeReadAloud() {
super.resumeReadAloud() super.resumeReadAloud()
kotlin.runCatching { kotlin.runCatching {
exoPlayer.play() if (pageChanged) {
upPlayPos() play()
} else {
exoPlayer.play()
upPlayPos()
}
} }
} }
@@ -32,7 +32,7 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
binding.titleBar.findViewById(R.id.search_view) binding.titleBar.findViewById(R.id.search_view)
} }
private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
it.uri?.let { treeUri -> it.uri?.let { treeUri ->
AppConfig.defaultBookTreeUri = treeUri.toString() AppConfig.defaultBookTreeUri = treeUri.toString()
localBookTreeSelectListener?.invoke(true) localBookTreeSelectListener?.invoke(true)
@@ -61,10 +61,12 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
*/ */
protected suspend fun setBookStorage() = suspendCoroutine { block -> protected suspend fun setBookStorage() = suspendCoroutine { block ->
localBookTreeSelectListener = { localBookTreeSelectListener = {
localBookTreeSelectListener = null
block.resume(it) block.resume(it)
} }
//测试书籍保存位置是否设置 //测试书籍保存位置是否设置
if (!AppConfig.defaultBookTreeUri.isNullOrBlank()) { if (!AppConfig.defaultBookTreeUri.isNullOrBlank()) {
localBookTreeSelectListener = null
block.resume(true) block.resume(true)
return@suspendCoroutine return@suspendCoroutine
} }
@@ -78,9 +80,11 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
} }
} }
noButton { noButton {
localBookTreeSelectListener = null
block.resume(false) block.resume(false)
} }
onCancelled { onCancelled {
localBookTreeSelectListener = null
block.resume(false) block.resume(false)
} }
} }
@@ -64,6 +64,14 @@ class RemoteBookActivity : BaseImportBookActivity<RemoteBookViewModel>(),
} }
} }
override fun observeLiveBus() {
viewModel.permissionDenialLiveData.observe(this) {
localBookTreeSelect.launch {
title = getString(R.string.select_book_folder)
}
}
}
private fun initView() { private fun initView() {
binding.layTop.setBackgroundColor(backgroundColor) binding.layTop.setBackgroundColor(backgroundColor)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
@@ -1,6 +1,7 @@
package io.legado.app.ui.book.import.remote package io.legado.app.ui.book.import.remote
import android.app.Application import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
@@ -25,6 +26,7 @@ class RemoteBookViewModel(application: Application) : BaseViewModel(application)
var sortKey = RemoteBookSort.Default var sortKey = RemoteBookSort.Default
var sortAscending = false var sortAscending = false
val dirList = arrayListOf<RemoteBook>() val dirList = arrayListOf<RemoteBook>()
val permissionDenialLiveData = MutableLiveData<Int>()
var dataCallback: DataCallback? = null var dataCallback: DataCallback? = null
@@ -141,6 +143,9 @@ class RemoteBookViewModel(application: Application) : BaseViewModel(application)
}.onError { }.onError {
AppLog.put("导入出错\n${it.localizedMessage}", it) AppLog.put("导入出错\n${it.localizedMessage}", it)
context.toastOnUi("导入出错\n${it.localizedMessage}") context.toastOnUi("导入出错\n${it.localizedMessage}")
if (it is SecurityException) {
permissionDenialLiveData.postValue(1)
}
}.onFinally { }.onFinally {
finally.invoke() finally.invoke()
} }
@@ -32,6 +32,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
private val selected = linkedSetOf<BookSource>() private val selected = linkedSetOf<BookSource>()
private val finalMessageRegex = Regex("成功|失败")
val selection: List<BookSource> val selection: List<BookSource>
get() { get() {
@@ -94,9 +95,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
cbBookSource.text = item.getDisPlayNameGroup() cbBookSource.text = item.getDisPlayNameGroup()
swtEnabled.isChecked = item.enabled swtEnabled.isChecked = item.enabled
cbBookSource.isChecked = selected.contains(item) cbBookSource.isChecked = selected.contains(item)
ivDebugText.text = Debug.debugMessageMap[item.bookSourceUrl] ?: "" upCheckSourceMessage(binding, item)
ivDebugText.visibility =
if (ivDebugText.text.toString().isNotBlank()) View.VISIBLE else View.GONE
upShowExplore(ivExplore, item) upShowExplore(ivExplore, item)
} else { } else {
payload.keySet().map { payload.keySet().map {
@@ -105,21 +104,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
"upName" -> cbBookSource.text = item.getDisPlayNameGroup() "upName" -> cbBookSource.text = item.getDisPlayNameGroup()
"upExplore" -> upShowExplore(ivExplore, item) "upExplore" -> upShowExplore(ivExplore, item)
"selected" -> cbBookSource.isChecked = selected.contains(item) "selected" -> cbBookSource.isChecked = selected.contains(item)
"checkSourceMessage" -> { "checkSourceMessage" -> upCheckSourceMessage(binding, item)
ivDebugText.text = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
val isEmpty = ivDebugText.text.toString().isEmpty()
var isFinalMessage =
ivDebugText.text.toString().contains(Regex("成功|失败"))
if (!Debug.isChecking && !isFinalMessage) {
Debug.updateFinalMessage(item.bookSourceUrl, "校验失败")
ivDebugText.text = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
isFinalMessage = true
}
ivDebugText.visibility =
if (!isEmpty) View.VISIBLE else View.GONE
ivProgressBar.visibility =
if (isFinalMessage || isEmpty || !Debug.isChecking) View.GONE else View.VISIBLE
}
} }
} }
} }
@@ -220,6 +205,22 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
} }
} }
private fun upCheckSourceMessage(binding: ItemBookSourceBinding, item: BookSource) = binding.run {
val msg = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
ivDebugText.text = msg
val isEmpty = msg.isEmpty()
var isFinalMessage = msg.contains(finalMessageRegex)
if (!Debug.isChecking && !isFinalMessage) {
Debug.updateFinalMessage(item.bookSourceUrl, "校验失败")
ivDebugText.text = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
isFinalMessage = true
}
ivDebugText.visibility =
if (!isEmpty) View.VISIBLE else View.GONE
ivProgressBar.visibility =
if (isFinalMessage || isEmpty || !Debug.isChecking) View.GONE else View.VISIBLE
}
fun selectAll() { fun selectAll() {
getItems().forEach { getItems().forEach {
selected.add(it) selected.add(it)
@@ -7,7 +7,9 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
@@ -30,7 +32,6 @@ import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -108,7 +109,7 @@ class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
launch { launch {
appDb.txtTocRuleDao.observeAll().conflate().collect { tocRules -> appDb.txtTocRuleDao.observeAll().conflate().collect { tocRules ->
initSelectedName(tocRules) initSelectedName(tocRules)
adapter.setItems(tocRules) adapter.setItems(tocRules, adapter.diffItemCallBack)
} }
} }
} }
@@ -191,6 +192,43 @@ class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
RecyclerAdapter<TxtTocRule, ItemTocRegexBinding>(context), RecyclerAdapter<TxtTocRule, ItemTocRegexBinding>(context),
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
val diffItemCallBack = object : DiffUtil.ItemCallback<TxtTocRule>() {
override fun areItemsTheSame(oldItem: TxtTocRule, newItem: TxtTocRule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: TxtTocRule, newItem: TxtTocRule): Boolean {
if (oldItem.name != newItem.name) {
return false
}
if (oldItem.enable != newItem.enable) {
return false
}
if (oldItem.example != newItem.example) {
return false
}
return true
}
override fun getChangePayload(oldItem: TxtTocRule, newItem: TxtTocRule): Any? {
val payload = Bundle()
if (oldItem.name != newItem.name) {
payload.putBoolean("upName", true)
}
if (oldItem.enable != newItem.enable) {
payload.putBoolean("enabled", newItem.enable)
}
if (oldItem.example != newItem.example) {
payload.putBoolean("upExample", true)
}
if (payload.isEmpty) {
return null
}
return payload
}
}
override fun getViewBinding(parent: ViewGroup): ItemTocRegexBinding { override fun getViewBinding(parent: ViewGroup): ItemTocRegexBinding {
return ItemTocRegexBinding.inflate(inflater, parent, false) return ItemTocRegexBinding.inflate(inflater, parent, false)
} }
@@ -202,14 +240,22 @@ class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
binding.apply { binding.apply {
if (payloads.isEmpty()) { val bundle = payloads.getOrNull(0) as? Bundle
if (bundle == null) {
root.setBackgroundColor(context.backgroundColor) root.setBackgroundColor(context.backgroundColor)
rbRegexName.text = item.name rbRegexName.text = item.name
titleExample.text = item.example titleExample.text = item.example
rbRegexName.isChecked = item.name == selectedName rbRegexName.isChecked = item.name == selectedName
swtEnabled.isChecked = item.enable swtEnabled.isChecked = item.enable
} else { } else {
rbRegexName.isChecked = item.name == selectedName bundle.keySet().map {
when (it) {
"upNmae" -> rbRegexName.text = item.name
"upExample" -> titleExample.text = item.example
"enabled" -> swtEnabled.isChecked = item.enable
"upSelect" -> rbRegexName.isChecked = item.name == selectedName
}
}
} }
} }
} }
@@ -219,16 +265,14 @@ class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
rbRegexName.setOnCheckedChangeListener { buttonView, isChecked -> rbRegexName.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed && isChecked) { if (buttonView.isPressed && isChecked) {
selectedName = getItem(holder.layoutPosition)?.name selectedName = getItem(holder.layoutPosition)?.name
updateItems(0, itemCount - 1, true) updateItems(0, itemCount - 1, bundleOf("upSelect" to null))
} }
} }
swtEnabled.setOnCheckedChangeListener { buttonView, isChecked -> swtEnabled.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed) { if (buttonView.isPressed) {
getItem(holder.layoutPosition)?.let { getItem(holder.layoutPosition)?.let {
it.enable = isChecked it.enable = isChecked
launch(IO) { viewModel.update(it)
appDb.txtTocRuleDao.update(it)
}
} }
} }
} }
@@ -237,9 +281,7 @@ class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
} }
ivDelete.setOnClickListener { ivDelete.setOnClickListener {
getItem(holder.layoutPosition)?.let { item -> getItem(holder.layoutPosition)?.let { item ->
launch(IO) { viewModel.del(item)
appDb.txtTocRuleDao.delete(item)
}
} }
} }
} }
@@ -259,9 +301,7 @@ class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
for ((index, item) in getItems().withIndex()) { for ((index, item) in getItems().withIndex()) {
item.serialNumber = index + 1 item.serialNumber = index + 1
} }
launch(IO) { viewModel.update(*getItems().toTypedArray())
appDb.txtTocRuleDao.update(*getItems().toTypedArray())
}
} }
isMoved = false isMoved = false
} }