mirror of
https://github.com/gedoor/legado.git
synced 2025-08-10 00:52:30 +00:00
优化
This commit is contained in:
@@ -19,6 +19,9 @@ interface TxtTocRuleDao {
|
||||
@get:Query("select * from txtTocRules where enable != 1 order by serialNumber")
|
||||
val disabled: List<TxtTocRule>
|
||||
|
||||
@Query("select * from txtTocRules where id = :id")
|
||||
fun get(id: Long): TxtTocRule?
|
||||
|
||||
@get:Query("select ifNull(min(serialNumber), 0) from txtTocRules")
|
||||
val minOrder: Int
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import io.legado.app.utils.GSON
|
||||
|
||||
|
||||
@Entity(tableName = "txtTocRules")
|
||||
@@ -15,7 +15,20 @@ data class TxtTocRule(
|
||||
var enable: Boolean = true
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
constructor() : this(name = "")
|
||||
override fun hashCode(): Int {
|
||||
return GSON.toJson(this).hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
other ?: return false
|
||||
if (other is TxtTocRule) {
|
||||
return id == other.id
|
||||
&& name == other.name
|
||||
&& rule == other.rule
|
||||
&& serialNumber == other.serialNumber
|
||||
&& enable == other.enable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -256,6 +256,25 @@ object ThemeConfig {
|
||||
var accentColor: String,
|
||||
var backgroundColor: String,
|
||||
var bottomBackground: String
|
||||
)
|
||||
) {
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return GSON.toJson(this).hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
other ?: return false
|
||||
if (other is Config) {
|
||||
return other.themeName == themeName
|
||||
&& other.isNightTheme == isNightTheme
|
||||
&& other.primaryColor == primaryColor
|
||||
&& other.accentColor == accentColor
|
||||
&& other.backgroundColor == backgroundColor
|
||||
&& other.bottomBackground == bottomBackground
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.data.entities.TxtTocRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
@@ -15,6 +15,27 @@ import io.legado.app.utils.isJsonArray
|
||||
|
||||
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
val successLive = MutableLiveData<Pair<String, String>>()
|
||||
val errorLive = MutableLiveData<String>()
|
||||
|
||||
fun importJson(json: String) {
|
||||
//暂时根据文件内容判断属于什么
|
||||
when {
|
||||
json.contains("bookSourceUrl") ->
|
||||
successLive.postValue(Pair("bookSource", json))
|
||||
json.contains("sourceUrl") ->
|
||||
successLive.postValue(Pair("rssSource", json))
|
||||
json.contains("pattern") ->
|
||||
successLive.postValue(Pair("replaceRule", json))
|
||||
json.contains("themeName") ->
|
||||
successLive.postValue(Pair("theme", json))
|
||||
json.contains("name") && json.contains("rule") ->
|
||||
successLive.postValue(Pair("txtRule", json))
|
||||
json.contains("name") && json.contains("url") ->
|
||||
successLive.postValue(Pair("httpTts", json))
|
||||
else -> errorLive.postValue("格式不对")
|
||||
}
|
||||
}
|
||||
|
||||
fun importTextTocRule(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||
execute {
|
||||
@@ -37,29 +58,6 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode
|
||||
}
|
||||
}
|
||||
|
||||
fun importHttpTTS(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||
execute {
|
||||
if (json.isJsonArray()) {
|
||||
HttpTTS.fromJsonArray(json).getOrThrow().let {
|
||||
appDb.httpTTSDao.insert(*it.toTypedArray())
|
||||
return@execute it.size
|
||||
}
|
||||
} else {
|
||||
HttpTTS.fromJson(json).getOrThrow().let {
|
||||
appDb.httpTTSDao.insert(it)
|
||||
return@execute 1
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
finally.invoke(context.getString(R.string.success), "导入${it}朗读引擎")
|
||||
}.onError {
|
||||
finally.invoke(
|
||||
context.getString(R.string.error),
|
||||
it.localizedMessage ?: context.getString(R.string.unknown_error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun importTheme(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||
execute {
|
||||
if (json.isJsonArray()) {
|
||||
|
||||
@@ -70,19 +70,29 @@ class FileAssociationActivity :
|
||||
}
|
||||
finish()
|
||||
}
|
||||
viewModel.importBookSourceLive.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
showDialogFragment(ImportBookSourceDialog(it, true))
|
||||
viewModel.successLive.observe(this) {
|
||||
when (it.first) {
|
||||
"bookSource" -> showDialogFragment(
|
||||
ImportBookSourceDialog(it.second, true)
|
||||
)
|
||||
"rssSource" -> showDialogFragment(
|
||||
ImportRssSourceDialog(it.second, true)
|
||||
)
|
||||
"replaceRule" -> showDialogFragment(
|
||||
ImportReplaceRuleDialog(it.second, true)
|
||||
)
|
||||
"httpTts" -> showDialogFragment(
|
||||
ImportHttpTtsDialog(it.second, true)
|
||||
)
|
||||
"theme" -> showDialogFragment(
|
||||
ImportThemeDialog(it.second, true)
|
||||
)
|
||||
"txtRule" -> showDialogFragment(
|
||||
ImportTxtRuleDialog(it.second, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
viewModel.importRssSourceLive.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
showDialogFragment(ImportRssSourceDialog(it, true))
|
||||
}
|
||||
viewModel.importReplaceRuleLive.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
showDialogFragment(ImportReplaceRuleDialog(it, true))
|
||||
}
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
viewModel.errorLive.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
toastOnUi(it)
|
||||
finish()
|
||||
@@ -95,7 +105,7 @@ class FileAssociationActivity :
|
||||
finish()
|
||||
}
|
||||
intent.data?.let { data ->
|
||||
viewModel.dispatchIndent(data, this::finallyDialog)
|
||||
viewModel.dispatchIndent(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,10 @@ import java.io.File
|
||||
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
|
||||
val importBookLiveData = MutableLiveData<Uri>()
|
||||
val onLineImportLive = MutableLiveData<Uri>()
|
||||
val importBookSourceLive = MutableLiveData<String>()
|
||||
val importRssSourceLive = MutableLiveData<String>()
|
||||
val importReplaceRuleLive = MutableLiveData<String>()
|
||||
val openBookLiveData = MutableLiveData<String>()
|
||||
val errorLiveData = MutableLiveData<String>()
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
fun dispatchIndent(uri: Uri, finally: (title: String, msg: String) -> Unit) {
|
||||
fun dispatchIndent(uri: Uri) {
|
||||
execute {
|
||||
lateinit var fileName: String
|
||||
lateinit var content: String
|
||||
@@ -39,22 +35,7 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
||||
}
|
||||
when {
|
||||
content.isJson() -> {
|
||||
//暂时根据文件内容判断属于什么
|
||||
when {
|
||||
content.contains("bookSourceUrl") ->
|
||||
importBookSourceLive.postValue(content)
|
||||
content.contains("sourceUrl") ->
|
||||
importRssSourceLive.postValue(content)
|
||||
content.contains("pattern") ->
|
||||
importReplaceRuleLive.postValue(content)
|
||||
content.contains("themeName") ->
|
||||
importTheme(content, finally)
|
||||
content.contains("name") && content.contains("rule") ->
|
||||
importTextTocRule(content, finally)
|
||||
content.contains("name") && content.contains("url") ->
|
||||
importHttpTTS(content, finally)
|
||||
else -> errorLiveData.postValue("格式不对")
|
||||
}
|
||||
importJson(content)
|
||||
}
|
||||
fileName.matches(bookFileRegex) -> {
|
||||
importBookLiveData.postValue(uri)
|
||||
@@ -68,7 +49,7 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
||||
}
|
||||
}.onError {
|
||||
it.printOnDebug()
|
||||
errorLiveData.postValue(it.localizedMessage)
|
||||
errorLive.postValue(it.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@ package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.jayway.jsonpath.JsonPath
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
@@ -56,7 +54,6 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
||||
}
|
||||
}
|
||||
appDb.httpTTSDao.insert(*selectSource.toTypedArray())
|
||||
ContentProcessor.upReplaceRules()
|
||||
}.onFinally {
|
||||
finally.invoke()
|
||||
}
|
||||
@@ -67,16 +64,8 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
||||
val mText = text.trim()
|
||||
when {
|
||||
mText.isJsonObject() -> {
|
||||
val json = JsonPath.parse(mText)
|
||||
val urls = json.read<List<String>>("$.sourceUrls")
|
||||
if (!urls.isNullOrEmpty()) {
|
||||
urls.forEach {
|
||||
importSourceUrl(it)
|
||||
}
|
||||
} else {
|
||||
HttpTTS.fromJson(mText).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
HttpTTS.fromJson(mText).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
mText.isJsonArray() -> HttpTTS.fromJsonArray(mText).getOrThrow().let { items ->
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseDialogFragment
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.databinding.DialogRecyclerViewBinding
|
||||
import io.legado.app.databinding.ItemSourceImportBinding
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.widget.dialog.CodeDialog
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.setLayout
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import io.legado.app.utils.visible
|
||||
import splitties.views.onClick
|
||||
|
||||
class ImportThemeDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) {
|
||||
|
||||
constructor(source: String, finishOnDismiss: Boolean = false) : this() {
|
||||
arguments = Bundle().apply {
|
||||
putString("source", source)
|
||||
putBoolean("finishOnDismiss", finishOnDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
||||
private val viewModel by viewModels<ImportThemeViewModel>()
|
||||
private val adapter by lazy { SourcesAdapter(requireContext()) }
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
if (arguments?.getBoolean("finishOnDismiss") == true) {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.toolBar.setBackgroundColor(primaryColor)
|
||||
binding.toolBar.setTitle(R.string.import_book_source)
|
||||
binding.rotateLoading.show()
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.tvCancel.visible()
|
||||
binding.tvCancel.setOnClickListener {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
binding.tvOk.visible()
|
||||
binding.tvOk.setOnClickListener {
|
||||
val waitDialog = WaitDialog(requireContext())
|
||||
waitDialog.show()
|
||||
viewModel.importSelect {
|
||||
waitDialog.dismiss()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
binding.tvFooterLeft.visible()
|
||||
binding.tvFooterLeft.setOnClickListener {
|
||||
val selectAll = viewModel.isSelectAll
|
||||
viewModel.selectStatus.forEachIndexed { index, b ->
|
||||
if (b != !selectAll) {
|
||||
viewModel.selectStatus[index] = !selectAll
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
upSelectText()
|
||||
}
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
binding.tvMsg.apply {
|
||||
text = it
|
||||
visible()
|
||||
}
|
||||
}
|
||||
viewModel.successLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
if (it > 0) {
|
||||
adapter.setItems(viewModel.allSources)
|
||||
upSelectText()
|
||||
} else {
|
||||
binding.tvMsg.apply {
|
||||
setText(R.string.wrong_format)
|
||||
visible()
|
||||
}
|
||||
}
|
||||
}
|
||||
val source = arguments?.getString("source")
|
||||
if (source.isNullOrEmpty()) {
|
||||
dismiss()
|
||||
return
|
||||
}
|
||||
viewModel.importSource(source)
|
||||
}
|
||||
|
||||
private fun upSelectText() {
|
||||
if (viewModel.isSelectAll) {
|
||||
binding.tvFooterLeft.text = getString(
|
||||
R.string.select_cancel_count,
|
||||
viewModel.selectCount,
|
||||
viewModel.allSources.size
|
||||
)
|
||||
} else {
|
||||
binding.tvFooterLeft.text = getString(
|
||||
R.string.select_all_count,
|
||||
viewModel.selectCount,
|
||||
viewModel.allSources.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inner class SourcesAdapter(context: Context) :
|
||||
RecyclerAdapter<ThemeConfig.Config, ItemSourceImportBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding {
|
||||
return ItemSourceImportBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemSourceImportBinding,
|
||||
item: ThemeConfig.Config,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.apply {
|
||||
cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
|
||||
cbSourceName.text = item.themeName
|
||||
val localSource = viewModel.checkSources[holder.layoutPosition]
|
||||
tvSourceState.text = when {
|
||||
localSource == null -> "新增"
|
||||
localSource != item -> "更新"
|
||||
else -> "已有"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) {
|
||||
binding.apply {
|
||||
cbSourceName.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (buttonView.isPressed) {
|
||||
viewModel.selectStatus[holder.layoutPosition] = isChecked
|
||||
upSelectText()
|
||||
}
|
||||
}
|
||||
root.onClick {
|
||||
cbSourceName.isChecked = !cbSourceName.isChecked
|
||||
viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked
|
||||
upSelectText()
|
||||
}
|
||||
tvOpen.setOnClickListener {
|
||||
val source = viewModel.allSources[holder.layoutPosition]
|
||||
showDialogFragment(
|
||||
CodeDialog(
|
||||
GSON.toJson(source),
|
||||
disableEdit = false,
|
||||
requestId = holder.layoutPosition.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.utils.*
|
||||
|
||||
class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
||||
|
||||
val errorLiveData = MutableLiveData<String>()
|
||||
val successLiveData = MutableLiveData<Int>()
|
||||
|
||||
val allSources = arrayListOf<ThemeConfig.Config>()
|
||||
val checkSources = arrayListOf<ThemeConfig.Config?>()
|
||||
val selectStatus = arrayListOf<Boolean>()
|
||||
|
||||
val isSelectAll: Boolean
|
||||
get() {
|
||||
selectStatus.forEach {
|
||||
if (!it) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val selectCount: Int
|
||||
get() {
|
||||
var count = 0
|
||||
selectStatus.forEach {
|
||||
if (it) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fun importSelect(finally: () -> Unit) {
|
||||
execute {
|
||||
selectStatus.forEachIndexed { index, b ->
|
||||
if (b) {
|
||||
ThemeConfig.addConfig(allSources[index])
|
||||
}
|
||||
}
|
||||
}.onFinally {
|
||||
finally.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun importSource(text: String) {
|
||||
execute {
|
||||
importSourceAwait(text.trim())
|
||||
}.onError {
|
||||
it.printOnDebug()
|
||||
errorLiveData.postValue(it.localizedMessage ?: "")
|
||||
}.onSuccess {
|
||||
comparisonSource()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSourceAwait(text: String) {
|
||||
when {
|
||||
text.isJsonObject() -> {
|
||||
GSON.fromJsonObject<ThemeConfig.Config>(text).getOrThrow()?.let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
text.isJsonArray() -> GSON.fromJsonArray<ThemeConfig.Config>(text).getOrThrow()
|
||||
?.let { items ->
|
||||
allSources.addAll(items)
|
||||
}
|
||||
text.isAbsUrl() -> {
|
||||
importSourceUrl(text)
|
||||
}
|
||||
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSourceUrl(url: String) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.text().let {
|
||||
importSourceAwait(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun comparisonSource() {
|
||||
execute {
|
||||
allSources.forEach { config ->
|
||||
val source = ThemeConfig.configList.find {
|
||||
it.themeName == config.themeName
|
||||
}
|
||||
checkSources.add(source)
|
||||
selectStatus.add(source == null || source != config)
|
||||
}
|
||||
successLiveData.postValue(allSources.size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseDialogFragment
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.data.entities.TxtTocRule
|
||||
import io.legado.app.databinding.DialogRecyclerViewBinding
|
||||
import io.legado.app.databinding.ItemSourceImportBinding
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.widget.dialog.CodeDialog
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.setLayout
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import io.legado.app.utils.visible
|
||||
import splitties.views.onClick
|
||||
|
||||
class ImportTxtRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) {
|
||||
|
||||
constructor(source: String, finishOnDismiss: Boolean = false) : this() {
|
||||
arguments = Bundle().apply {
|
||||
putString("source", source)
|
||||
putBoolean("finishOnDismiss", finishOnDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
||||
private val viewModel by viewModels<ImportTxtRuleViewModel>()
|
||||
private val adapter by lazy { SourcesAdapter(requireContext()) }
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
if (arguments?.getBoolean("finishOnDismiss") == true) {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.toolBar.setBackgroundColor(primaryColor)
|
||||
binding.toolBar.setTitle(R.string.import_book_source)
|
||||
binding.rotateLoading.show()
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.tvCancel.visible()
|
||||
binding.tvCancel.setOnClickListener {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
binding.tvOk.visible()
|
||||
binding.tvOk.setOnClickListener {
|
||||
val waitDialog = WaitDialog(requireContext())
|
||||
waitDialog.show()
|
||||
viewModel.importSelect {
|
||||
waitDialog.dismiss()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
binding.tvFooterLeft.visible()
|
||||
binding.tvFooterLeft.setOnClickListener {
|
||||
val selectAll = viewModel.isSelectAll
|
||||
viewModel.selectStatus.forEachIndexed { index, b ->
|
||||
if (b != !selectAll) {
|
||||
viewModel.selectStatus[index] = !selectAll
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
upSelectText()
|
||||
}
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
binding.tvMsg.apply {
|
||||
text = it
|
||||
visible()
|
||||
}
|
||||
}
|
||||
viewModel.successLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
if (it > 0) {
|
||||
adapter.setItems(viewModel.allSources)
|
||||
upSelectText()
|
||||
} else {
|
||||
binding.tvMsg.apply {
|
||||
setText(R.string.wrong_format)
|
||||
visible()
|
||||
}
|
||||
}
|
||||
}
|
||||
val source = arguments?.getString("source")
|
||||
if (source.isNullOrEmpty()) {
|
||||
dismiss()
|
||||
return
|
||||
}
|
||||
viewModel.importSource(source)
|
||||
}
|
||||
|
||||
private fun upSelectText() {
|
||||
if (viewModel.isSelectAll) {
|
||||
binding.tvFooterLeft.text = getString(
|
||||
R.string.select_cancel_count,
|
||||
viewModel.selectCount,
|
||||
viewModel.allSources.size
|
||||
)
|
||||
} else {
|
||||
binding.tvFooterLeft.text = getString(
|
||||
R.string.select_all_count,
|
||||
viewModel.selectCount,
|
||||
viewModel.allSources.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inner class SourcesAdapter(context: Context) :
|
||||
RecyclerAdapter<TxtTocRule, ItemSourceImportBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding {
|
||||
return ItemSourceImportBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemSourceImportBinding,
|
||||
item: TxtTocRule,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.apply {
|
||||
cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
|
||||
cbSourceName.text = item.name
|
||||
val localSource = viewModel.checkSources[holder.layoutPosition]
|
||||
tvSourceState.text = when {
|
||||
localSource == null -> "新增"
|
||||
item != localSource -> "更新"
|
||||
else -> "已有"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) {
|
||||
binding.apply {
|
||||
cbSourceName.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (buttonView.isPressed) {
|
||||
viewModel.selectStatus[holder.layoutPosition] = isChecked
|
||||
upSelectText()
|
||||
}
|
||||
}
|
||||
root.onClick {
|
||||
cbSourceName.isChecked = !cbSourceName.isChecked
|
||||
viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked
|
||||
upSelectText()
|
||||
}
|
||||
tvOpen.setOnClickListener {
|
||||
val source = viewModel.allSources[holder.layoutPosition]
|
||||
showDialogFragment(
|
||||
CodeDialog(
|
||||
GSON.toJson(source),
|
||||
disableEdit = false,
|
||||
requestId = holder.layoutPosition.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.TxtTocRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.utils.*
|
||||
|
||||
class ImportTxtRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||
|
||||
val errorLiveData = MutableLiveData<String>()
|
||||
val successLiveData = MutableLiveData<Int>()
|
||||
|
||||
val allSources = arrayListOf<TxtTocRule>()
|
||||
val checkSources = arrayListOf<TxtTocRule?>()
|
||||
val selectStatus = arrayListOf<Boolean>()
|
||||
|
||||
val isSelectAll: Boolean
|
||||
get() {
|
||||
selectStatus.forEach {
|
||||
if (!it) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val selectCount: Int
|
||||
get() {
|
||||
var count = 0
|
||||
selectStatus.forEach {
|
||||
if (it) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fun importSelect(finally: () -> Unit) {
|
||||
execute {
|
||||
val selectSource = arrayListOf<TxtTocRule>()
|
||||
selectStatus.forEachIndexed { index, b ->
|
||||
if (b) {
|
||||
selectSource.add(allSources[index])
|
||||
}
|
||||
}
|
||||
appDb.txtTocRuleDao.insert(*selectSource.toTypedArray())
|
||||
}.onFinally {
|
||||
finally.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun importSource(text: String) {
|
||||
execute {
|
||||
importSourceAwait(text.trim())
|
||||
}.onError {
|
||||
it.printOnDebug()
|
||||
errorLiveData.postValue(it.localizedMessage ?: "")
|
||||
}.onSuccess {
|
||||
comparisonSource()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSourceAwait(text: String) {
|
||||
when {
|
||||
text.isJsonObject() -> {
|
||||
GSON.fromJsonObject<TxtTocRule>(text).getOrThrow()?.let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
text.isJsonArray() -> GSON.fromJsonArray<TxtTocRule>(text).getOrThrow()
|
||||
?.let { items ->
|
||||
allSources.addAll(items)
|
||||
}
|
||||
text.isAbsUrl() -> {
|
||||
importSourceUrl(text)
|
||||
}
|
||||
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSourceUrl(url: String) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.text().let {
|
||||
importSourceAwait(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun comparisonSource() {
|
||||
execute {
|
||||
allSources.forEach {
|
||||
val source = appDb.txtTocRuleDao.get(it.id)
|
||||
checkSources.add(source)
|
||||
selectStatus.add(source == null || it != source)
|
||||
}
|
||||
successLiveData.postValue(allSources.size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,6 +34,12 @@ class OnLineImportActivity :
|
||||
"httpTts" -> showDialogFragment(
|
||||
ImportHttpTtsDialog(it.second, true)
|
||||
)
|
||||
"theme" -> showDialogFragment(
|
||||
ImportThemeDialog(it.second, true)
|
||||
)
|
||||
"txtRule" -> showDialogFragment(
|
||||
ImportTxtRuleDialog(it.second, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
viewModel.errorLive.observe(this) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
@@ -10,8 +9,6 @@ import io.legado.app.help.http.text
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
|
||||
class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
|
||||
val successLive = MutableLiveData<Pair<String, String>>()
|
||||
val errorLive = MutableLiveData<String>()
|
||||
|
||||
fun getText(url: String, success: (text: String) -> Unit) {
|
||||
execute {
|
||||
@@ -76,21 +73,7 @@ class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
|
||||
}
|
||||
else -> {
|
||||
val json = rs.text("utf-8")
|
||||
when {
|
||||
json.contains("bookSourceUrl") ->
|
||||
successLive.postValue(Pair("bookSource", json))
|
||||
json.contains("sourceUrl") ->
|
||||
successLive.postValue(Pair("rssSource", json))
|
||||
json.contains("replacement") ->
|
||||
successLive.postValue(Pair("replaceRule", json))
|
||||
json.contains("themeName") ->
|
||||
importTheme(json, finally)
|
||||
json.contains("name") && json.contains("rule") ->
|
||||
importTextTocRule(json, finally)
|
||||
json.contains("name") && json.contains("url") ->
|
||||
successLive.postValue(Pair("httpTts", json))
|
||||
else -> errorLive.postValue("格式不对")
|
||||
}
|
||||
importJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user