kunfei 4 tahun lalu
induk
melakukan
ec3aca9e9d

+ 3 - 0
app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt

@@ -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
 

+ 16 - 3
app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt

@@ -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
+    }
 
 }

+ 20 - 1
app/src/main/java/io/legado/app/help/config/ThemeConfig.kt

@@ -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
+        }
+
+    }
 
 }

+ 22 - 24
app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt

@@ -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()) {

+ 23 - 13
app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt

@@ -70,19 +70,29 @@ class FileAssociationActivity :
             }
             finish()
         }
-        viewModel.importBookSourceLive.observe(this) {
-            binding.rotateLoading.hide()
-            showDialogFragment(ImportBookSourceDialog(it, true))
-        }
-        viewModel.importRssSourceLive.observe(this) {
-            binding.rotateLoading.hide()
-            showDialogFragment(ImportRssSourceDialog(it, true))
-        }
-        viewModel.importReplaceRuleLive.observe(this) {
-            binding.rotateLoading.hide()
-            showDialogFragment(ImportReplaceRuleDialog(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.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)
         }
     }
 

+ 3 - 22
app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt

@@ -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
app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt

@@ -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 ->

+ 179 - 0
app/src/main/java/io/legado/app/ui/association/ImportThemeDialog.kt

@@ -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()
+                        )
+                    )
+                }
+            }
+        }
+
+    }
+}

+ 106 - 0
app/src/main/java/io/legado/app/ui/association/ImportThemeViewModel.kt

@@ -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)
+        }
+    }
+
+}

+ 179 - 0
app/src/main/java/io/legado/app/ui/association/ImportTxtRuleDialog.kt

@@ -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()
+                        )
+                    )
+                }
+            }
+        }
+
+    }
+}

+ 107 - 0
app/src/main/java/io/legado/app/ui/association/ImportTxtRuleViewModel.kt

@@ -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)
+        }
+    }
+
+}

+ 6 - 0
app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt

@@ -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 - 18
app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt

@@ -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)
                 }
             }
         }