kunfei 4 лет назад
Родитель
Сommit
0eb80f0cba

+ 122 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt

@@ -0,0 +1,122 @@
+package io.legado.app.ui.book.changesource
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.widget.PopupMenu
+import androidx.recyclerview.widget.DiffUtil
+import io.legado.app.R
+import io.legado.app.base.adapter.DiffRecyclerAdapter
+import io.legado.app.base.adapter.ItemViewHolder
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.databinding.ItemChangeSourceBinding
+import io.legado.app.utils.invisible
+import io.legado.app.utils.visible
+import splitties.views.onLongClick
+
+
+class ChangeChapterSourceAdapter(
+    context: Context,
+    val viewModel: ChangeBookSourceViewModel,
+    val callBack: CallBack
+) : DiffRecyclerAdapter<SearchBook, ItemChangeSourceBinding>(context) {
+
+    override val diffItemCallback = object : DiffUtil.ItemCallback<SearchBook>() {
+        override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean {
+            return oldItem.bookUrl == newItem.bookUrl
+        }
+
+        override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean {
+            return oldItem.originName == newItem.originName
+                && oldItem.getDisplayLastChapterTitle() == newItem.getDisplayLastChapterTitle()
+        }
+
+    }
+
+    override fun getViewBinding(parent: ViewGroup): ItemChangeSourceBinding {
+        return ItemChangeSourceBinding.inflate(inflater, parent, false)
+    }
+
+    override fun convert(
+        holder: ItemViewHolder,
+        binding: ItemChangeSourceBinding,
+        item: SearchBook,
+        payloads: MutableList<Any>
+    ) {
+        val bundle = payloads.getOrNull(0) as? Bundle
+        binding.apply {
+            if (bundle == null) {
+                tvOrigin.text = item.originName
+                tvAuthor.text = item.author
+                tvLast.text = item.getDisplayLastChapterTitle()
+                if (callBack.bookUrl == item.bookUrl) {
+                    ivChecked.visible()
+                } else {
+                    ivChecked.invisible()
+                }
+            } else {
+                bundle.keySet().map {
+                    when (it) {
+                        "name" -> tvOrigin.text = item.originName
+                        "latest" -> tvLast.text = item.getDisplayLastChapterTitle()
+                        "upCurSource" -> if (callBack.bookUrl == item.bookUrl) {
+                            ivChecked.visible()
+                        } else {
+                            ivChecked.invisible()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
+        holder.itemView.setOnClickListener {
+            getItem(holder.layoutPosition)?.let {
+                callBack.changeTo(it)
+            }
+        }
+        holder.itemView.onLongClick {
+            showMenu(holder.itemView, getItem(holder.layoutPosition))
+        }
+    }
+
+    private fun showMenu(view: View, searchBook: SearchBook?) {
+        searchBook ?: return
+        val popupMenu = PopupMenu(context, view)
+        popupMenu.inflate(R.menu.change_source_item)
+        popupMenu.setOnMenuItemClickListener {
+            when (it.itemId) {
+                R.id.menu_top_source -> {
+                    callBack.topSource(searchBook)
+                }
+                R.id.menu_bottom_source -> {
+                    callBack.bottomSource(searchBook)
+                }
+                R.id.menu_edit_source -> {
+                    callBack.editSource(searchBook)
+                }
+                R.id.menu_disable_source -> {
+                    callBack.disableSource(searchBook)
+                }
+                R.id.menu_delete_source -> {
+                    callBack.deleteSource(searchBook)
+                    updateItems(0, itemCount, listOf<Int>())
+                }
+            }
+            true
+        }
+        popupMenu.show()
+    }
+
+    interface CallBack {
+        val bookUrl: String?
+        fun changeTo(searchBook: SearchBook)
+        fun topSource(searchBook: SearchBook)
+        fun bottomSource(searchBook: SearchBook)
+        fun editSource(searchBook: SearchBook)
+        fun disableSource(searchBook: SearchBook)
+        fun deleteSource(searchBook: SearchBook)
+    }
+}

+ 287 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt

@@ -0,0 +1,287 @@
+package io.legado.app.ui.book.changesource
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.widget.SearchView
+import androidx.appcompat.widget.Toolbar
+import androidx.core.os.bundleOf
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import io.legado.app.R
+import io.legado.app.base.BaseDialogFragment
+import io.legado.app.constant.AppPattern
+import io.legado.app.constant.EventBus
+import io.legado.app.constant.PreferKey
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.databinding.DialogChangeSourceBinding
+import io.legado.app.help.AppConfig
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.book.source.edit.BookSourceEditActivity
+import io.legado.app.ui.book.source.manage.BookSourceActivity
+import io.legado.app.ui.widget.recycler.VerticalDivider
+import io.legado.app.utils.*
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+
+class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_change_source),
+    Toolbar.OnMenuItemClickListener,
+    ChangeBookSourceAdapter.CallBack {
+
+    constructor(name: String, author: String) : this() {
+        arguments = Bundle().apply {
+            putString("name", name)
+            putString("author", author)
+        }
+    }
+
+    private val binding by viewBinding(DialogChangeSourceBinding::bind)
+    private val groups = linkedSetOf<String>()
+    private val callBack: CallBack? get() = activity as? CallBack
+    private val viewModel: ChangeBookSourceViewModel by viewModels()
+    private val adapter by lazy { ChangeBookSourceAdapter(requireContext(), viewModel, this) }
+    private val editSourceResult =
+        registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
+            viewModel.startSearch()
+        }
+
+    override fun onStart() {
+        super.onStart()
+        setLayout(0.9f, 0.9f)
+    }
+
+    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
+        binding.toolBar.setBackgroundColor(primaryColor)
+        viewModel.initData(arguments)
+        showTitle()
+        initMenu()
+        initRecyclerView()
+        initSearchView()
+        initLiveData()
+    }
+
+    private fun showTitle() {
+        binding.toolBar.title = viewModel.name
+        binding.toolBar.subtitle = viewModel.author
+    }
+
+    private fun initMenu() {
+        binding.toolBar.inflateMenu(R.menu.change_source)
+        binding.toolBar.menu.applyTint(requireContext())
+        binding.toolBar.setOnMenuItemClickListener(this)
+        binding.toolBar.menu.findItem(R.id.menu_check_author)
+            ?.isChecked = AppConfig.changeSourceCheckAuthor
+        binding.toolBar.menu.findItem(R.id.menu_load_info)
+            ?.isChecked = AppConfig.changeSourceLoadInfo
+        binding.toolBar.menu.findItem(R.id.menu_load_toc)
+            ?.isChecked = AppConfig.changeSourceLoadToc
+    }
+
+    private fun initRecyclerView() {
+        binding.recyclerView.layoutManager = LinearLayoutManager(context)
+        binding.recyclerView.addItemDecoration(VerticalDivider(requireContext()))
+        binding.recyclerView.adapter = adapter
+        adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+                if (positionStart == 0) {
+                    binding.recyclerView.scrollToPosition(0)
+                }
+            }
+
+            override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
+                if (toPosition == 0) {
+                    binding.recyclerView.scrollToPosition(0)
+                }
+            }
+        })
+    }
+
+    private fun initSearchView() {
+        val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView
+        searchView.setOnCloseListener {
+            showTitle()
+            false
+        }
+        searchView.setOnSearchClickListener {
+            binding.toolBar.title = ""
+            binding.toolBar.subtitle = ""
+        }
+        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+            override fun onQueryTextSubmit(query: String?): Boolean {
+                return false
+            }
+
+            override fun onQueryTextChange(newText: String?): Boolean {
+                viewModel.screen(newText)
+                return false
+            }
+
+        })
+    }
+
+    private fun initLiveData() {
+        viewModel.searchStateData.observe(viewLifecycleOwner) {
+            binding.refreshProgressBar.isAutoLoading = it
+            if (it) {
+                startStopMenuItem?.let { item ->
+                    item.setIcon(R.drawable.ic_stop_black_24dp)
+                    item.setTitle(R.string.stop)
+                }
+            } else {
+                startStopMenuItem?.let { item ->
+                    item.setIcon(R.drawable.ic_refresh_black_24dp)
+                    item.setTitle(R.string.refresh)
+                }
+            }
+            binding.toolBar.menu.applyTint(requireContext())
+        }
+        launch {
+            viewModel.searchDataFlow
+                .collect {
+                    adapter.setItems(it)
+                    delay(1000)
+                }
+        }
+        launch {
+            appDb.bookSourceDao.flowGroupEnabled().collect {
+                groups.clear()
+                it.map { group ->
+                    groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
+                }
+                upGroupMenu()
+            }
+        }
+    }
+
+    private val startStopMenuItem: MenuItem?
+        get() = binding.toolBar.menu.findItem(R.id.menu_start_stop)
+
+    override fun onMenuItemClick(item: MenuItem?): Boolean {
+        when (item?.itemId) {
+            R.id.menu_check_author -> {
+                AppConfig.changeSourceCheckAuthor = !item.isChecked
+                item.isChecked = !item.isChecked
+                viewModel.refresh()
+            }
+            R.id.menu_load_toc -> {
+                putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked)
+                item.isChecked = !item.isChecked
+            }
+            R.id.menu_load_info -> {
+                putPrefBoolean(PreferKey.changeSourceLoadInfo, !item.isChecked)
+                item.isChecked = !item.isChecked
+            }
+            R.id.menu_start_stop -> viewModel.startOrStopSearch()
+            R.id.menu_source_manage -> startActivity<BookSourceActivity>()
+            else -> if (item?.groupId == R.id.source_group) {
+                if (!item.isChecked) {
+                    item.isChecked = true
+                    if (item.title.toString() == getString(R.string.all_source)) {
+                        putPrefString("searchGroup", "")
+                    } else {
+                        putPrefString("searchGroup", item.title.toString())
+                    }
+                    viewModel.startOrStopSearch()
+                    viewModel.refresh()
+                }
+            }
+        }
+        return false
+    }
+
+    override fun changeTo(searchBook: SearchBook) {
+        changeSource(searchBook)
+        dismissAllowingStateLoss()
+    }
+
+    override val bookUrl: String?
+        get() = callBack?.oldBook?.bookUrl
+
+    override fun topSource(searchBook: SearchBook) {
+        viewModel.topSource(searchBook)
+    }
+
+    override fun bottomSource(searchBook: SearchBook) {
+        viewModel.bottomSource(searchBook)
+    }
+
+    override fun editSource(searchBook: SearchBook) {
+        editSourceResult.launch {
+            putExtra("sourceUrl", searchBook.origin)
+        }
+    }
+
+    override fun disableSource(searchBook: SearchBook) {
+        viewModel.disableSource(searchBook)
+    }
+
+    override fun deleteSource(searchBook: SearchBook) {
+        viewModel.del(searchBook)
+        if (bookUrl == searchBook.bookUrl) {
+            viewModel.firstSourceOrNull(searchBook)?.let {
+                changeSource(it)
+            }
+        }
+    }
+
+    private fun changeSource(searchBook: SearchBook) {
+        try {
+            val book = searchBook.toBook()
+            book.upInfoFromOld(callBack?.oldBook)
+            val source = appDb.bookSourceDao.getBookSource(book.origin)
+            callBack?.changeTo(source!!, book)
+            searchBook.time = System.currentTimeMillis()
+            viewModel.updateSource(searchBook)
+        } catch (e: Exception) {
+            toastOnUi("换源失败\n${e.localizedMessage}")
+        }
+    }
+
+    /**
+     * 更新分组菜单
+     */
+    private fun upGroupMenu() {
+        val menu: Menu = binding.toolBar.menu
+        val selectedGroup = getPrefString("searchGroup")
+        menu.removeGroup(R.id.source_group)
+        val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
+        var hasSelectedGroup = false
+        groups.sortedWith { o1, o2 ->
+            o1.cnCompare(o2)
+        }.forEach { group ->
+            menu.add(R.id.source_group, Menu.NONE, Menu.NONE, group)?.let {
+                if (group == selectedGroup) {
+                    it.isChecked = true
+                    hasSelectedGroup = true
+                }
+            }
+        }
+        menu.setGroupCheckable(R.id.source_group, true, true)
+        if (!hasSelectedGroup) {
+            allItem.isChecked = true
+        }
+    }
+
+    override fun observeLiveBus() {
+        observeEvent<String>(EventBus.SOURCE_CHANGED) {
+            adapter.notifyItemRangeChanged(
+                0,
+                adapter.itemCount,
+                bundleOf(Pair("upCurSource", bookUrl))
+            )
+        }
+    }
+
+    interface CallBack {
+        val oldBook: Book?
+        fun changeTo(source: BookSource, book: Book)
+    }
+
+}

+ 327 - 0
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt

@@ -0,0 +1,327 @@
+package io.legado.app.ui.book.changesource
+
+import android.app.Application
+import android.os.Bundle
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import cn.hutool.core.collection.ConcurrentHashSet
+import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.AppConst
+import io.legado.app.constant.AppPattern
+import io.legado.app.constant.PreferKey
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.help.AppConfig
+import io.legado.app.help.coroutine.CompositeCoroutine
+import io.legado.app.model.webBook.WebBook
+import io.legado.app.utils.getPrefBoolean
+import io.legado.app.utils.getPrefString
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.ExecutorCoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import splitties.init.appCtx
+import timber.log.Timber
+import java.util.concurrent.Executors
+import kotlin.math.min
+
+@Suppress("MemberVisibilityCanBePrivate")
+class ChangeChapterSourceViewModel(application: Application) : BaseViewModel(application) {
+    private val threadCount = AppConfig.threadCount
+    private var searchPool: ExecutorCoroutineDispatcher? = null
+    val searchStateData = MutableLiveData<Boolean>()
+    var name: String = ""
+    var author: String = ""
+    private var tasks = CompositeCoroutine()
+    private var screenKey: String = ""
+    private var bookSourceList = arrayListOf<BookSource>()
+    private val searchBooks = ConcurrentHashSet<SearchBook>()
+    private val searchGroup get() = appCtx.getPrefString("searchGroup") ?: ""
+    private var searchCallback: SourceCallback? = null
+    val searchDataFlow = callbackFlow {
+
+        searchCallback = object : SourceCallback {
+
+            override fun searchSuccess(searchBook: SearchBook) {
+                appDb.searchBookDao.insert(searchBook)
+                when {
+                    screenKey.isEmpty() -> searchBooks.add(searchBook)
+                    searchBook.name.contains(screenKey) -> searchBooks.add(searchBook)
+                    else -> return
+                }
+                trySend(searchBooks)
+            }
+
+            override fun upAdapter() {
+                trySend(searchBooks)
+            }
+
+        }
+
+        getDbSearchBooks().let {
+            searchBooks.clear()
+            searchBooks.addAll(it)
+            trySend(searchBooks)
+        }
+
+        if (searchBooks.size <= 1) {
+            startSearch()
+        }
+
+        awaitClose {
+            searchCallback = null
+        }
+    }.conflate()
+        .map {
+            searchBooks.sortedBy { it.originOrder }
+        }.flowOn(IO)
+
+    @Volatile
+    private var searchIndex = -1
+
+    fun initData(arguments: Bundle?) {
+        arguments?.let { bundle ->
+            bundle.getString("name")?.let {
+                name = it
+            }
+            bundle.getString("author")?.let {
+                author = it.replace(AppPattern.authorRegex, "")
+            }
+        }
+    }
+
+    private fun initSearchPool() {
+        searchPool = Executors
+            .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
+        searchIndex = -1
+    }
+
+    fun refresh() {
+        getDbSearchBooks().let {
+            searchBooks.clear()
+            searchBooks.addAll(it)
+            searchCallback?.upAdapter()
+        }
+    }
+
+    fun startSearch() {
+        execute {
+            stopSearch()
+            appDb.searchBookDao.clear(name, author)
+            searchBooks.clear()
+            bookSourceList.clear()
+            if (searchGroup.isBlank()) {
+                bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
+            } else {
+                val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup)
+                if (sources.isEmpty()) {
+                    bookSourceList.addAll(appDb.bookSourceDao.allEnabled)
+                } else {
+                    bookSourceList.addAll(sources)
+                }
+            }
+            searchStateData.postValue(true)
+            initSearchPool()
+            for (i in 0 until threadCount) {
+                search()
+            }
+        }
+    }
+
+    private fun search() {
+        synchronized(this) {
+            if (searchIndex >= bookSourceList.lastIndex) {
+                return
+            }
+            searchIndex++
+        }
+        val source = bookSourceList[searchIndex]
+        val task = WebBook
+            .searchBook(viewModelScope, source, name, context = searchPool!!)
+            .timeout(60000L)
+            .onSuccess(IO) {
+                it.forEach { searchBook ->
+                    if (searchBook.name == name) {
+                        if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author))
+                            || !AppConfig.changeSourceCheckAuthor
+                        ) {
+                            if (searchBook.latestChapterTitle.isNullOrEmpty()) {
+                                if (AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc) {
+                                    loadBookInfo(source, searchBook.toBook())
+                                } else {
+                                    searchCallback?.searchSuccess(searchBook)
+                                }
+                            } else {
+                                searchCallback?.searchSuccess(searchBook)
+                            }
+                        }
+                    }
+                }
+            }
+            .onFinally(searchPool) {
+                synchronized(this) {
+                    if (searchIndex < bookSourceList.lastIndex) {
+                        search()
+                    } else {
+                        searchIndex++
+                    }
+                    if (searchIndex >= bookSourceList.lastIndex + bookSourceList.size
+                        || searchIndex >= bookSourceList.lastIndex + threadCount
+                    ) {
+                        searchStateData.postValue(false)
+                        tasks.clear()
+                    }
+                }
+
+            }
+        tasks.add(task)
+
+    }
+
+    private fun loadBookInfo(source: BookSource, book: Book) {
+        WebBook.getBookInfo(viewModelScope, source, book, context = searchPool!!)
+            .onSuccess(IO) {
+                if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
+                    loadBookToc(source, book)
+                } else {
+                    //从详情页里获取最新章节
+                    book.latestChapterTitle = it.latestChapterTitle
+                    val searchBook = book.toSearchBook()
+                    searchCallback?.searchSuccess(searchBook)
+                }
+            }.onError(IO) {
+                Timber.e(it)
+            }
+    }
+
+    private fun loadBookToc(source: BookSource, book: Book) {
+        WebBook.getChapterList(viewModelScope, source, book, context = searchPool!!)
+            .onSuccess(IO) { chapters ->
+                book.latestChapterTitle = chapters.last().title
+                val searchBook: SearchBook = book.toSearchBook()
+                searchCallback?.searchSuccess(searchBook)
+            }.onError(IO) {
+                Timber.e(it)
+            }
+    }
+
+    private fun getDbSearchBooks(): List<SearchBook> {
+        return if (screenKey.isEmpty()) {
+            if (AppConfig.changeSourceCheckAuthor) {
+                appDb.searchBookDao.getChangeSourceSearch(name, author, searchGroup)
+            } else {
+                appDb.searchBookDao.getChangeSourceSearch(name, "", searchGroup)
+            }
+        } else {
+            if (AppConfig.changeSourceCheckAuthor) {
+                appDb.searchBookDao.getChangeSourceSearch(name, author, screenKey, searchGroup)
+            } else {
+                appDb.searchBookDao.getChangeSourceSearch(name, "", screenKey, searchGroup)
+            }
+        }
+    }
+
+    /**
+     * 筛选
+     */
+    fun screen(key: String?) {
+        screenKey = key?.trim() ?: ""
+        execute {
+            getDbSearchBooks().let {
+                searchBooks.clear()
+                searchBooks.addAll(it)
+                searchCallback?.upAdapter()
+            }
+        }
+    }
+
+    fun startOrStopSearch() {
+        if (tasks.isEmpty) {
+            startSearch()
+        } else {
+            stopSearch()
+        }
+    }
+
+    fun stopSearch() {
+        tasks.clear()
+        searchPool?.close()
+        searchStateData.postValue(false)
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        searchPool?.close()
+    }
+
+    fun disableSource(searchBook: SearchBook) {
+        execute {
+            appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
+                source.enabled = false
+                appDb.bookSourceDao.update(source)
+            }
+            searchBooks.remove(searchBook)
+            searchCallback?.upAdapter()
+        }
+    }
+
+    fun topSource(searchBook: SearchBook) {
+        execute {
+            appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
+                val minOrder = appDb.bookSourceDao.minOrder - 1
+                source.customOrder = minOrder
+                searchBook.originOrder = source.customOrder
+                appDb.bookSourceDao.update(source)
+                updateSource(searchBook)
+            }
+            searchCallback?.upAdapter()
+        }
+    }
+
+    fun bottomSource(searchBook: SearchBook) {
+        execute {
+            appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
+                val maxOrder = appDb.bookSourceDao.maxOrder + 1
+                source.customOrder = maxOrder
+                searchBook.originOrder = source.customOrder
+                appDb.bookSourceDao.update(source)
+                updateSource(searchBook)
+            }
+            searchCallback?.upAdapter()
+        }
+    }
+
+    fun updateSource(searchBook: SearchBook) {
+        appDb.searchBookDao.update(searchBook)
+    }
+
+    fun del(searchBook: SearchBook) {
+        execute {
+            appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
+                appDb.bookSourceDao.delete(source)
+                appDb.searchBookDao.delete(searchBook)
+            }
+        }
+        searchBooks.remove(searchBook)
+        searchCallback?.upAdapter()
+    }
+
+    fun firstSourceOrNull(searchBook: SearchBook): SearchBook? {
+        return searchBooks.firstOrNull { it.bookUrl != searchBook.bookUrl }
+    }
+
+    interface SourceCallback {
+
+        fun searchSuccess(searchBook: SearchBook)
+
+        fun upAdapter()
+
+    }
+
+}