Horis il y a 3 ans
Parent
commit
39a90d21d6
22 fichiers modifiés avec 394 ajouts et 143 suppressions
  1. 7 0
      app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt
  2. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  3. 82 16
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  4. 73 0
      app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt
  5. 33 20
      app/src/main/java/io/legado/app/help/http/BackstageWebView.kt
  6. 2 1
      app/src/main/java/io/legado/app/model/CheckSource.kt
  7. 12 13
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  8. 6 3
      app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt
  9. 5 0
      app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt
  10. 50 18
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  11. 40 32
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  12. 45 40
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  13. 8 0
      app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt
  14. 10 0
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  15. 2 0
      app/src/main/res/values-es-rES/strings.xml
  16. 2 0
      app/src/main/res/values-ja-rJP/strings.xml
  17. 2 0
      app/src/main/res/values-pt-rBR/strings.xml
  18. 2 0
      app/src/main/res/values-zh-rHK/strings.xml
  19. 2 0
      app/src/main/res/values-zh-rTW/strings.xml
  20. 2 0
      app/src/main/res/values-zh/strings.xml
  21. 2 0
      app/src/main/res/values/strings.xml
  22. 6 0
      app/src/main/res/xml/pref_config_other.xml

+ 7 - 0
app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt

@@ -5,6 +5,7 @@ import android.content.Context
 import android.util.SparseArray
 import android.util.SparseArray
 import android.view.LayoutInflater
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import android.view.ViewGroup
+import androidx.core.os.postDelayed
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView
@@ -160,6 +161,12 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
                     onCurrentListChanged()
                     onCurrentListChanged()
                 }
                 }
             }
             }
+            handler.postDelayed(1000) {
+                if (diffJob?.isCompleted == false) {
+                    diffJob?.cancel()
+                    setItems(items)
+                }
+            }
         }
         }
     }
     }
 
 

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -130,6 +130,7 @@ object PreferKey {
     const val clearWebViewData = "clearWebViewData"
     const val clearWebViewData = "clearWebViewData"
     const val onlyLatestBackup = "onlyLatestBackup"
     const val onlyLatestBackup = "onlyLatestBackup"
     const val brightnessVwPos = "brightnessVwPos"
     const val brightnessVwPos = "brightnessVwPos"
+    const val shrinkDatabase = "shrinkDatabase"
 
 
     const val cPrimary = "colorPrimary"
     const val cPrimary = "colorPrimary"
     const val cAccent = "colorAccent"
     const val cAccent = "colorAccent"

+ 82 - 16
app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt

@@ -3,6 +3,7 @@ package io.legado.app.data.dao
 import androidx.room.*
 import androidx.room.*
 import io.legado.app.constant.AppPattern
 import io.legado.app.constant.AppPattern
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.splitNotBlank
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
@@ -11,21 +12,29 @@ import kotlinx.coroutines.flow.map
 @Dao
 @Dao
 interface BookSourceDao {
 interface BookSourceDao {
 
 
-    @Query("select * from book_sources order by customOrder asc")
-    fun flowAll(): Flow<List<BookSource>>
+    @Query(
+        """select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources order by customOrder asc"""
+    )
+    fun flowAll(): Flow<List<BookSourcePart>>
 
 
     @Query(
     @Query(
-        """select * from book_sources 
+        """select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources 
         where bookSourceName like '%' || :searchKey || '%'
         where bookSourceName like '%' || :searchKey || '%'
         or bookSourceGroup like '%' || :searchKey || '%'
         or bookSourceGroup like '%' || :searchKey || '%'
         or bookSourceUrl like '%' || :searchKey || '%'
         or bookSourceUrl like '%' || :searchKey || '%'
         or bookSourceComment like '%' || :searchKey || '%' 
         or bookSourceComment like '%' || :searchKey || '%' 
         order by customOrder asc"""
         order by customOrder asc"""
     )
     )
-    fun flowSearch(searchKey: String): Flow<List<BookSource>>
+    fun flowSearch(searchKey: String): Flow<List<BookSourcePart>>
 
 
     @Query(
     @Query(
-        """select * from book_sources 
+        """select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources 
         where enabled = 1 and 
         where enabled = 1 and 
         (bookSourceName like '%' || :searchKey || '%' 
         (bookSourceName like '%' || :searchKey || '%' 
         or bookSourceGroup like '%' || :searchKey || '%' 
         or bookSourceGroup like '%' || :searchKey || '%' 
@@ -33,32 +42,42 @@ interface BookSourceDao {
         or bookSourceComment like '%' || :searchKey || '%')
         or bookSourceComment like '%' || :searchKey || '%')
         order by customOrder asc"""
         order by customOrder asc"""
     )
     )
-    fun flowSearchEnabled(searchKey: String): Flow<List<BookSource>>
+    fun flowSearchEnabled(searchKey: String): Flow<List<BookSourcePart>>
 
 
     @Query(
     @Query(
-        """select * from book_sources 
+        """select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources 
         where bookSourceGroup = :searchKey
         where bookSourceGroup = :searchKey
         or bookSourceGroup like :searchKey || ',%' 
         or bookSourceGroup like :searchKey || ',%' 
         or bookSourceGroup like  '%,' || :searchKey
         or bookSourceGroup like  '%,' || :searchKey
         or bookSourceGroup like  '%,' || :searchKey || ',%' 
         or bookSourceGroup like  '%,' || :searchKey || ',%' 
         order by customOrder asc"""
         order by customOrder asc"""
     )
     )
-    fun flowGroupSearch(searchKey: String): Flow<List<BookSource>>
+    fun flowGroupSearch(searchKey: String): Flow<List<BookSourcePart>>
 
 
-    @Query("select * from book_sources where enabled = 1 order by customOrder asc")
-    fun flowEnabled(): Flow<List<BookSource>>
+    @Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources where enabled = 1 order by customOrder asc""")
+    fun flowEnabled(): Flow<List<BookSourcePart>>
 
 
-    @Query("select * from book_sources where enabled = 0 order by customOrder asc")
-    fun flowDisabled(): Flow<List<BookSource>>
+    @Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources where enabled = 0 order by customOrder asc""")
+    fun flowDisabled(): Flow<List<BookSourcePart>>
 
 
     @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
     @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
     fun flowExplore(): Flow<List<BookSource>>
     fun flowExplore(): Flow<List<BookSource>>
 
 
-    @Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
-    fun flowLogin(): Flow<List<BookSource>>
+    @Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources where loginUrl is not null and loginUrl != ''""")
+    fun flowLogin(): Flow<List<BookSourcePart>>
 
 
-    @Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'")
-    fun flowNoGroup(): Flow<List<BookSource>>
+    @Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
+        trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
+        from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'""")
+    fun flowNoGroup(): Flow<List<BookSourcePart>>
 
 
     @Query(
     @Query(
         """select * from book_sources 
         """select * from book_sources 
@@ -155,12 +174,59 @@ interface BookSourceDao {
     @Query("delete from book_sources where bookSourceUrl = :key")
     @Query("delete from book_sources where bookSourceUrl = :key")
     fun delete(key: String)
     fun delete(key: String)
 
 
+    @Transaction
+    fun delete(bookSources: List<BookSourcePart>) {
+        for (bs in bookSources) {
+            delete(bs.bookSourceUrl)
+        }
+    }
+
     @get:Query("select min(customOrder) from book_sources")
     @get:Query("select min(customOrder) from book_sources")
     val minOrder: Int
     val minOrder: Int
 
 
     @get:Query("select max(customOrder) from book_sources")
     @get:Query("select max(customOrder) from book_sources")
     val maxOrder: Int
     val maxOrder: Int
 
 
+    @Query("update book_sources set enabled = :enable where bookSourceUrl = :bookSourceUrl")
+    fun enable(bookSourceUrl: String, enable: Boolean)
+
+    @Transaction
+    fun enable(enable: Boolean, bookSources: List<BookSourcePart>) {
+        for (bs in bookSources) {
+            enable(bs.bookSourceUrl, enable)
+        }
+    }
+
+    @Query("update book_sources set enabledExplore = :enable where bookSourceUrl = :bookSourceUrl")
+    fun enableExplore(bookSourceUrl: String, enable: Boolean)
+
+    @Transaction
+    fun enableExplore(enable: Boolean, bookSources: List<BookSourcePart>) {
+        for (bs in bookSources) {
+            enableExplore(bs.bookSourceUrl, enable)
+        }
+    }
+
+    @Query("update book_sources set customOrder = :customOrder where bookSourceUrl = :bookSourceUrl")
+    fun upOrder(bookSourceUrl: String, customOrder: Int)
+
+    @Transaction
+    fun upOrder(bookSources: List<BookSourcePart>) {
+        for (bs in bookSources) {
+            upOrder(bs.bookSourceUrl, bs.customOrder)
+        }
+    }
+
+    @Query("update book_sources set bookSourceGroup = :bookSourceGroup where bookSourceUrl = :bookSourceUrl")
+    fun upGroup(bookSourceUrl: String, bookSourceGroup: String)
+
+    @Transaction
+    fun upGroup(bookSources: List<BookSourcePart>) {
+        for (bs in bookSources) {
+            bs.bookSourceGroup?.let { upGroup(bs.bookSourceUrl, it) }
+        }
+    }
+
     private fun dealGroups(list: List<String>): List<String> {
     private fun dealGroups(list: List<String>): List<String> {
         val groups = linkedSetOf<String>()
         val groups = linkedSetOf<String>()
         list.forEach {
         list.forEach {

+ 73 - 0
app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt

@@ -0,0 +1,73 @@
+package io.legado.app.data.entities
+
+import android.text.TextUtils
+import io.legado.app.constant.AppPattern
+import io.legado.app.data.appDb
+import io.legado.app.utils.splitNotBlank
+
+
+data class BookSourcePart(
+    // 地址,包括 http/https
+    var bookSourceUrl: String = "",
+    // 名称
+    var bookSourceName: String = "",
+    // 分组
+    var bookSourceGroup: String? = null,
+    // 手动排序编号
+    var customOrder: Int = 0,
+    // 是否启用
+    var enabled: Boolean = true,
+    // 启用发现
+    var enabledExplore: Boolean = true,
+    // 是否有登录地址
+    var hasLoginUrl: Boolean = false,
+    // 最后更新时间,用于排序
+    var lastUpdateTime: Long = 0,
+    // 响应时间,用于排序
+    var respondTime: Long = 180000L,
+    // 智能排序的权重
+    var weight: Int = 0,
+    // 是否有发现url
+    var hasExploreUrl: Boolean = false
+) {
+
+    override fun hashCode(): Int {
+        return bookSourceUrl.hashCode()
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return if (other is BookSourcePart) other.bookSourceUrl == bookSourceUrl else false
+    }
+
+    fun getDisPlayNameGroup(): String {
+        return if (bookSourceGroup.isNullOrBlank()) {
+            bookSourceName
+        } else {
+            String.format("%s (%s)", bookSourceName, bookSourceGroup)
+        }
+    }
+
+    fun getBookSource(): BookSource? {
+        return appDb.bookSourceDao.getBookSource(bookSourceUrl)
+    }
+
+    fun addGroup(groups: String) {
+        bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
+            it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
+            bookSourceGroup = TextUtils.join(",", it)
+        }
+        if (bookSourceGroup.isNullOrBlank()) bookSourceGroup = groups
+    }
+
+    fun removeGroup(groups: String) {
+        bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
+            it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
+            bookSourceGroup = TextUtils.join(",", it)
+        }
+    }
+
+}
+
+fun List<BookSourcePart>.toBookSource(): List<BookSource> {
+    return mapNotNull { it.getBookSource() }
+}

+ 33 - 20
app/src/main/java/io/legado/app/help/http/BackstageWebView.kt

@@ -13,6 +13,7 @@ import android.webkit.WebViewClient
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
+import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.utils.runOnUI
 import io.legado.app.utils.runOnUI
 import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -80,6 +81,7 @@ class BackstageWebView(
                 } else {
                 } else {
                     webView.loadDataWithBaseURL(url, html, "text/html", getEncoding(), url)
                     webView.loadDataWithBaseURL(url, html, "text/html", getEncoding(), url)
                 }
                 }
+
                 else -> if (headerMap == null) {
                 else -> if (headerMap == null) {
                     webView.loadUrl(url!!)
                     webView.loadUrl(url!!)
                 } else {
                 } else {
@@ -131,10 +133,15 @@ class BackstageWebView(
 
 
     private inner class HtmlWebViewClient : WebViewClient() {
     private inner class HtmlWebViewClient : WebViewClient() {
 
 
+        var runnable: EvalJsRunnable? = null
+
         override fun onPageFinished(view: WebView, url: String) {
         override fun onPageFinished(view: WebView, url: String) {
             setCookie(url)
             setCookie(url)
-            val runnable = EvalJsRunnable(view, url, getJs())
-            mHandler.postDelayed(runnable, 1000)
+            if (runnable == null) {
+                runnable = EvalJsRunnable(view, url, getJs())
+            }
+            mHandler.removeCallbacks(runnable!!)
+            mHandler.postDelayed(runnable!!, 1000)
         }
         }
 
 
         @SuppressLint("WebViewClientOnReceivedSslError")
         @SuppressLint("WebViewClientOnReceivedSslError")
@@ -157,29 +164,34 @@ class BackstageWebView(
         private val mWebView: WeakReference<WebView> = WeakReference(webView)
         private val mWebView: WeakReference<WebView> = WeakReference(webView)
         override fun run() {
         override fun run() {
             mWebView.get()?.evaluateJavascript(mJavaScript) {
             mWebView.get()?.evaluateJavascript(mJavaScript) {
-                if (it.isNotEmpty() && it != "null") {
-                    val content = StringEscapeUtils.unescapeJson(it)
-                        .replace("^\"|\"$".toRegex(), "")
-                    try {
-                        val response = StrResponse(url, content)
-                        callback?.onResult(response)
-                    } catch (e: Exception) {
-                        callback?.onError(e)
-                    }
-                    mHandler.removeCallbacks(this)
+                handleResult(it)
+            }
+        }
+
+        private fun handleResult(result: String) = Coroutine.async {
+            if (result.isNotEmpty() && result != "null") {
+                val content = StringEscapeUtils.unescapeJson(result)
+                    .replace(quoteRegex, "")
+                try {
+                    val response = StrResponse(url, content)
+                    callback?.onResult(response)
+                } catch (e: Exception) {
+                    callback?.onError(e)
+                }
+                mHandler.post {
                     destroy()
                     destroy()
-                    return@evaluateJavascript
                 }
                 }
-                if (retry > 30) {
-                    callback?.onError(NoStackTraceException("js执行超时"))
-                    mHandler.removeCallbacks(this)
+                return@async
+            }
+            if (retry > 30) {
+                callback?.onError(NoStackTraceException("js执行超时"))
+                mHandler.post {
                     destroy()
                     destroy()
-                    return@evaluateJavascript
                 }
                 }
-                retry++
-                mHandler.removeCallbacks(this)
-                mHandler.postDelayed(this, 1000)
+                return@async
             }
             }
+            retry++
+            mHandler.postDelayed(this@EvalJsRunnable, 1000)
         }
         }
     }
     }
 
 
@@ -231,6 +243,7 @@ class BackstageWebView(
 
 
     companion object {
     companion object {
         const val JS = "document.documentElement.outerHTML"
         const val JS = "document.documentElement.outerHTML"
+        private val quoteRegex = "^\"|\"$".toRegex()
     }
     }
 
 
     abstract class Callback {
     abstract class Callback {

+ 2 - 1
app/src/main/java/io/legado/app/model/CheckSource.kt

@@ -4,6 +4,7 @@ import android.content.Context
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.IntentAction
 import io.legado.app.constant.IntentAction
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.help.CacheManager
 import io.legado.app.help.CacheManager
 import io.legado.app.help.IntentData
 import io.legado.app.help.IntentData
 import io.legado.app.service.CheckSourceService
 import io.legado.app.service.CheckSourceService
@@ -22,7 +23,7 @@ object CheckSource {
     var checkContent = CacheManager.get("checkContent")?.toBoolean() ?: true
     var checkContent = CacheManager.get("checkContent")?.toBoolean() ?: true
     val summary get() = upSummary()
     val summary get() = upSummary()
 
 
-    fun start(context: Context, sources: List<BookSource>) {
+    fun start(context: Context, sources: List<BookSourcePart>) {
         val selectedIds = sources.map {
         val selectedIds = sources.map {
             it.bookSourceUrl
             it.bookSourceUrl
         }
         }

+ 12 - 13
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt

@@ -56,18 +56,17 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
     fun explore() {
     fun explore() {
         val source = bookSource
         val source = bookSource
         val url = exploreUrl
         val url = exploreUrl
-        if (source != null && url != null) {
-            WebBook.exploreBook(viewModelScope, source, url, page)
-                .timeout(if (BuildConfig.DEBUG) 0L else 30000L)
-                .onSuccess(IO) { searchBooks ->
-                    booksData.postValue(searchBooks)
-                    appDb.searchBookDao.insert(*searchBooks.toTypedArray())
-                    page++
-                }.onError {
-                    it.printOnDebug()
-                    errorLiveData.postValue(it.stackTraceStr)
-                }
-        }
+        if (source == null || url == null) return
+        WebBook.exploreBook(viewModelScope, source, url, page)
+            .timeout(if (BuildConfig.DEBUG) 0L else 30000L)
+            .onSuccess(IO) { searchBooks ->
+                booksData.postValue(searchBooks)
+                appDb.searchBookDao.insert(*searchBooks.toTypedArray())
+                page++
+            }.onError {
+                it.printOnDebug()
+                errorLiveData.postValue(it.stackTraceStr)
+            }
     }
     }
 
 
-}
+}

+ 6 - 3
app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt

@@ -13,6 +13,7 @@ import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.databinding.DialogSourcePickerBinding
 import io.legado.app.databinding.DialogSourcePickerBinding
 import io.legado.app.databinding.Item1lineTextBinding
 import io.legado.app.databinding.Item1lineTextBinding
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.lib.theme.primaryColor
@@ -81,7 +82,7 @@ class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) {
     }
     }
 
 
     inner class SourceAdapter(context: Context) :
     inner class SourceAdapter(context: Context) :
-        RecyclerAdapter<BookSource, Item1lineTextBinding>(context) {
+        RecyclerAdapter<BookSourcePart, Item1lineTextBinding>(context) {
 
 
         override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding {
         override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding {
             return Item1lineTextBinding.inflate(inflater, parent, false).apply {
             return Item1lineTextBinding.inflate(inflater, parent, false).apply {
@@ -92,7 +93,7 @@ class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) {
         override fun convert(
         override fun convert(
             holder: ItemViewHolder,
             holder: ItemViewHolder,
             binding: Item1lineTextBinding,
             binding: Item1lineTextBinding,
-            item: BookSource,
+            item: BookSourcePart,
             payloads: MutableList<Any>
             payloads: MutableList<Any>
         ) {
         ) {
             binding.textView.text = item.getDisPlayNameGroup()
             binding.textView.text = item.getDisPlayNameGroup()
@@ -101,7 +102,9 @@ class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) {
         override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) {
         override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) {
             binding.root.onClick {
             binding.root.onClick {
                 getItemByLayoutPosition(holder.layoutPosition)?.let {
                 getItemByLayoutPosition(holder.layoutPosition)?.let {
-                    callback?.sourceOnClick(it)
+                    it.getBookSource()?.let { source ->
+                        callback?.sourceOnClick(source)
+                    }
                     dismissAllowingStateLoss()
                     dismissAllowingStateLoss()
                 }
                 }
             }
             }

+ 5 - 0
app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt

@@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.splitNotBlank
 import splitties.init.appCtx
 import splitties.init.appCtx
@@ -20,6 +21,10 @@ data class SearchScope(private var scope: String) {
         "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
         "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
     )
     )
 
 
+    constructor(source: BookSourcePart) : this(
+        "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
+    )
+
     override fun toString(): String {
     override fun toString(): String {
         return scope
         return scope
     }
     }

+ 50 - 18
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -17,6 +17,8 @@ import io.legado.app.constant.AppLog
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.EventBus
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
+import io.legado.app.data.entities.toBookSource
 import io.legado.app.databinding.ActivityBookSourceBinding
 import io.legado.app.databinding.ActivityBookSourceBinding
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.DirectLinkUpload
@@ -143,54 +145,66 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 mode = HandleFileContract.FILE
                 mode = HandleFileContract.FILE
                 allowExtensions = arrayOf("txt", "json")
                 allowExtensions = arrayOf("txt", "json")
             }
             }
+
             R.id.menu_import_onLine -> showImportDialog()
             R.id.menu_import_onLine -> showImportDialog()
             R.id.menu_sort_manual -> {
             R.id.menu_sort_manual -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Default)
                 sortCheck(Sort.Default)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_sort_auto -> {
             R.id.menu_sort_auto -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Weight)
                 sortCheck(Sort.Weight)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_sort_name -> {
             R.id.menu_sort_name -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Name)
                 sortCheck(Sort.Name)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_sort_url -> {
             R.id.menu_sort_url -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Url)
                 sortCheck(Sort.Url)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_sort_time -> {
             R.id.menu_sort_time -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Update)
                 sortCheck(Sort.Update)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_sort_respondTime -> {
             R.id.menu_sort_respondTime -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Respond)
                 sortCheck(Sort.Respond)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_sort_enable -> {
             R.id.menu_sort_enable -> {
                 item.isChecked = true
                 item.isChecked = true
                 sortCheck(Sort.Enable)
                 sortCheck(Sort.Enable)
                 upBookSource(searchView.query?.toString())
                 upBookSource(searchView.query?.toString())
             }
             }
+
             R.id.menu_enabled_group -> {
             R.id.menu_enabled_group -> {
                 searchView.setQuery(getString(R.string.enabled), true)
                 searchView.setQuery(getString(R.string.enabled), true)
             }
             }
+
             R.id.menu_disabled_group -> {
             R.id.menu_disabled_group -> {
                 searchView.setQuery(getString(R.string.disabled), true)
                 searchView.setQuery(getString(R.string.disabled), true)
             }
             }
+
             R.id.menu_group_login -> {
             R.id.menu_group_login -> {
                 searchView.setQuery(getString(R.string.need_login), true)
                 searchView.setQuery(getString(R.string.need_login), true)
             }
             }
+
             R.id.menu_group_null -> {
             R.id.menu_group_null -> {
                 searchView.setQuery(getString(R.string.no_group), true)
                 searchView.setQuery(getString(R.string.no_group), true)
             }
             }
+
             R.id.menu_help -> showHelp()
             R.id.menu_help -> showHelp()
         }
         }
         if (item.groupId == R.id.source_group) {
         if (item.groupId == R.id.source_group) {
@@ -228,22 +242,28 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 searchKey.isNullOrEmpty() -> {
                 searchKey.isNullOrEmpty() -> {
                     appDb.bookSourceDao.flowAll()
                     appDb.bookSourceDao.flowAll()
                 }
                 }
+
                 searchKey == getString(R.string.enabled) -> {
                 searchKey == getString(R.string.enabled) -> {
                     appDb.bookSourceDao.flowEnabled()
                     appDb.bookSourceDao.flowEnabled()
                 }
                 }
+
                 searchKey == getString(R.string.disabled) -> {
                 searchKey == getString(R.string.disabled) -> {
                     appDb.bookSourceDao.flowDisabled()
                     appDb.bookSourceDao.flowDisabled()
                 }
                 }
+
                 searchKey == getString(R.string.need_login) -> {
                 searchKey == getString(R.string.need_login) -> {
                     appDb.bookSourceDao.flowLogin()
                     appDb.bookSourceDao.flowLogin()
                 }
                 }
+
                 searchKey == getString(R.string.no_group) -> {
                 searchKey == getString(R.string.no_group) -> {
                     appDb.bookSourceDao.flowNoGroup()
                     appDb.bookSourceDao.flowNoGroup()
                 }
                 }
+
                 searchKey.startsWith("group:") -> {
                 searchKey.startsWith("group:") -> {
                     val key = searchKey.substringAfter("group:")
                     val key = searchKey.substringAfter("group:")
                     appDb.bookSourceDao.flowGroupSearch(key)
                     appDb.bookSourceDao.flowGroupSearch(key)
                 }
                 }
+
                 else -> {
                 else -> {
                     appDb.bookSourceDao.flowSearch(searchKey)
                     appDb.bookSourceDao.flowSearch(searchKey)
                 }
                 }
@@ -253,6 +273,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                     Sort.Name -> data.sortedWith { o1, o2 ->
                     Sort.Name -> data.sortedWith { o1, o2 ->
                         o1.bookSourceName.cnCompare(o2.bookSourceName)
                         o1.bookSourceName.cnCompare(o2.bookSourceName)
                     }
                     }
+
                     Sort.Url -> data.sortedBy { it.bookSourceUrl }
                     Sort.Url -> data.sortedBy { it.bookSourceUrl }
                     Sort.Update -> data.sortedByDescending { it.lastUpdateTime }
                     Sort.Update -> data.sortedByDescending { it.lastUpdateTime }
                     Sort.Respond -> data.sortedBy { it.respondTime }
                     Sort.Respond -> data.sortedBy { it.respondTime }
@@ -263,6 +284,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                         }
                         }
                         sort
                         sort
                     }
                     }
+
                     else -> data
                     else -> data
                 }
                 }
                 else when (sort) {
                 else when (sort) {
@@ -270,6 +292,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                     Sort.Name -> data.sortedWith { o1, o2 ->
                     Sort.Name -> data.sortedWith { o1, o2 ->
                         o2.bookSourceName.cnCompare(o1.bookSourceName)
                         o2.bookSourceName.cnCompare(o1.bookSourceName)
                     }
                     }
+
                     Sort.Url -> data.sortedByDescending { it.bookSourceUrl }
                     Sort.Url -> data.sortedByDescending { it.bookSourceUrl }
                     Sort.Update -> data.sortedBy { it.lastUpdateTime }
                     Sort.Update -> data.sortedBy { it.lastUpdateTime }
                     Sort.Respond -> data.sortedByDescending { it.respondTime }
                     Sort.Respond -> data.sortedByDescending { it.respondTime }
@@ -280,6 +303,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                         }
                         }
                         sort
                         sort
                     }
                     }
+
                     else -> data.reversed()
                     else -> data.reversed()
                 }
                 }
             }.catch {
             }.catch {
@@ -330,7 +354,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
 
 
     override fun onClickSelectBarMainAction() {
     override fun onClickSelectBarMainAction() {
         alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
         alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
-            yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
+            yesButton { viewModel.del(adapter.selection) }
             noButton()
             noButton()
         }
         }
     }
     }
@@ -353,7 +377,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
             R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray())
             R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray())
             R.id.menu_add_group -> selectionAddToGroups()
             R.id.menu_add_group -> selectionAddToGroups()
             R.id.menu_remove_group -> selectionRemoveFromGroups()
             R.id.menu_remove_group -> selectionRemoveFromGroups()
-            R.id.menu_export_selection -> viewModel.saveToFile(adapter.selection) { file ->
+            R.id.menu_export_selection -> viewModel.saveToFile(adapter.selection.toBookSource()) { file ->
                 exportDir.launch {
                 exportDir.launch {
                     mode = HandleFileContract.EXPORT
                     mode = HandleFileContract.EXPORT
                     fileData = HandleFileContract.FileData(
                     fileData = HandleFileContract.FileData(
@@ -363,9 +387,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                     )
                     )
                 }
                 }
             }
             }
-            R.id.menu_share_source -> viewModel.saveToFile(adapter.selection) {
+
+            R.id.menu_share_source -> viewModel.saveToFile(adapter.selection.toBookSource()) {
                 share(it)
                 share(it)
             }
             }
+
             R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
             R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
         }
         }
         return true
         return true
@@ -386,9 +412,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                         CheckSource.keyword = it
                         CheckSource.keyword = it
                     }
                     }
                 }
                 }
-                CheckSource.start(this@BookSourceActivity, adapter.selection)
-                val firstItem = adapter.getItems().indexOf(adapter.selection.firstOrNull())
-                val lastItem = adapter.getItems().indexOf(adapter.selection.lastOrNull())
+                val selectItems = adapter.selection
+                CheckSource.start(this@BookSourceActivity, selectItems)
+                val adapterItems = adapter.getItems()
+                val firstItem = adapterItems.indexOf(selectItems.firstOrNull())
+                val lastItem = adapterItems.indexOf(selectItems.lastOrNull())
                 Debug.isChecking = firstItem >= 0 && lastItem >= 0
                 Debug.isChecking = firstItem >= 0 && lastItem >= 0
                 checkMessageRefreshJob(firstItem, lastItem).start()
                 checkMessageRefreshJob(firstItem, lastItem).start()
             }
             }
@@ -587,45 +615,49 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
         return false
         return false
     }
     }
 
 
-    override fun del(bookSource: BookSource) {
+    override fun del(bookSource: BookSourcePart) {
         alert(R.string.draw) {
         alert(R.string.draw) {
             setMessage(getString(R.string.sure_del) + "\n" + bookSource.bookSourceName)
             setMessage(getString(R.string.sure_del) + "\n" + bookSource.bookSourceName)
             noButton()
             noButton()
             yesButton {
             yesButton {
-                viewModel.del(bookSource)
+                viewModel.del(listOf(bookSource))
             }
             }
         }
         }
     }
     }
 
 
-    override fun update(vararg bookSource: BookSource) {
-        viewModel.update(*bookSource)
-    }
-
-    override fun edit(bookSource: BookSource) {
+    override fun edit(bookSource: BookSourcePart) {
         startActivity<BookSourceEditActivity> {
         startActivity<BookSourceEditActivity> {
             putExtra("sourceUrl", bookSource.bookSourceUrl)
             putExtra("sourceUrl", bookSource.bookSourceUrl)
         }
         }
     }
     }
 
 
-    override fun upOrder(items: List<BookSource>) {
+    override fun upOrder(items: List<BookSourcePart>) {
         viewModel.upOrder(items)
         viewModel.upOrder(items)
     }
     }
 
 
-    override fun toTop(bookSource: BookSource) {
+    override fun enable(enable: Boolean, bookSource: BookSourcePart) {
+        viewModel.enable(enable, listOf(bookSource))
+    }
+
+    override fun enableExplore(enable: Boolean, bookSource: BookSourcePart) {
+        viewModel.enableExplore(enable, listOf(bookSource))
+    }
+
+    override fun toTop(bookSource: BookSourcePart) {
         viewModel.topSource(bookSource)
         viewModel.topSource(bookSource)
     }
     }
 
 
-    override fun toBottom(bookSource: BookSource) {
+    override fun toBottom(bookSource: BookSourcePart) {
         viewModel.bottomSource(bookSource)
         viewModel.bottomSource(bookSource)
     }
     }
 
 
-    override fun searchBook(bookSource: BookSource) {
+    override fun searchBook(bookSource: BookSourcePart) {
         startActivity<SearchActivity> {
         startActivity<SearchActivity> {
             putExtra("searchScope", SearchScope(bookSource).toString())
             putExtra("searchScope", SearchScope(bookSource).toString())
         }
         }
     }
     }
 
 
-    override fun debug(bookSource: BookSource) {
+    override fun debug(bookSource: BookSourcePart) {
         startActivity<BookSourceDebugActivity> {
         startActivity<BookSourceDebugActivity> {
             putExtra("key", bookSource.bookSourceUrl)
             putExtra("key", bookSource.bookSourceUrl)
         }
         }

+ 40 - 32
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt

@@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.base.adapter.RecyclerAdapter
-import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.databinding.ItemBookSourceBinding
 import io.legado.app.databinding.ItemBookSourceBinding
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.lib.theme.backgroundColor
 import io.legado.app.model.Debug
 import io.legado.app.model.Debug
@@ -28,34 +28,34 @@ import java.util.*
 
 
 
 
 class BookSourceAdapter(context: Context, val callBack: CallBack) :
 class BookSourceAdapter(context: Context, val callBack: CallBack) :
-    RecyclerAdapter<BookSource, ItemBookSourceBinding>(context),
+    RecyclerAdapter<BookSourcePart, ItemBookSourceBinding>(context),
     ItemTouchCallback.Callback {
     ItemTouchCallback.Callback {
 
 
-    private val selected = linkedSetOf<BookSource>()
+    private val selected = linkedSetOf<BookSourcePart>()
     private val finalMessageRegex = Regex("成功|失败")
     private val finalMessageRegex = Regex("成功|失败")
 
 
-    val selection: List<BookSource>
+    val selection: List<BookSourcePart>
         get() {
         get() {
             return getItems().filter {
             return getItems().filter {
                 selected.contains(it)
                 selected.contains(it)
             }
             }
         }
         }
 
 
-    val diffItemCallback = object : DiffUtil.ItemCallback<BookSource>() {
+    val diffItemCallback = object : DiffUtil.ItemCallback<BookSourcePart>() {
 
 
-        override fun areItemsTheSame(oldItem: BookSource, newItem: BookSource): Boolean {
+        override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
             return oldItem.bookSourceUrl == newItem.bookSourceUrl
             return oldItem.bookSourceUrl == newItem.bookSourceUrl
         }
         }
 
 
-        override fun areContentsTheSame(oldItem: BookSource, newItem: BookSource): Boolean {
+        override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
             return oldItem.bookSourceName == newItem.bookSourceName
             return oldItem.bookSourceName == newItem.bookSourceName
                     && oldItem.bookSourceGroup == newItem.bookSourceGroup
                     && oldItem.bookSourceGroup == newItem.bookSourceGroup
                     && oldItem.enabled == newItem.enabled
                     && oldItem.enabled == newItem.enabled
                     && oldItem.enabledExplore == newItem.enabledExplore
                     && oldItem.enabledExplore == newItem.enabledExplore
-                    && oldItem.exploreUrl == newItem.exploreUrl
+                    && oldItem.hasExploreUrl == newItem.hasExploreUrl
         }
         }
 
 
-        override fun getChangePayload(oldItem: BookSource, newItem: BookSource): Any? {
+        override fun getChangePayload(oldItem: BookSourcePart, newItem: BookSourcePart): Any? {
             val payload = Bundle()
             val payload = Bundle()
             if (oldItem.bookSourceName != newItem.bookSourceName
             if (oldItem.bookSourceName != newItem.bookSourceName
                 || oldItem.bookSourceGroup != newItem.bookSourceGroup
                 || oldItem.bookSourceGroup != newItem.bookSourceGroup
@@ -66,7 +66,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
                 payload.putBoolean("enabled", newItem.enabled)
                 payload.putBoolean("enabled", newItem.enabled)
             }
             }
             if (oldItem.enabledExplore != newItem.enabledExplore ||
             if (oldItem.enabledExplore != newItem.enabledExplore ||
-                oldItem.exploreUrl != newItem.exploreUrl
+                oldItem.hasExploreUrl != newItem.hasExploreUrl
             ) {
             ) {
                 payload.putBoolean("upExplore", true)
                 payload.putBoolean("upExplore", true)
             }
             }
@@ -85,7 +85,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
     override fun convert(
     override fun convert(
         holder: ItemViewHolder,
         holder: ItemViewHolder,
         binding: ItemBookSourceBinding,
         binding: ItemBookSourceBinding,
-        item: BookSource,
+        item: BookSourcePart,
         payloads: MutableList<Any>
         payloads: MutableList<Any>
     ) {
     ) {
         binding.run {
         binding.run {
@@ -117,7 +117,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
                 getItem(holder.layoutPosition)?.let {
                 getItem(holder.layoutPosition)?.let {
                     if (view.isPressed) {
                     if (view.isPressed) {
                         it.enabled = checked
                         it.enabled = checked
-                        callBack.update(it)
+                        callBack.enable(checked, it)
                     }
                     }
                 }
                 }
             }
             }
@@ -153,7 +153,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         val popupMenu = PopupMenu(context, view)
         val popupMenu = PopupMenu(context, view)
         popupMenu.inflate(R.menu.book_source_item)
         popupMenu.inflate(R.menu.book_source_item)
         val qyMenu = popupMenu.menu.findItem(R.id.menu_enable_explore)
         val qyMenu = popupMenu.menu.findItem(R.id.menu_enable_explore)
-        if (source.exploreUrl.isNullOrEmpty()) {
+        if (!source.hasExploreUrl) {
             qyMenu.isVisible = false
             qyMenu.isVisible = false
         } else {
         } else {
             if (source.enabledExplore) {
             if (source.enabledExplore) {
@@ -163,7 +163,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
             }
             }
         }
         }
         val loginMenu = popupMenu.menu.findItem(R.id.menu_login)
         val loginMenu = popupMenu.menu.findItem(R.id.menu_login)
-        loginMenu.isVisible = !source.loginUrl.isNullOrBlank()
+        loginMenu.isVisible = source.hasLoginUrl
         popupMenu.setOnMenuItemClickListener { menuItem ->
         popupMenu.setOnMenuItemClickListener { menuItem ->
             when (menuItem.itemId) {
             when (menuItem.itemId) {
                 R.id.menu_top -> callBack.toTop(source)
                 R.id.menu_top -> callBack.toTop(source)
@@ -172,14 +172,16 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
                     putExtra("type", "bookSource")
                     putExtra("type", "bookSource")
                     putExtra("key", source.bookSourceUrl)
                     putExtra("key", source.bookSourceUrl)
                 }
                 }
+
                 R.id.menu_search -> callBack.searchBook(source)
                 R.id.menu_search -> callBack.searchBook(source)
                 R.id.menu_debug_source -> callBack.debug(source)
                 R.id.menu_debug_source -> callBack.debug(source)
                 R.id.menu_del -> {
                 R.id.menu_del -> {
                     callBack.del(source)
                     callBack.del(source)
                     selected.remove(source)
                     selected.remove(source)
                 }
                 }
+
                 R.id.menu_enable_explore -> {
                 R.id.menu_enable_explore -> {
-                    callBack.update(source.copy(enabledExplore = !source.enabledExplore))
+                    callBack.enableExplore(!source.enabledExplore, source)
                 }
                 }
             }
             }
             true
             true
@@ -187,16 +189,18 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         popupMenu.show()
         popupMenu.show()
     }
     }
 
 
-    private fun upShowExplore(iv: ImageView, source: BookSource) {
+    private fun upShowExplore(iv: ImageView, source: BookSourcePart) {
         when {
         when {
-            source.exploreUrl.isNullOrEmpty() -> {
+            !source.hasExploreUrl -> {
                 iv.invisible()
                 iv.invisible()
             }
             }
+
             source.enabledExplore -> {
             source.enabledExplore -> {
                 iv.setColorFilter(Color.GREEN)
                 iv.setColorFilter(Color.GREEN)
                 iv.visible()
                 iv.visible()
                 iv.contentDescription = context.getString(R.string.tag_explore_enabled)
                 iv.contentDescription = context.getString(R.string.tag_explore_enabled)
             }
             }
+
             else -> {
             else -> {
                 iv.setColorFilter(Color.RED)
                 iv.setColorFilter(Color.RED)
                 iv.visible()
                 iv.visible()
@@ -205,7 +209,10 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         }
         }
     }
     }
 
 
-    private fun upCheckSourceMessage(binding: ItemBookSourceBinding, item: BookSource) = binding.run {
+    private fun upCheckSourceMessage(
+        binding: ItemBookSourceBinding,
+        item: BookSourcePart
+    ) = binding.run {
         val msg = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
         val msg = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
         ivDebugText.text = msg
         ivDebugText.text = msg
         val isEmpty = msg.isEmpty()
         val isEmpty = msg.isEmpty()
@@ -274,7 +281,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         return true
         return true
     }
     }
 
 
-    private val movedItems = hashSetOf<BookSource>()
+    private val movedItems = hashSetOf<BookSourcePart>()
 
 
     override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
     override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
         if (movedItems.isNotEmpty()) {
         if (movedItems.isNotEmpty()) {
@@ -285,19 +292,19 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
             if (movedItems.size > sortNumberSet.size) {
             if (movedItems.size > sortNumberSet.size) {
                 callBack.upOrder(getItems())
                 callBack.upOrder(getItems())
             } else {
             } else {
-                callBack.update(*movedItems.toTypedArray())
+                callBack.upOrder(movedItems.toList())
             }
             }
             movedItems.clear()
             movedItems.clear()
         }
         }
     }
     }
 
 
     val dragSelectCallback: DragSelectTouchHelper.Callback =
     val dragSelectCallback: DragSelectTouchHelper.Callback =
-        object : DragSelectTouchHelper.AdvanceCallback<BookSource>(Mode.ToggleAndReverse) {
-            override fun currentSelectedId(): MutableSet<BookSource> {
+        object : DragSelectTouchHelper.AdvanceCallback<BookSourcePart>(Mode.ToggleAndReverse) {
+            override fun currentSelectedId(): MutableSet<BookSourcePart> {
                 return selected
                 return selected
             }
             }
 
 
-            override fun getItemId(position: Int): BookSource {
+            override fun getItemId(position: Int): BookSourcePart {
                 return getItem(position)!!
                 return getItem(position)!!
             }
             }
 
 
@@ -317,14 +324,15 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         }
         }
 
 
     interface CallBack {
     interface CallBack {
-        fun del(bookSource: BookSource)
-        fun edit(bookSource: BookSource)
-        fun update(vararg bookSource: BookSource)
-        fun toTop(bookSource: BookSource)
-        fun toBottom(bookSource: BookSource)
-        fun searchBook(bookSource: BookSource)
-        fun debug(bookSource: BookSource)
-        fun upOrder(items: List<BookSource>)
+        fun del(bookSource: BookSourcePart)
+        fun edit(bookSource: BookSourcePart)
+        fun toTop(bookSource: BookSourcePart)
+        fun toBottom(bookSource: BookSourcePart)
+        fun searchBook(bookSource: BookSourcePart)
+        fun debug(bookSource: BookSourcePart)
+        fun upOrder(items: List<BookSourcePart>)
+        fun enable(enable: Boolean, bookSource: BookSourcePart)
+        fun enableExplore(enable: Boolean, bookSource: BookSourcePart)
         fun upCountView()
         fun upCountView()
     }
     }
-}
+}

+ 45 - 40
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt

@@ -5,6 +5,7 @@ import android.text.TextUtils
 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.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.help.config.SourceConfig
 import io.legado.app.help.config.SourceConfig
 import io.legado.app.utils.*
 import io.legado.app.utils.*
 import java.io.File
 import java.io.File
@@ -16,31 +17,31 @@ import java.io.FileOutputStream
  */
  */
 class BookSourceViewModel(application: Application) : BaseViewModel(application) {
 class BookSourceViewModel(application: Application) : BaseViewModel(application) {
 
 
-    fun topSource(vararg sources: BookSource) {
+    fun topSource(vararg sources: BookSourcePart) {
         execute {
         execute {
             sources.sortBy { it.customOrder }
             sources.sortBy { it.customOrder }
             val minOrder = appDb.bookSourceDao.minOrder - 1
             val minOrder = appDb.bookSourceDao.minOrder - 1
-            val array = Array(sources.size) {
-                sources[it].copy(customOrder = minOrder - it)
+            val array = sources.mapIndexed { index, it ->
+                it.copy(customOrder = minOrder - index)
             }
             }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.upOrder(array)
         }
         }
     }
     }
 
 
-    fun bottomSource(vararg sources: BookSource) {
+    fun bottomSource(vararg sources: BookSourcePart) {
         execute {
         execute {
             sources.sortBy { it.customOrder }
             sources.sortBy { it.customOrder }
             val maxOrder = appDb.bookSourceDao.maxOrder + 1
             val maxOrder = appDb.bookSourceDao.maxOrder + 1
-            val array = Array(sources.size) {
-                sources[it].copy(customOrder = maxOrder + it)
+            val array = sources.mapIndexed { index, it ->
+                it.copy(customOrder = maxOrder + index)
             }
             }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.upOrder(array)
         }
         }
     }
     }
 
 
-    fun del(vararg sources: BookSource) {
+    fun del(sources: List<BookSourcePart>) {
         execute {
         execute {
-            appDb.bookSourceDao.delete(*sources)
+            appDb.bookSourceDao.delete(sources)
             sources.forEach {
             sources.forEach {
                 SourceConfig.removeSource(it.bookSourceUrl)
                 SourceConfig.removeSource(it.bookSourceUrl)
             }
             }
@@ -51,68 +52,72 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
         execute { appDb.bookSourceDao.update(*bookSource) }
         execute { appDb.bookSourceDao.update(*bookSource) }
     }
     }
 
 
-    fun upOrder(items: List<BookSource>) {
+    fun upOrder(items: List<BookSourcePart>) {
         if (items.isEmpty()) return
         if (items.isEmpty()) return
         execute {
         execute {
             val firstSortNumber = items[0].customOrder
             val firstSortNumber = items[0].customOrder
             items.forEachIndexed { index, bookSource ->
             items.forEachIndexed { index, bookSource ->
                 bookSource.customOrder = firstSortNumber + index
                 bookSource.customOrder = firstSortNumber + index
-                appDb.bookSourceDao.update(bookSource)
             }
             }
+            appDb.bookSourceDao.upOrder(items)
         }
         }
     }
     }
 
 
-    fun enableSelection(sources: List<BookSource>) {
+    fun enable(enable: Boolean, items: List<BookSourcePart>) {
         execute {
         execute {
-            val array = Array(sources.size) {
-                sources[it].copy(enabled = true)
-            }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.enable(enable, items)
         }
         }
     }
     }
 
 
-    fun disableSelection(sources: List<BookSource>) {
+    fun enableSelection(sources: List<BookSourcePart>) {
         execute {
         execute {
-            val array = Array(sources.size) {
-                sources[it].copy(enabled = false)
-            }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.enable(true, sources)
         }
         }
     }
     }
 
 
-    fun enableSelectExplore(sources: List<BookSource>) {
+    fun disableSelection(sources: List<BookSourcePart>) {
         execute {
         execute {
-            val array = Array(sources.size) {
-                sources[it].copy(enabledExplore = true)
-            }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.enable(false, sources)
         }
         }
     }
     }
 
 
-    fun disableSelectExplore(sources: List<BookSource>) {
+    fun enableExplore(enable: Boolean, items: List<BookSourcePart>) {
         execute {
         execute {
-            val array = Array(sources.size) {
-                sources[it].copy(enabledExplore = false)
-            }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.enableExplore(enable, items)
+        }
+    }
+
+    fun enableSelectExplore(sources: List<BookSourcePart>) {
+        execute {
+            appDb.bookSourceDao.enableExplore(true, sources)
         }
         }
     }
     }
 
 
-    fun selectionAddToGroups(sources: List<BookSource>, groups: String) {
+    fun disableSelectExplore(sources: List<BookSourcePart>) {
         execute {
         execute {
-            val array = Array(sources.size) {
-                sources[it].copy().addGroup(groups)
+            appDb.bookSourceDao.enableExplore(false, sources)
+        }
+    }
+
+    fun selectionAddToGroups(sources: List<BookSourcePart>, groups: String) {
+        execute {
+            val array = sources.map {
+                it.copy().apply {
+                    addGroup(groups)
+                }
             }
             }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.upGroup(array)
         }
         }
     }
     }
 
 
-    fun selectionRemoveFromGroups(sources: List<BookSource>, groups: String) {
+    fun selectionRemoveFromGroups(sources: List<BookSourcePart>, groups: String) {
         execute {
         execute {
-            val array = Array(sources.size) {
-                sources[it].copy().removeGroup(groups)
+            val array = sources.map {
+                it.copy().apply {
+                    removeGroup(groups)
+                }
             }
             }
-            appDb.bookSourceDao.update(*array)
+            appDb.bookSourceDao.upGroup(array)
         }
         }
     }
     }
 
 

+ 8 - 0
app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt

@@ -4,6 +4,7 @@ import android.app.Application
 import android.content.Context
 import android.content.Context
 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.help.AppWebDav
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
@@ -34,5 +35,12 @@ class ConfigViewModel(application: Application) : BaseViewModel(application) {
         }
         }
     }
     }
 
 
+    fun shrinkDatabase() {
+        execute {
+            appDb.openHelper.writableDatabase.execSQL("VACUUM")
+        }.onSuccess {
+            context.toastOnUi(R.string.success)
+        }
+    }
 
 
 }
 }

+ 10 - 0
app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt

@@ -135,6 +135,7 @@ class OtherConfigFragment : PreferenceFragment(),
 
 
             PreferKey.clearWebViewData -> clearWebViewData()
             PreferKey.clearWebViewData -> clearWebViewData()
             "localPassword" -> alertLocalPassword()
             "localPassword" -> alertLocalPassword()
+            PreferKey.shrinkDatabase -> shrinkDatabase()
         }
         }
         return super.onPreferenceTreeClick(preference)
         return super.onPreferenceTreeClick(preference)
     }
     }
@@ -234,6 +235,15 @@ class OtherConfigFragment : PreferenceFragment(),
         }
         }
     }
     }
 
 
+    private fun shrinkDatabase() {
+        alert(R.string.sure, R.string.shrink_database) {
+            okButton {
+                viewModel.shrinkDatabase()
+            }
+            noButton()
+        }
+    }
+
     private fun clearWebViewData() {
     private fun clearWebViewData() {
         alert(R.string.clear_webview_data, R.string.sure_del) {
         alert(R.string.clear_webview_data, R.string.sure_del) {
             okButton {
             okButton {

+ 2 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -1110,4 +1110,6 @@
     <string name="custom_export">Custom Export</string>
     <string name="custom_export">Custom Export</string>
     <string name="file_contains_number">The number of chapters contained in each file</string>
     <string name="file_contains_number">The number of chapters contained in each file</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -1113,4 +1113,6 @@
     <string name="custom_export">Custom Export</string>
     <string name="custom_export">Custom Export</string>
     <string name="file_contains_number">The number of chapters contained in each file</string>
     <string name="file_contains_number">The number of chapters contained in each file</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -1113,4 +1113,6 @@
     <string name="custom_export">Custom Export</string>
     <string name="custom_export">Custom Export</string>
     <string name="file_contains_number">The index of chapters contained in each file</string>
     <string name="file_contains_number">The index of chapters contained in each file</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1110,4 +1110,6 @@
     <string name="custom_export">自定義導出</string>
     <string name="custom_export">自定義導出</string>
     <string name="file_contains_number">每個文件包含的章節數量</string>
     <string name="file_contains_number">每個文件包含的章節數量</string>
     <string name="export_chapter_index">需要輸出的章節</string>
     <string name="export_chapter_index">需要輸出的章節</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1112,4 +1112,6 @@
     <string name="custom_export">自定義導出</string>
     <string name="custom_export">自定義導出</string>
     <string name="file_contains_number">每個文件包含的章節數量</string>
     <string name="file_contains_number">每個文件包含的章節數量</string>
     <string name="export_chapter_index">需要輸出的章節</string>
     <string name="export_chapter_index">需要輸出的章節</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values-zh/strings.xml

@@ -1112,4 +1112,6 @@
     <string name="custom_export">自定义导出</string>
     <string name="custom_export">自定义导出</string>
     <string name="file_contains_number">每个文件包含的章节数量</string>
     <string name="file_contains_number">每个文件包含的章节数量</string>
     <string name="export_chapter_index">需要输出的章节</string>
     <string name="export_chapter_index">需要输出的章节</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1113,4 +1113,6 @@
     <string name="custom_export">Custom Export</string>
     <string name="custom_export">Custom Export</string>
     <string name="file_contains_number">The number of chapters contained in each file</string>
     <string name="file_contains_number">The number of chapters contained in each file</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
     <string name="export_chapter_index">The section index that needs to be exported</string>
+    <string name="shrink_database">压缩数据库</string>
+    <string name="shrink_database_summary">减小数据库文件的大小</string>
 </resources>
 </resources>

+ 6 - 0
app/src/main/res/xml/pref_config_other.xml

@@ -156,6 +156,12 @@
             android:title="@string/clear_webview_data"
             android:title="@string/clear_webview_data"
             app:iconSpaceReserved="false" />
             app:iconSpaceReserved="false" />
 
 
+        <io.legado.app.lib.prefs.Preference
+            android:key="shrinkDatabase"
+            android:summary="@string/shrink_database_summary"
+            android:title="@string/shrink_database"
+            app:iconSpaceReserved="false" />
+
         <io.legado.app.lib.prefs.Preference
         <io.legado.app.lib.prefs.Preference
             android:key="threadCount"
             android:key="threadCount"
             android:title="@string/threads_num_title"
             android:title="@string/threads_num_title"