优化
This commit is contained in:
@@ -14,7 +14,6 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新
|
* 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新
|
||||||
@@ -91,13 +90,3 @@ suspend fun BookSourcePart.clearExploreKindsCache() {
|
|||||||
exploreKindsMap.remove(exploreKindsKey)
|
exploreKindsMap.remove(exploreKindsKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BookSource.contains(word: String?): Boolean {
|
|
||||||
if (word.isNullOrEmpty()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return bookSourceName.contains(word)
|
|
||||||
|| bookSourceUrl.contains(word)
|
|
||||||
|| bookSourceGroup?.contains(word) == true
|
|
||||||
|| bookSourceComment?.contains(word) == true
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import io.legado.app.constant.AppLog
|
|||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
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.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.book.addType
|
import io.legado.app.help.book.addType
|
||||||
@@ -352,13 +353,14 @@ object WebBook {
|
|||||||
*/
|
*/
|
||||||
fun preciseSearch(
|
fun preciseSearch(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
bookSources: List<BookSource>,
|
bookSourceParts: List<BookSourcePart>,
|
||||||
name: String,
|
name: String,
|
||||||
author: String,
|
author: String,
|
||||||
context: CoroutineContext = Dispatchers.IO,
|
context: CoroutineContext = Dispatchers.IO,
|
||||||
): Coroutine<Pair<Book, BookSource>> {
|
): Coroutine<Pair<Book, BookSource>> {
|
||||||
return Coroutine.async(scope, context) {
|
return Coroutine.async(scope, context) {
|
||||||
for (source in bookSources) {
|
for (s in bookSourceParts) {
|
||||||
|
val source = s.getBookSource() ?: continue
|
||||||
val book = preciseSearchAwait(scope, source, name, author).getOrNull()
|
val book = preciseSearchAwait(scope, source, name, author).getOrNull()
|
||||||
if (book != null) {
|
if (book != null) {
|
||||||
return@async Pair(book, source)
|
return@async Pair(book, source)
|
||||||
|
|||||||
@@ -6,30 +6,39 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
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.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
import io.legado.app.utils.mapParallelSafe
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import java.util.*
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.util.Collections
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
|
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
|
||||||
private val threadCount = AppConfig.threadCount
|
private val threadCount = AppConfig.threadCount
|
||||||
private var searchPool: ExecutorCoroutineDispatcher? = null
|
private var searchPool: ExecutorCoroutineDispatcher? = null
|
||||||
private val tasks = CompositeCoroutine()
|
|
||||||
private var searchSuccess: ((SearchBook) -> Unit)? = null
|
private var searchSuccess: ((SearchBook) -> Unit)? = null
|
||||||
private var upAdapter: (() -> Unit)? = null
|
private var upAdapter: (() -> Unit)? = null
|
||||||
private var bookSourceList = arrayListOf<BookSource>()
|
private var bookSourceParts = arrayListOf<BookSourcePart>()
|
||||||
private val defaultCover by lazy {
|
private val defaultCover by lazy {
|
||||||
listOf(
|
listOf(
|
||||||
SearchBook(
|
SearchBook(
|
||||||
@@ -40,6 +49,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
private var task: Job? = null
|
||||||
val searchStateData = MutableLiveData<Boolean>()
|
val searchStateData = MutableLiveData<Boolean>()
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
var author: String = ""
|
var author: String = ""
|
||||||
@@ -73,9 +83,6 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
|
|||||||
}
|
}
|
||||||
}.flowOn(IO)
|
}.flowOn(IO)
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var searchIndex = -1
|
|
||||||
|
|
||||||
fun initData(arguments: Bundle?) {
|
fun initData(arguments: Bundle?) {
|
||||||
arguments?.let { bundle ->
|
arguments?.let { bundle ->
|
||||||
bundle.getString("name")?.let {
|
bundle.getString("name")?.let {
|
||||||
@@ -90,7 +97,6 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
|
|||||||
private fun initSearchPool() {
|
private fun initSearchPool() {
|
||||||
searchPool = Executors
|
searchPool = Executors
|
||||||
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
|
||||||
searchIndex = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startSearch() {
|
private fun startSearch() {
|
||||||
@@ -98,71 +104,50 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
|
|||||||
stopSearch()
|
stopSearch()
|
||||||
searchBooks.clear()
|
searchBooks.clear()
|
||||||
upAdapter?.invoke()
|
upAdapter?.invoke()
|
||||||
bookSourceList.clear()
|
bookSourceParts.clear()
|
||||||
bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
|
bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart)
|
||||||
searchStateData.postValue(true)
|
|
||||||
initSearchPool()
|
initSearchPool()
|
||||||
for (i in 0 until threadCount) {
|
search()
|
||||||
search()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun search() {
|
private fun search() {
|
||||||
if (searchIndex >= bookSourceList.lastIndex) {
|
task = viewModelScope.launch(searchPool!!) {
|
||||||
return
|
flow {
|
||||||
}
|
for (bs in bookSourceParts) {
|
||||||
searchIndex++
|
bs.getBookSource()?.let {
|
||||||
val source = bookSourceList[searchIndex]
|
emit(it)
|
||||||
if (source.getSearchRule().coverUrl.isNullOrBlank()) {
|
|
||||||
searchNext()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val task = WebBook
|
|
||||||
.searchBook(
|
|
||||||
viewModelScope,
|
|
||||||
source,
|
|
||||||
name,
|
|
||||||
context = searchPool!!,
|
|
||||||
executeContext = searchPool!!
|
|
||||||
)
|
|
||||||
.timeout(60000L)
|
|
||||||
.onSuccess(IO) {
|
|
||||||
it.firstOrNull()?.let { searchBook ->
|
|
||||||
if (searchBook.name == name && searchBook.author == author
|
|
||||||
&& !searchBook.coverUrl.isNullOrEmpty()
|
|
||||||
) {
|
|
||||||
appDb.searchBookDao.insert(searchBook)
|
|
||||||
searchSuccess?.invoke(searchBook)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.onStart {
|
||||||
.onFinally {
|
searchStateData.postValue(true)
|
||||||
searchNext()
|
}.mapParallelSafe(threadCount) {
|
||||||
}
|
withTimeout(60000L) {
|
||||||
tasks.add(task)
|
search(it)
|
||||||
|
}
|
||||||
|
}.onCompletion {
|
||||||
|
searchStateData.postValue(false)
|
||||||
|
}.catch {
|
||||||
|
AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it)
|
||||||
|
}.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
private suspend fun search(source: BookSource) {
|
||||||
private fun searchNext() {
|
if (source.getSearchRule().coverUrl.isNullOrBlank()) {
|
||||||
if (searchIndex < bookSourceList.lastIndex) {
|
return
|
||||||
search()
|
|
||||||
} else {
|
|
||||||
searchIndex++
|
|
||||||
}
|
}
|
||||||
if (searchIndex >= bookSourceList.lastIndex + min(
|
val searchBook = WebBook.searchBookAwait(source, name).firstOrNull() ?: return
|
||||||
bookSourceList.size,
|
if (searchBook.name == name && searchBook.author == author
|
||||||
threadCount
|
&& !searchBook.coverUrl.isNullOrEmpty()
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
searchStateData.postValue(false)
|
appDb.searchBookDao.insert(searchBook)
|
||||||
tasks.clear()
|
searchSuccess?.invoke(searchBook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startOrStopSearch() {
|
fun startOrStopSearch() {
|
||||||
if (tasks.isEmpty) {
|
if (task == null || !task!!.isActive) {
|
||||||
startSearch()
|
startSearch()
|
||||||
} else {
|
} else {
|
||||||
stopSearch()
|
stopSearch()
|
||||||
@@ -170,7 +155,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSearch() {
|
private fun stopSearch() {
|
||||||
tasks.clear()
|
task?.cancel()
|
||||||
searchPool?.close()
|
searchPool?.close()
|
||||||
searchStateData.postValue(false)
|
searchStateData.postValue(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,27 +10,34 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseDialogFragment
|
import io.legado.app.base.BaseDialogFragment
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
import io.legado.app.base.adapter.ItemViewHolder
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.data.AppDatabase
|
||||||
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.BookSourcePart
|
||||||
import io.legado.app.databinding.DialogSearchScopeBinding
|
import io.legado.app.databinding.DialogSearchScopeBinding
|
||||||
import io.legado.app.databinding.ItemCheckBoxBinding
|
import io.legado.app.databinding.ItemCheckBoxBinding
|
||||||
import io.legado.app.databinding.ItemRadioButtonBinding
|
import io.legado.app.databinding.ItemRadioButtonBinding
|
||||||
import io.legado.app.help.source.contains
|
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.utils.applyTint
|
import io.legado.app.utils.applyTint
|
||||||
|
import io.legado.app.utils.flowWithLifecycleAndDatabaseChange
|
||||||
import io.legado.app.utils.setLayout
|
import io.legado.app.utils.setLayout
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
||||||
|
|
||||||
private val binding by viewBinding(DialogSearchScopeBinding::bind)
|
private val binding by viewBinding(DialogSearchScopeBinding::bind)
|
||||||
|
private var sourceFlowJob: Job? = null
|
||||||
val callback: Callback get() = parentFragment as? Callback ?: activity as Callback
|
val callback: Callback get() = parentFragment as? Callback ?: activity as Callback
|
||||||
var groups: List<String> = emptyList()
|
var groups: List<String> = emptyList()
|
||||||
var sources: List<BookSource> = emptyList()
|
val screenSources = arrayListOf<BookSourcePart>()
|
||||||
val screenSources = arrayListOf<BookSource>()
|
|
||||||
var screenText: String? = null
|
var screenText: String? = null
|
||||||
|
|
||||||
val adapter by lazy {
|
val adapter by lazy {
|
||||||
@@ -49,7 +56,6 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
initSearchView()
|
initSearchView()
|
||||||
initOtherView()
|
initOtherView()
|
||||||
initData()
|
initData()
|
||||||
upData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMenu() {
|
private fun initMenu() {
|
||||||
@@ -105,34 +111,49 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
groups = withContext(IO) {
|
groups = withContext(IO) {
|
||||||
appDb.bookSourceDao.allEnabledGroups()
|
appDb.bookSourceDao.allEnabledGroups()
|
||||||
}
|
}
|
||||||
sources = withContext(IO) {
|
|
||||||
appDb.bookSourceDao.allEnabled
|
|
||||||
}
|
|
||||||
upData()
|
upData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun upData() {
|
private fun upData() {
|
||||||
lifecycleScope.launch {
|
if (binding.rbSource.isChecked) {
|
||||||
withContext(IO) {
|
upBookSource(screenText)
|
||||||
if (binding.rbSource.isChecked) {
|
} else {
|
||||||
sources.filter { source ->
|
|
||||||
source.contains(screenText)
|
|
||||||
}.let {
|
|
||||||
screenSources.clear()
|
|
||||||
screenSources.addAll(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
private fun upBookSource(searchKey: String? = null) {
|
||||||
|
sourceFlowJob?.cancel()
|
||||||
|
sourceFlowJob = lifecycleScope.launch {
|
||||||
|
when {
|
||||||
|
searchKey.isNullOrEmpty() -> {
|
||||||
|
appDb.bookSourceDao.flowAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
appDb.bookSourceDao.flowSearch(searchKey)
|
||||||
|
}
|
||||||
|
}.flowWithLifecycleAndDatabaseChange(
|
||||||
|
lifecycle,
|
||||||
|
table = AppDatabase.BOOK_SOURCE_TABLE_NAME
|
||||||
|
).catch {
|
||||||
|
AppLog.put("多分组/书源界面更新书源出错", it)
|
||||||
|
}.flowOn(IO).conflate().collect { data ->
|
||||||
|
screenSources.clear()
|
||||||
|
screenSources.addAll(data)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner class RecyclerAdapter : RecyclerView.Adapter<ItemViewHolder>() {
|
inner class RecyclerAdapter : RecyclerView.Adapter<ItemViewHolder>() {
|
||||||
|
|
||||||
val selectGroups = arrayListOf<String>()
|
val selectGroups = arrayListOf<String>()
|
||||||
var selectSource: BookSource? = null
|
var selectSource: BookSourcePart? = null
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return if (binding.rbSource.isChecked) {
|
return if (binding.rbSource.isChecked) {
|
||||||
@@ -165,6 +186,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
holder.binding.checkBox.text = it
|
holder.binding.checkBox.text = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ItemRadioButtonBinding -> {
|
is ItemRadioButtonBinding -> {
|
||||||
screenSources.getOrNull(position)?.let {
|
screenSources.getOrNull(position)?.let {
|
||||||
holder.binding.radioButton.isChecked = selectSource == it
|
holder.binding.radioButton.isChecked = selectSource == it
|
||||||
@@ -195,6 +217,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ItemRadioButtonBinding -> {
|
is ItemRadioButtonBinding -> {
|
||||||
screenSources.getOrNull(position)?.let {
|
screenSources.getOrNull(position)?.let {
|
||||||
holder.binding.radioButton.isChecked = selectSource == it
|
holder.binding.radioButton.isChecked = selectSource == it
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ import kotlinx.coroutines.isActive
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import kotlin.collections.List
|
|
||||||
import kotlin.collections.Map
|
|
||||||
import kotlin.collections.forEach
|
|
||||||
import kotlin.collections.hashMapOf
|
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
|
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
|
||||||
val addBookProgressLiveData = MutableLiveData(-1)
|
val addBookProgressLiveData = MutableLiveData(-1)
|
||||||
@@ -156,13 +151,13 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
|||||||
|
|
||||||
private fun importBookshelfByJson(json: String, groupId: Long) {
|
private fun importBookshelfByJson(json: String, groupId: Long) {
|
||||||
execute {
|
execute {
|
||||||
val bookSources = appDb.bookSourceDao.allEnabled
|
val bookSourceParts = appDb.bookSourceDao.allEnabledPart
|
||||||
GSON.fromJsonArray<Map<String, String?>>(json).getOrThrow().forEach { bookInfo ->
|
GSON.fromJsonArray<Map<String, String?>>(json).getOrThrow().forEach { bookInfo ->
|
||||||
if (!isActive) return@execute
|
if (!isActive) return@execute
|
||||||
val name = bookInfo["name"] ?: ""
|
val name = bookInfo["name"] ?: ""
|
||||||
val author = bookInfo["author"] ?: ""
|
val author = bookInfo["author"] ?: ""
|
||||||
if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) {
|
if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) {
|
||||||
WebBook.preciseSearch(this, bookSources, name, author)
|
WebBook.preciseSearch(this, bookSourceParts, name, author)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
val book = it.first
|
val book = it.first
|
||||||
if (groupId > 0) {
|
if (groupId > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user