优化
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
@Query("select * from txtTocRules where id = :id")
|
||||||
|
fun get(id: Long): TxtTocRule?
|
||||||
|
|
||||||
@get:Query("select ifNull(min(serialNumber), 0) from txtTocRules")
|
@get:Query("select ifNull(min(serialNumber), 0) from txtTocRules")
|
||||||
val minOrder: Int
|
val minOrder: Int
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package io.legado.app.data.entities
|
package io.legado.app.data.entities
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Ignore
|
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
|
||||||
|
|
||||||
@Entity(tableName = "txtTocRules")
|
@Entity(tableName = "txtTocRules")
|
||||||
@@ -15,7 +15,20 @@ data class TxtTocRule(
|
|||||||
var enable: Boolean = true
|
var enable: Boolean = true
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Ignore
|
override fun hashCode(): Int {
|
||||||
constructor() : this(name = "")
|
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 accentColor: String,
|
||||||
var backgroundColor: String,
|
var backgroundColor: String,
|
||||||
var bottomBackground: 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
|
package io.legado.app.ui.association
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.HttpTTS
|
|
||||||
import io.legado.app.data.entities.TxtTocRule
|
import io.legado.app.data.entities.TxtTocRule
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.config.ThemeConfig
|
import io.legado.app.help.config.ThemeConfig
|
||||||
@@ -15,6 +15,27 @@ import io.legado.app.utils.isJsonArray
|
|||||||
|
|
||||||
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
|
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) {
|
fun importTextTocRule(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||||
execute {
|
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) {
|
fun importTheme(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||||
execute {
|
execute {
|
||||||
if (json.isJsonArray()) {
|
if (json.isJsonArray()) {
|
||||||
|
|||||||
@@ -70,19 +70,29 @@ class FileAssociationActivity :
|
|||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
viewModel.importBookSourceLive.observe(this) {
|
viewModel.successLive.observe(this) {
|
||||||
binding.rotateLoading.hide()
|
when (it.first) {
|
||||||
showDialogFragment(ImportBookSourceDialog(it, true))
|
"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) {
|
viewModel.errorLive.observe(this) {
|
||||||
binding.rotateLoading.hide()
|
|
||||||
showDialogFragment(ImportRssSourceDialog(it, true))
|
|
||||||
}
|
|
||||||
viewModel.importReplaceRuleLive.observe(this) {
|
|
||||||
binding.rotateLoading.hide()
|
|
||||||
showDialogFragment(ImportReplaceRuleDialog(it, true))
|
|
||||||
}
|
|
||||||
viewModel.errorLiveData.observe(this) {
|
|
||||||
binding.rotateLoading.hide()
|
binding.rotateLoading.hide()
|
||||||
toastOnUi(it)
|
toastOnUi(it)
|
||||||
finish()
|
finish()
|
||||||
@@ -95,7 +105,7 @@ class FileAssociationActivity :
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
intent.data?.let { data ->
|
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) {
|
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
|
||||||
val importBookLiveData = MutableLiveData<Uri>()
|
val importBookLiveData = MutableLiveData<Uri>()
|
||||||
val onLineImportLive = MutableLiveData<Uri>()
|
val onLineImportLive = MutableLiveData<Uri>()
|
||||||
val importBookSourceLive = MutableLiveData<String>()
|
|
||||||
val importRssSourceLive = MutableLiveData<String>()
|
|
||||||
val importReplaceRuleLive = MutableLiveData<String>()
|
|
||||||
val openBookLiveData = MutableLiveData<String>()
|
val openBookLiveData = MutableLiveData<String>()
|
||||||
val errorLiveData = MutableLiveData<String>()
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
fun dispatchIndent(uri: Uri, finally: (title: String, msg: String) -> Unit) {
|
fun dispatchIndent(uri: Uri) {
|
||||||
execute {
|
execute {
|
||||||
lateinit var fileName: String
|
lateinit var fileName: String
|
||||||
lateinit var content: String
|
lateinit var content: String
|
||||||
@@ -39,22 +35,7 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
|||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
content.isJson() -> {
|
content.isJson() -> {
|
||||||
//暂时根据文件内容判断属于什么
|
importJson(content)
|
||||||
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("格式不对")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fileName.matches(bookFileRegex) -> {
|
fileName.matches(bookFileRegex) -> {
|
||||||
importBookLiveData.postValue(uri)
|
importBookLiveData.postValue(uri)
|
||||||
@@ -68,7 +49,7 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
|||||||
}
|
}
|
||||||
}.onError {
|
}.onError {
|
||||||
it.printOnDebug()
|
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 android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.jayway.jsonpath.JsonPath
|
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.HttpTTS
|
import io.legado.app.data.entities.HttpTTS
|
||||||
import io.legado.app.exception.NoStackTraceException
|
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.newCallResponseBody
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.help.http.text
|
import io.legado.app.help.http.text
|
||||||
@@ -56,7 +54,6 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
appDb.httpTTSDao.insert(*selectSource.toTypedArray())
|
appDb.httpTTSDao.insert(*selectSource.toTypedArray())
|
||||||
ContentProcessor.upReplaceRules()
|
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
finally.invoke()
|
finally.invoke()
|
||||||
}
|
}
|
||||||
@@ -67,16 +64,8 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
val mText = text.trim()
|
val mText = text.trim()
|
||||||
when {
|
when {
|
||||||
mText.isJsonObject() -> {
|
mText.isJsonObject() -> {
|
||||||
val json = JsonPath.parse(mText)
|
HttpTTS.fromJson(mText).getOrThrow().let {
|
||||||
val urls = json.read<List<String>>("$.sourceUrls")
|
allSources.add(it)
|
||||||
if (!urls.isNullOrEmpty()) {
|
|
||||||
urls.forEach {
|
|
||||||
importSourceUrl(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HttpTTS.fromJson(mText).getOrThrow().let {
|
|
||||||
allSources.add(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mText.isJsonArray() -> HttpTTS.fromJsonArray(mText).getOrThrow().let { items ->
|
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(
|
"httpTts" -> showDialogFragment(
|
||||||
ImportHttpTtsDialog(it.second, true)
|
ImportHttpTtsDialog(it.second, true)
|
||||||
)
|
)
|
||||||
|
"theme" -> showDialogFragment(
|
||||||
|
ImportThemeDialog(it.second, true)
|
||||||
|
)
|
||||||
|
"txtRule" -> showDialogFragment(
|
||||||
|
ImportTxtRuleDialog(it.second, true)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.errorLive.observe(this) {
|
viewModel.errorLive.observe(this) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.legado.app.ui.association
|
package io.legado.app.ui.association
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.http.newCallResponseBody
|
import io.legado.app.help.http.newCallResponseBody
|
||||||
@@ -10,8 +9,6 @@ import io.legado.app.help.http.text
|
|||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
|
||||||
class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
|
class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
|
||||||
val successLive = MutableLiveData<Pair<String, String>>()
|
|
||||||
val errorLive = MutableLiveData<String>()
|
|
||||||
|
|
||||||
fun getText(url: String, success: (text: String) -> Unit) {
|
fun getText(url: String, success: (text: String) -> Unit) {
|
||||||
execute {
|
execute {
|
||||||
@@ -76,21 +73,7 @@ class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val json = rs.text("utf-8")
|
val json = rs.text("utf-8")
|
||||||
when {
|
importJson(json)
|
||||||
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("格式不对")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user