Merge remote-tracking branch 'origin/master'

This commit is contained in:
kunfei 2023-06-11 19:08:25 +08:00
commit ba90882998
22 changed files with 399 additions and 148 deletions

View File

@ -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<ITEM, VB : ViewBinding>(protected val context: Co
onCurrentListChanged()
}
}
handler.postDelayed(1000) {
if (diffJob?.isCompleted == false) {
diffJob?.cancel()
setItems(items)
}
}
}
}

View File

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

View File

@ -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<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(
"""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<List<BookSource>>
fun flowSearch(searchKey: String): Flow<List<BookSourcePart>>
@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<List<BookSource>>
fun flowSearchEnabled(searchKey: String): Flow<List<BookSourcePart>>
@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<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")
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(
"""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<BookSourcePart>) {
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<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> {
val groups = linkedSetOf<String>()
list.forEach {

View File

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

View File

@ -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<WebView> = 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 {

View File

@ -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<BookSource>) {
fun start(context: Context, sources: List<BookSourcePart>) {
val selectedIds = sources.map {
it.bookSourceUrl
}

View File

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

View File

@ -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<BookSource, Item1lineTextBinding>(context) {
RecyclerAdapter<BookSourcePart, Item1lineTextBinding>(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<Any>
) {
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()
}
}

View File

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

View File

@ -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<ActivityBookSourceBinding, BookSourceV
mode = HandleFileContract.FILE
allowExtensions = arrayOf("txt", "json")
}
R.id.menu_import_onLine -> 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<ActivityBookSourceBinding, BookSourceV
searchKey.isNullOrEmpty() -> {
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<ActivityBookSourceBinding, BookSourceV
Sort.Name -> 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<ActivityBookSourceBinding, BookSourceV
}
sort
}
else -> data
}
else when (sort) {
@ -270,6 +292,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
Sort.Name -> 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<ActivityBookSourceBinding, BookSourceV
}
sort
}
else -> data.reversed()
}
}.catch {
@ -330,7 +354,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
override fun onClickSelectBarMainAction() {
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
yesButton { viewModel.del(adapter.selection) }
noButton()
}
}
@ -353,7 +377,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
R.id.menu_bottom_sel -> 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<ActivityBookSourceBinding, BookSourceV
)
}
}
R.id.menu_share_source -> 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<ActivityBookSourceBinding, BookSourceV
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
checkMessageRefreshJob(firstItem, lastItem).start()
}
@ -587,45 +615,49 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
return false
}
override fun del(bookSource: BookSource) {
override fun del(bookSource: BookSourcePart) {
alert(R.string.draw) {
setMessage(getString(R.string.sure_del) + "\n" + bookSource.bookSourceName)
noButton()
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> {
putExtra("sourceUrl", bookSource.bookSourceUrl)
}
}
override fun upOrder(items: List<BookSource>) {
override fun upOrder(items: List<BookSourcePart>) {
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<SearchActivity> {
putExtra("searchScope", SearchScope(bookSource).toString())
}
}
override fun debug(bookSource: BookSource) {
override fun debug(bookSource: BookSourcePart) {
startActivity<BookSourceDebugActivity> {
putExtra("key", bookSource.bookSourceUrl)
}

View File

@ -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<BookSource, ItemBookSourceBinding>(context),
RecyclerAdapter<BookSourcePart, ItemBookSourceBinding>(context),
ItemTouchCallback.Callback {
private val selected = linkedSetOf<BookSource>()
private val selected = linkedSetOf<BookSourcePart>()
private val finalMessageRegex = Regex("成功|失败")
val selection: List<BookSource>
val selection: List<BookSourcePart>
get() {
return getItems().filter {
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
}
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<Any>
) {
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<BookSource>()
private val movedItems = hashSetOf<BookSourcePart>()
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<BookSource>(Mode.ToggleAndReverse) {
override fun currentSelectedId(): MutableSet<BookSource> {
object : DragSelectTouchHelper.AdvanceCallback<BookSourcePart>(Mode.ToggleAndReverse) {
override fun currentSelectedId(): MutableSet<BookSourcePart> {
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<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()
}
}
}

View File

@ -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<BookSourcePart>) {
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<BookSource>) {
fun upOrder(items: List<BookSourcePart>) {
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<BookSource>) {
fun enable(enable: Boolean, items: List<BookSourcePart>) {
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 {
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 {
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 {
val array = Array(sources.size) {
sources[it].copy(enabledExplore = false)
}
appDb.bookSourceDao.update(*array)
appDb.bookSourceDao.enableExplore(enable, items)
}
}
fun selectionAddToGroups(sources: List<BookSource>, groups: String) {
fun enableSelectExplore(sources: List<BookSourcePart>) {
execute {
val array = Array(sources.size) {
sources[it].copy().addGroup(groups)
}
appDb.bookSourceDao.update(*array)
appDb.bookSourceDao.enableExplore(true, sources)
}
}
fun selectionRemoveFromGroups(sources: List<BookSource>, groups: String) {
fun disableSelectExplore(sources: List<BookSourcePart>) {
execute {
val array = Array(sources.size) {
sources[it].copy().removeGroup(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<BookSourcePart>, groups: String) {
execute {
val array = sources.map {
it.copy().apply {
removeGroup(groups)
}
}
appDb.bookSourceDao.upGroup(array)
}
}

View File

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

View File

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

View File

@ -1110,4 +1110,6 @@
<string name="custom_export">Custom Export</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="shrink_database">压缩数据库</string>
<string name="shrink_database_summary">减小数据库文件的大小</string>
</resources>

View File

@ -1113,4 +1113,6 @@
<string name="custom_export">Custom Export</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="shrink_database">压缩数据库</string>
<string name="shrink_database_summary">减小数据库文件的大小</string>
</resources>

View File

@ -1113,4 +1113,6 @@
<string name="custom_export">Custom Export</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="shrink_database">压缩数据库</string>
<string name="shrink_database_summary">减小数据库文件的大小</string>
</resources>

View File

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

View File

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

View File

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

View File

@ -1113,4 +1113,6 @@
<string name="custom_export">Custom Export</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="shrink_database">压缩数据库</string>
<string name="shrink_database_summary">减小数据库文件的大小</string>
</resources>

View File

@ -156,6 +156,12 @@
android:title="@string/clear_webview_data"
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
android:key="threadCount"
android:title="@string/threads_num_title"