diff --git a/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt index d47781403..fd5288016 100644 --- a/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt +++ b/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.view.LayoutInflater import android.view.ViewGroup +import androidx.core.os.postDelayed import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -160,6 +161,12 @@ abstract class RecyclerAdapter(protected val context: Co onCurrentListChanged() } } + handler.postDelayed(1000) { + if (diffJob?.isCompleted == false) { + diffJob?.cancel() + setItems(items) + } + } } } diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 5cf81230b..931f0191c 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -130,6 +130,7 @@ object PreferKey { const val clearWebViewData = "clearWebViewData" const val onlyLatestBackup = "onlyLatestBackup" const val brightnessVwPos = "brightnessVwPos" + const val shrinkDatabase = "shrinkDatabase" const val cPrimary = "colorPrimary" const val cAccent = "colorAccent" diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt index 4cd47af68..e20d3ceed 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt +++ b/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 io.legado.app.constant.AppPattern 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.splitNotBlank import kotlinx.coroutines.flow.Flow @@ -11,21 +12,29 @@ import kotlinx.coroutines.flow.map @Dao interface BookSourceDao { - @Query("select * from book_sources order by customOrder asc") - fun flowAll(): Flow> + @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> @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 || '%' or bookSourceGroup like '%' || :searchKey || '%' or bookSourceUrl like '%' || :searchKey || '%' or bookSourceComment like '%' || :searchKey || '%' order by customOrder asc""" ) - fun flowSearch(searchKey: String): Flow> + fun flowSearch(searchKey: String): Flow> @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 (bookSourceName like '%' || :searchKey || '%' or bookSourceGroup like '%' || :searchKey || '%' @@ -33,32 +42,42 @@ interface BookSourceDao { or bookSourceComment like '%' || :searchKey || '%') order by customOrder asc""" ) - fun flowSearchEnabled(searchKey: String): Flow> + fun flowSearchEnabled(searchKey: String): Flow> @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 or bookSourceGroup like :searchKey || ',%' or bookSourceGroup like '%,' || :searchKey or bookSourceGroup like '%,' || :searchKey || ',%' order by customOrder asc""" ) - fun flowGroupSearch(searchKey: String): Flow> + fun flowGroupSearch(searchKey: String): Flow> - @Query("select * from book_sources where enabled = 1 order by customOrder asc") - fun flowEnabled(): Flow> + @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> - @Query("select * from book_sources where enabled = 0 order by customOrder asc") - fun flowDisabled(): Flow> + @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> @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc") fun flowExplore(): Flow> - @Query("select * from book_sources where loginUrl is not null and loginUrl != ''") - fun flowLogin(): Flow> + @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> - @Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'") - fun flowNoGroup(): Flow> + @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> @Query( """select * from book_sources @@ -155,12 +174,59 @@ interface BookSourceDao { @Query("delete from book_sources where bookSourceUrl = :key") fun delete(key: String) + @Transaction + fun delete(bookSources: List) { + for (bs in bookSources) { + delete(bs.bookSourceUrl) + } + } + @get:Query("select min(customOrder) from book_sources") val minOrder: Int @get:Query("select max(customOrder) from book_sources") 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) { + 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) { + 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) { + 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) { + for (bs in bookSources) { + bs.bookSourceGroup?.let { upGroup(bs.bookSourceUrl, it) } + } + } + private fun dealGroups(list: List): List { val groups = linkedSetOf() list.forEach { diff --git a/app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt b/app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt new file mode 100644 index 000000000..2cd48c6e1 --- /dev/null +++ b/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.toBookSource(): List { + return mapNotNull { it.getBookSource() } +} diff --git a/app/src/main/java/io/legado/app/help/http/BackstageWebView.kt b/app/src/main/java/io/legado/app/help/http/BackstageWebView.kt index defd7b93f..3dfb8cff2 100644 --- a/app/src/main/java/io/legado/app/help/http/BackstageWebView.kt +++ b/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.exception.NoStackTraceException import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine import io.legado.app.utils.runOnUI import kotlinx.coroutines.Runnable import kotlinx.coroutines.suspendCancellableCoroutine @@ -80,6 +81,7 @@ class BackstageWebView( } else { webView.loadDataWithBaseURL(url, html, "text/html", getEncoding(), url) } + else -> if (headerMap == null) { webView.loadUrl(url!!) } else { @@ -131,10 +133,15 @@ class BackstageWebView( private inner class HtmlWebViewClient : WebViewClient() { + var runnable: EvalJsRunnable? = null + override fun onPageFinished(view: WebView, url: String) { 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") @@ -157,30 +164,35 @@ class BackstageWebView( private val mWebView: WeakReference = WeakReference(webView) override fun run() { 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) - destroy() - return@evaluateJavascript - } - if (retry > 30) { - callback?.onError(NoStackTraceException("js执行超时")) - mHandler.removeCallbacks(this) - destroy() - return@evaluateJavascript - } - retry++ - mHandler.removeCallbacks(this) - mHandler.postDelayed(this, 1000) + 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() + } + return@async + } + if (retry > 30) { + callback?.onError(NoStackTraceException("js执行超时")) + mHandler.post { + destroy() + } + return@async + } + retry++ + mHandler.postDelayed(this@EvalJsRunnable, 1000) + } } private inner class SnifferWebClient : WebViewClient() { @@ -231,6 +243,7 @@ class BackstageWebView( companion object { const val JS = "document.documentElement.outerHTML" + private val quoteRegex = "^\"|\"$".toRegex() } abstract class Callback { diff --git a/app/src/main/java/io/legado/app/model/CheckSource.kt b/app/src/main/java/io/legado/app/model/CheckSource.kt index 887f79550..d30a66918 100644 --- a/app/src/main/java/io/legado/app/model/CheckSource.kt +++ b/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.constant.IntentAction 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.IntentData import io.legado.app.service.CheckSourceService @@ -22,7 +23,7 @@ object CheckSource { var checkContent = CacheManager.get("checkContent")?.toBoolean() ?: true val summary get() = upSummary() - fun start(context: Context, sources: List) { + fun start(context: Context, sources: List) { val selectedIds = sources.map { it.bookSourceUrl } diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt index e54fdac1a..185557136 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt @@ -56,18 +56,17 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application fun explore() { val source = bookSource 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) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt b/app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt index 1be0837dc..b00cbbe18 100644 --- a/app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt +++ b/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.data.appDb 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.Item1lineTextBinding import io.legado.app.lib.theme.primaryColor @@ -81,7 +82,7 @@ class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) { } inner class SourceAdapter(context: Context) : - RecyclerAdapter(context) { + RecyclerAdapter(context) { override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding { return Item1lineTextBinding.inflate(inflater, parent, false).apply { @@ -92,7 +93,7 @@ class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) { override fun convert( holder: ItemViewHolder, binding: Item1lineTextBinding, - item: BookSource, + item: BookSourcePart, payloads: MutableList ) { binding.textView.text = item.getDisPlayNameGroup() @@ -101,7 +102,9 @@ class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker) { override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) { binding.root.onClick { getItemByLayoutPosition(holder.layoutPosition)?.let { - callback?.sourceOnClick(it) + it.getBookSource()?.let { source -> + callback?.sourceOnClick(source) + } dismissAllowingStateLoss() } } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt index 8e8c7b674..6686c8ab6 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt +++ b/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.data.appDb 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.utils.splitNotBlank import splitties.init.appCtx @@ -20,6 +21,10 @@ data class SearchScope(private var scope: String) { "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" ) + constructor(source: BookSourcePart) : this( + "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" + ) + override fun toString(): String { return scope } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index e42a68310..2162314b7 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/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.data.appDb 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.DialogEditTextBinding import io.legado.app.help.DirectLinkUpload @@ -143,54 +145,66 @@ class BookSourceActivity : VMBaseActivity showImportDialog() R.id.menu_sort_manual -> { item.isChecked = true sortCheck(Sort.Default) upBookSource(searchView.query?.toString()) } + R.id.menu_sort_auto -> { item.isChecked = true sortCheck(Sort.Weight) upBookSource(searchView.query?.toString()) } + R.id.menu_sort_name -> { item.isChecked = true sortCheck(Sort.Name) upBookSource(searchView.query?.toString()) } + R.id.menu_sort_url -> { item.isChecked = true sortCheck(Sort.Url) upBookSource(searchView.query?.toString()) } + R.id.menu_sort_time -> { item.isChecked = true sortCheck(Sort.Update) upBookSource(searchView.query?.toString()) } + R.id.menu_sort_respondTime -> { item.isChecked = true sortCheck(Sort.Respond) upBookSource(searchView.query?.toString()) } + R.id.menu_sort_enable -> { item.isChecked = true sortCheck(Sort.Enable) upBookSource(searchView.query?.toString()) } + R.id.menu_enabled_group -> { searchView.setQuery(getString(R.string.enabled), true) } + R.id.menu_disabled_group -> { searchView.setQuery(getString(R.string.disabled), true) } + R.id.menu_group_login -> { searchView.setQuery(getString(R.string.need_login), true) } + R.id.menu_group_null -> { searchView.setQuery(getString(R.string.no_group), true) } + R.id.menu_help -> showHelp() } if (item.groupId == R.id.source_group) { @@ -228,22 +242,28 @@ class BookSourceActivity : VMBaseActivity { appDb.bookSourceDao.flowAll() } + searchKey == getString(R.string.enabled) -> { appDb.bookSourceDao.flowEnabled() } + searchKey == getString(R.string.disabled) -> { appDb.bookSourceDao.flowDisabled() } + searchKey == getString(R.string.need_login) -> { appDb.bookSourceDao.flowLogin() } + searchKey == getString(R.string.no_group) -> { appDb.bookSourceDao.flowNoGroup() } + searchKey.startsWith("group:") -> { val key = searchKey.substringAfter("group:") appDb.bookSourceDao.flowGroupSearch(key) } + else -> { appDb.bookSourceDao.flowSearch(searchKey) } @@ -253,6 +273,7 @@ class BookSourceActivity : VMBaseActivity data.sortedWith { o1, o2 -> o1.bookSourceName.cnCompare(o2.bookSourceName) } + Sort.Url -> data.sortedBy { it.bookSourceUrl } Sort.Update -> data.sortedByDescending { it.lastUpdateTime } Sort.Respond -> data.sortedBy { it.respondTime } @@ -263,6 +284,7 @@ class BookSourceActivity : VMBaseActivity data } else when (sort) { @@ -270,6 +292,7 @@ class BookSourceActivity : VMBaseActivity data.sortedWith { o1, o2 -> o2.bookSourceName.cnCompare(o1.bookSourceName) } + Sort.Url -> data.sortedByDescending { it.bookSourceUrl } Sort.Update -> data.sortedBy { it.lastUpdateTime } Sort.Respond -> data.sortedByDescending { it.respondTime } @@ -280,6 +303,7 @@ class BookSourceActivity : VMBaseActivity data.reversed() } }.catch { @@ -330,7 +354,7 @@ class BookSourceActivity : VMBaseActivity viewModel.bottomSource(*adapter.selection.toTypedArray()) R.id.menu_add_group -> selectionAddToGroups() 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 { mode = HandleFileContract.EXPORT fileData = HandleFileContract.FileData( @@ -363,9 +387,11 @@ class BookSourceActivity : VMBaseActivity viewModel.saveToFile(adapter.selection) { + + R.id.menu_share_source -> viewModel.saveToFile(adapter.selection.toBookSource()) { share(it) } + R.id.menu_check_selected_interval -> adapter.checkSelectedInterval() } return true @@ -386,9 +412,11 @@ class BookSourceActivity : VMBaseActivity= 0 && lastItem >= 0 checkMessageRefreshJob(firstItem, lastItem).start() } @@ -587,45 +615,49 @@ class BookSourceActivity : VMBaseActivity { putExtra("sourceUrl", bookSource.bookSourceUrl) } } - override fun upOrder(items: List) { + override fun upOrder(items: List) { 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) } - override fun toBottom(bookSource: BookSource) { + override fun toBottom(bookSource: BookSourcePart) { viewModel.bottomSource(bookSource) } - override fun searchBook(bookSource: BookSource) { + override fun searchBook(bookSource: BookSourcePart) { startActivity { putExtra("searchScope", SearchScope(bookSource).toString()) } } - override fun debug(bookSource: BookSource) { + override fun debug(bookSource: BookSourcePart) { startActivity { putExtra("key", bookSource.bookSourceUrl) } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt index 7ae9e1a8f..ab2ba1021 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt +++ b/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.base.adapter.ItemViewHolder 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.lib.theme.backgroundColor import io.legado.app.model.Debug @@ -28,34 +28,34 @@ import java.util.* class BookSourceAdapter(context: Context, val callBack: CallBack) : - RecyclerAdapter(context), + RecyclerAdapter(context), ItemTouchCallback.Callback { - private val selected = linkedSetOf() + private val selected = linkedSetOf() private val finalMessageRegex = Regex("成功|失败") - val selection: List + val selection: List get() { return getItems().filter { selected.contains(it) } } - val diffItemCallback = object : DiffUtil.ItemCallback() { + val diffItemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BookSource, newItem: BookSource): Boolean { + override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { 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 && oldItem.bookSourceGroup == newItem.bookSourceGroup && oldItem.enabled == newItem.enabled && 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() if (oldItem.bookSourceName != newItem.bookSourceName || oldItem.bookSourceGroup != newItem.bookSourceGroup @@ -66,7 +66,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : payload.putBoolean("enabled", newItem.enabled) } if (oldItem.enabledExplore != newItem.enabledExplore || - oldItem.exploreUrl != newItem.exploreUrl + oldItem.hasExploreUrl != newItem.hasExploreUrl ) { payload.putBoolean("upExplore", true) } @@ -85,7 +85,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : override fun convert( holder: ItemViewHolder, binding: ItemBookSourceBinding, - item: BookSource, + item: BookSourcePart, payloads: MutableList ) { binding.run { @@ -117,7 +117,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : getItem(holder.layoutPosition)?.let { if (view.isPressed) { 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) popupMenu.inflate(R.menu.book_source_item) val qyMenu = popupMenu.menu.findItem(R.id.menu_enable_explore) - if (source.exploreUrl.isNullOrEmpty()) { + if (!source.hasExploreUrl) { qyMenu.isVisible = false } else { if (source.enabledExplore) { @@ -163,7 +163,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : } } val loginMenu = popupMenu.menu.findItem(R.id.menu_login) - loginMenu.isVisible = !source.loginUrl.isNullOrBlank() + loginMenu.isVisible = source.hasLoginUrl popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.menu_top -> callBack.toTop(source) @@ -172,14 +172,16 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : putExtra("type", "bookSource") putExtra("key", source.bookSourceUrl) } + R.id.menu_search -> callBack.searchBook(source) R.id.menu_debug_source -> callBack.debug(source) R.id.menu_del -> { callBack.del(source) selected.remove(source) } + R.id.menu_enable_explore -> { - callBack.update(source.copy(enabledExplore = !source.enabledExplore)) + callBack.enableExplore(!source.enabledExplore, source) } } true @@ -187,16 +189,18 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : popupMenu.show() } - private fun upShowExplore(iv: ImageView, source: BookSource) { + private fun upShowExplore(iv: ImageView, source: BookSourcePart) { when { - source.exploreUrl.isNullOrEmpty() -> { + !source.hasExploreUrl -> { iv.invisible() } + source.enabledExplore -> { iv.setColorFilter(Color.GREEN) iv.visible() iv.contentDescription = context.getString(R.string.tag_explore_enabled) } + else -> { iv.setColorFilter(Color.RED) 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] ?: "" ivDebugText.text = msg val isEmpty = msg.isEmpty() @@ -274,7 +281,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : return true } - private val movedItems = hashSetOf() + private val movedItems = hashSetOf() override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { if (movedItems.isNotEmpty()) { @@ -285,19 +292,19 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : if (movedItems.size > sortNumberSet.size) { callBack.upOrder(getItems()) } else { - callBack.update(*movedItems.toTypedArray()) + callBack.upOrder(movedItems.toList()) } movedItems.clear() } } val dragSelectCallback: DragSelectTouchHelper.Callback = - object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { - override fun currentSelectedId(): MutableSet { + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { return selected } - override fun getItemId(position: Int): BookSource { + override fun getItemId(position: Int): BookSourcePart { return getItem(position)!! } @@ -317,14 +324,15 @@ class BookSourceAdapter(context: Context, val callBack: 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) + 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) + fun enable(enable: Boolean, bookSource: BookSourcePart) + fun enableExplore(enable: Boolean, bookSource: BookSourcePart) fun upCountView() } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index c95bfa331..73f2f22aa 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/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.data.appDb 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.utils.* import java.io.File @@ -16,31 +17,31 @@ import java.io.FileOutputStream */ class BookSourceViewModel(application: Application) : BaseViewModel(application) { - fun topSource(vararg sources: BookSource) { + fun topSource(vararg sources: BookSourcePart) { execute { sources.sortBy { it.customOrder } 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 { sources.sortBy { it.customOrder } 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) { execute { - appDb.bookSourceDao.delete(*sources) + appDb.bookSourceDao.delete(sources) sources.forEach { SourceConfig.removeSource(it.bookSourceUrl) } @@ -51,68 +52,72 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) execute { appDb.bookSourceDao.update(*bookSource) } } - fun upOrder(items: List) { + fun upOrder(items: List) { if (items.isEmpty()) return execute { val firstSortNumber = items[0].customOrder items.forEachIndexed { index, bookSource -> bookSource.customOrder = firstSortNumber + index - appDb.bookSourceDao.update(bookSource) } + appDb.bookSourceDao.upOrder(items) } } - fun enableSelection(sources: List) { + fun enable(enable: Boolean, items: List) { execute { - val array = Array(sources.size) { - sources[it].copy(enabled = true) - } - appDb.bookSourceDao.update(*array) + appDb.bookSourceDao.enable(enable, items) } } - fun disableSelection(sources: List) { + fun enableSelection(sources: List) { execute { - val array = Array(sources.size) { - sources[it].copy(enabled = false) - } - appDb.bookSourceDao.update(*array) + appDb.bookSourceDao.enable(true, sources) } } - fun enableSelectExplore(sources: List) { + fun disableSelection(sources: List) { execute { - val array = Array(sources.size) { - sources[it].copy(enabledExplore = true) - } - appDb.bookSourceDao.update(*array) + appDb.bookSourceDao.enable(false, sources) } } - fun disableSelectExplore(sources: List) { + fun enableExplore(enable: Boolean, items: List) { execute { - val array = Array(sources.size) { - sources[it].copy(enabledExplore = false) - } - appDb.bookSourceDao.update(*array) + appDb.bookSourceDao.enableExplore(enable, items) } } - fun selectionAddToGroups(sources: List, groups: String) { + fun enableSelectExplore(sources: List) { execute { - val array = Array(sources.size) { - sources[it].copy().addGroup(groups) - } - appDb.bookSourceDao.update(*array) + appDb.bookSourceDao.enableExplore(true, sources) } } - fun selectionRemoveFromGroups(sources: List, groups: String) { + fun disableSelectExplore(sources: List) { execute { - val array = Array(sources.size) { - sources[it].copy().removeGroup(groups) + appDb.bookSourceDao.enableExplore(false, sources) + } + } + + fun selectionAddToGroups(sources: List, groups: String) { + execute { + val array = sources.map { + it.copy().apply { + addGroup(groups) + } } - appDb.bookSourceDao.update(*array) + appDb.bookSourceDao.upGroup(array) + } + } + + fun selectionRemoveFromGroups(sources: List, groups: String) { + execute { + val array = sources.map { + it.copy().apply { + removeGroup(groups) + } + } + appDb.bookSourceDao.upGroup(array) } } diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt index bcf9c8697..103027b85 100644 --- a/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.help.AppWebDav import io.legado.app.help.book.BookHelp 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) + } + } } diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt index 55c5b9897..eb9d040a2 100644 --- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -135,6 +135,7 @@ class OtherConfigFragment : PreferenceFragment(), PreferKey.clearWebViewData -> clearWebViewData() "localPassword" -> alertLocalPassword() + PreferKey.shrinkDatabase -> shrinkDatabase() } 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() { alert(R.string.clear_webview_data, R.string.sure_del) { okButton { diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index ef327d25d..5d22ec44d 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -1110,4 +1110,6 @@ Custom Export The number of chapters contained in each file The section index that needs to be exported + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index a3856efbe..7f88a66b0 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -1113,4 +1113,6 @@ Custom Export The number of chapters contained in each file The section index that needs to be exported + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0735f2ff0..6e7138950 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1113,4 +1113,6 @@ Custom Export The index of chapters contained in each file The section index that needs to be exported + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index ba211f181..af5722d6c 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1110,4 +1110,6 @@ 自定義導出 每個文件包含的章節數量 需要輸出的章節 + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 96926e0cc..672ad5236 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1112,4 +1112,6 @@ 自定義導出 每個文件包含的章節數量 需要輸出的章節 + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index cf8435b52..9e6de4af0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1112,4 +1112,6 @@ 自定义导出 每个文件包含的章节数量 需要输出的章节 + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b04c5ec4..4bf5f948f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1113,4 +1113,6 @@ Custom Export The number of chapters contained in each file The section index that needs to be exported + 压缩数据库 + 减小数据库文件的大小 diff --git a/app/src/main/res/xml/pref_config_other.xml b/app/src/main/res/xml/pref_config_other.xml index c5ac1ce44..9648022e2 100644 --- a/app/src/main/res/xml/pref_config_other.xml +++ b/app/src/main/res/xml/pref_config_other.xml @@ -156,6 +156,12 @@ android:title="@string/clear_webview_data" app:iconSpaceReserved="false" /> + +