From 9fe1a7e2ae14c4d7e70bfd85516d91c44514f04a Mon Sep 17 00:00:00 2001
From: Hunter Nickel <dementor9@yahoo.com>
Date: Fri, 19 Nov 2021 09:24:46 -0700
Subject: [PATCH] Add feature to clear database manga by source (#6241)

* Implement feature to selectively clear manga from database based on it's source

* Code cleanup and refactoring
---
 .../database/models/SourceIdMangaCount.kt     |   3 +
 .../data/database/queries/MangaQueries.kt     |  20 +-
 .../data/database/queries/RawQueries.kt       |  12 ++
 .../SourceIdMangaCountGetResolver.kt          |  23 +++
 .../ui/setting/SettingsAdvancedController.kt  |  27 +--
 .../database/ClearDatabaseController.kt       | 172 ++++++++++++++++++
 .../database/ClearDatabasePresenter.kt        |  39 ++++
 .../database/ClearDatabaseSourceItem.kt       |  55 ++++++
 .../res/layout/clear_database_controller.xml  |  32 ++++
 .../res/layout/clear_database_source_item.xml |  68 +++++++
 app/src/main/res/values/strings.xml           |   2 +
 11 files changed, 426 insertions(+), 27 deletions(-)
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt
 create mode 100644 app/src/main/res/layout/clear_database_controller.xml
 create mode 100644 app/src/main/res/layout/clear_database_source_item.xml

diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt
new file mode 100644
index 0000000000..bb91e1337c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt
@@ -0,0 +1,3 @@
+package eu.kanade.tachiyomi.data.database.models
+
+data class SourceIdMangaCount(val source: Long, val count: Int)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
index 667720aa0b..e82305c07e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.data.database.queries
 
+import com.pushtorefresh.storio.Queries
 import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
 import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
 import com.pushtorefresh.storio.sqlite.queries.Query
@@ -7,6 +8,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
 import eu.kanade.tachiyomi.data.database.DbProvider
 import eu.kanade.tachiyomi.data.database.models.LibraryManga
 import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
 import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
@@ -14,6 +16,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
+import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
 import eu.kanade.tachiyomi.data.database.tables.CategoryTable
 import eu.kanade.tachiyomi.data.database.tables.ChapterTable
 import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@@ -70,6 +73,17 @@ interface MangaQueries : DbProvider {
         )
         .prepare()
 
+    fun getSourceIdsWithNonLibraryManga() = db.get()
+        .listOfObjects(SourceIdMangaCount::class.java)
+        .withQuery(
+            RawQuery.builder()
+                .query(getSourceIdsWithNonLibraryMangaQuery())
+                .observesTables(MangaTable.TABLE)
+                .build()
+        )
+        .withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
+        .prepare()
+
     fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
 
     fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
@@ -123,12 +137,12 @@ interface MangaQueries : DbProvider {
 
     fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
 
-    fun deleteMangasNotInLibrary() = db.delete()
+    fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
         .byQuery(
             DeleteQuery.builder()
                 .table(MangaTable.TABLE)
-                .where("${MangaTable.COL_FAVORITE} = ?")
-                .whereArgs(0)
+                .where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
+                .whereArgs(0, *sourceIds.toTypedArray())
                 .build()
         )
         .prepare()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
index c79246e6b1..57091cd07b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.data.database.queries
 
+import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
 import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
 import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
 import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
@@ -142,3 +143,14 @@ fun getCategoriesForMangaQuery() =
     ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
     WHERE ${MangaCategory.COL_MANGA_ID} = ?
 """
+
+/** Query to get the list of sources in the database that have
+ * non-library manga, and how many
+ */
+fun getSourceIdsWithNonLibraryMangaQuery() =
+    """
+    SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT}
+    FROM ${Manga.TABLE}
+    WHERE ${Manga.COL_FAVORITE} = 0
+    GROUP BY ${Manga.COL_SOURCE}
+    """
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt
new file mode 100644
index 0000000000..ace4cc252c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt
@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.data.database.resolvers
+
+import android.annotation.SuppressLint
+import android.database.Cursor
+import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
+import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
+import eu.kanade.tachiyomi.data.database.tables.MangaTable
+
+class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
+
+    companion object {
+        val INSTANCE = SourceIdMangaCountGetResolver()
+        const val COL_COUNT = "manga_count"
+    }
+
+    @SuppressLint("Range")
+    override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
+        val sourceID = cursor.getLong(cursor.getColumnIndex(MangaTable.COL_SOURCE))
+        val count = cursor.getInt(cursor.getColumnIndex(COL_COUNT))
+
+        return SourceIdMangaCount(sourceID, count)
+    }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
index 4ab35e05e4..858df6d165 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
@@ -1,10 +1,8 @@
 package eu.kanade.tachiyomi.ui.setting
 
 import android.annotation.SuppressLint
-import android.app.Dialog
 import android.content.ActivityNotFoundException
 import android.content.Intent
-import android.os.Bundle
 import android.provider.Settings
 import androidx.core.net.toUri
 import androidx.preference.PreferenceScreen
@@ -20,8 +18,9 @@ import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
 import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
 import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.setting.database.ClearDatabaseController
 import eu.kanade.tachiyomi.util.CrashLogUtil
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.withUIContext
@@ -143,9 +142,7 @@ class SettingsAdvancedController : SettingsController() {
                 summaryRes = R.string.pref_clear_database_summary
 
                 onClick {
-                    val ctrl = ClearDatabaseDialogController()
-                    ctrl.targetController = this@SettingsAdvancedController
-                    ctrl.showDialog(router)
+                    router.pushController(ClearDatabaseController().withFadeTransaction())
                 }
             }
         }
@@ -278,24 +275,6 @@ class SettingsAdvancedController : SettingsController() {
             }
         }
     }
-
-    class ClearDatabaseDialogController : DialogController() {
-        override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-            return MaterialAlertDialogBuilder(activity!!)
-                .setMessage(R.string.clear_database_confirmation)
-                .setPositiveButton(android.R.string.ok) { _, _ ->
-                    (targetController as? SettingsAdvancedController)?.clearDatabase()
-                }
-                .setNegativeButton(android.R.string.cancel, null)
-                .create()
-        }
-    }
-
-    private fun clearDatabase() {
-        db.deleteMangasNotInLibrary().executeAsBlocking()
-        db.deleteHistoryNoLastRead().executeAsBlocking()
-        activity?.toast(R.string.clear_database_completed)
-    }
 }
 
 private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt
new file mode 100644
index 0000000000..2b66018815
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt
@@ -0,0 +1,172 @@
+package eu.kanade.tachiyomi.ui.setting.database
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import androidx.core.view.forEach
+import androidx.core.view.get
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+import dev.chrisbanes.insetter.applyInsetter
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.Payload
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.databinding.ClearDatabaseControllerBinding
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.ui.base.controller.FabController
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.util.system.toast
+
+class ClearDatabaseController :
+    NucleusController<ClearDatabaseControllerBinding, ClearDatabasePresenter>(),
+    FlexibleAdapter.OnItemClickListener,
+    FlexibleAdapter.OnUpdateListener,
+    FabController {
+
+    private var recycler: RecyclerView? = null
+    private var adapter: FlexibleAdapter<ClearDatabaseSourceItem>? = null
+
+    private var menu: Menu? = null
+
+    private var actionFab: ExtendedFloatingActionButton? = null
+    private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
+
+    init {
+        setHasOptionsMenu(true)
+    }
+
+    override fun createBinding(inflater: LayoutInflater): ClearDatabaseControllerBinding {
+        return ClearDatabaseControllerBinding.inflate(inflater)
+    }
+
+    override fun createPresenter(): ClearDatabasePresenter {
+        return ClearDatabasePresenter()
+    }
+
+    override fun getTitle(): String? {
+        return activity?.getString(R.string.pref_clear_database)
+    }
+
+    override fun onViewCreated(view: View) {
+        super.onViewCreated(view)
+
+        binding.recycler.applyInsetter {
+            type(navigationBars = true) {
+                padding()
+            }
+        }
+
+        adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
+        binding.recycler.adapter = adapter
+        binding.recycler.layoutManager = LinearLayoutManager(activity)
+        binding.recycler.setHasFixedSize(true)
+        adapter?.fastScroller = binding.fastScroller
+        recycler = binding.recycler
+    }
+
+    override fun onDestroyView(view: View) {
+        adapter = null
+        super.onDestroyView(view)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.generic_selection, menu)
+        this.menu = menu
+        menu.forEach { menuItem -> menuItem.isVisible = (adapter?.itemCount ?: 0) > 0 }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        val adapter = adapter ?: return false
+        when (item.itemId) {
+            R.id.action_select_all -> adapter.selectAll()
+            R.id.action_select_inverse -> {
+                val currentSelection = adapter.selectedPositionsAsSet
+                val invertedSelection = (0..adapter.itemCount)
+                    .filterNot { currentSelection.contains(it) }
+                currentSelection.clear()
+                currentSelection.addAll(invertedSelection)
+            }
+        }
+        updateFab()
+        adapter.notifyItemRangeChanged(0, adapter.itemCount, Payload.SELECTION)
+        return super.onOptionsItemSelected(item)
+    }
+
+    override fun onUpdateEmptyView(size: Int) {
+        if (size > 0) {
+            binding.emptyView.hide()
+        } else {
+            binding.emptyView.show(activity!!.getString(R.string.database_clean))
+        }
+
+        menu?.forEach { menuItem -> menuItem.isVisible = size > 0 }
+    }
+
+    override fun onItemClick(view: View?, position: Int): Boolean {
+        val adapter = adapter ?: return false
+        adapter.toggleSelection(position)
+        adapter.notifyItemChanged(position, Payload.SELECTION)
+        updateFab()
+        return true
+    }
+
+    fun setItems(items: List<ClearDatabaseSourceItem>) {
+        adapter?.updateDataSet(items)
+    }
+
+    override fun configureFab(fab: ExtendedFloatingActionButton) {
+        fab.setIconResource(R.drawable.ic_delete_24dp)
+        fab.setText(R.string.action_delete)
+        fab.isVisible = false
+        fab.setOnClickListener {
+            val ctrl = ClearDatabaseSourcesDialog()
+            ctrl.targetController = this
+            ctrl.showDialog(router)
+        }
+        actionFab = fab
+    }
+
+    private fun updateFab() {
+        val adapter = adapter ?: return
+        actionFab?.isVisible = adapter.selectedItemCount > 0
+    }
+
+    override fun cleanupFab(fab: ExtendedFloatingActionButton) {
+        actionFab?.setOnClickListener(null)
+        actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
+        actionFab = null
+    }
+
+    class ClearDatabaseSourcesDialog : DialogController() {
+        override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+            return MaterialAlertDialogBuilder(activity!!)
+                .setMessage(R.string.clear_database_confirmation)
+                .setPositiveButton(android.R.string.ok) { _, _ ->
+                    (targetController as? ClearDatabaseController)?.clearDatabaseForSelectedSources()
+                }
+                .setNegativeButton(android.R.string.cancel, null)
+                .create()
+        }
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    private fun clearDatabaseForSelectedSources() {
+        val adapter = adapter ?: return
+        val selectedSourceIds = adapter.selectedPositions.mapNotNull { position ->
+            adapter.getItem(position)?.source?.id
+        }
+        presenter.clearDatabaseForSourceIds(selectedSourceIds)
+        actionFab!!.isVisible = false
+        adapter.clearSelection()
+        adapter.notifyDataSetChanged()
+        activity?.toast(R.string.clear_database_completed)
+    }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt
new file mode 100644
index 0000000000..05dafd73c0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt
@@ -0,0 +1,39 @@
+package eu.kanade.tachiyomi.ui.setting.database
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import rx.Observable
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
+
+    private val db = Injekt.get<DatabaseHelper>()
+
+    private val sourceManager = Injekt.get<SourceManager>()
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        getDatabaseSourcesObservable()
+            .subscribeOn(Schedulers.io())
+            .subscribeLatestCache(ClearDatabaseController::setItems)
+    }
+
+    fun clearDatabaseForSourceIds(sources: List<Long>) {
+        db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
+        db.deleteHistoryNoLastRead().executeAsBlocking()
+    }
+
+    private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {
+        return db.getSourceIdsWithNonLibraryManga().asRxObservable()
+            .map { sourceCounts ->
+                sourceCounts.map {
+                    val sourceObj = sourceManager.getOrStub(it.source)
+                    ClearDatabaseSourceItem(sourceObj, it.count)
+                }.sortedBy { it.source.name }
+            }
+    }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt
new file mode 100644
index 0000000000..f599c8e804
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt
@@ -0,0 +1,55 @@
+package eu.kanade.tachiyomi.ui.setting.database
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.databinding.ClearDatabaseSourceItemBinding
+import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.icon
+
+data class ClearDatabaseSourceItem(val source: Source, private val mangaCount: Int) : AbstractFlexibleItem<ClearDatabaseSourceItem.Holder>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.clear_database_source_item
+    }
+
+    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
+        return Holder(view, adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?, holder: Holder?, position: Int, payloads: MutableList<Any>?) {
+        if (payloads.isNullOrEmpty()) {
+            holder?.bind(source, mangaCount)
+        } else {
+            holder?.updateCheckbox()
+        }
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
+
+        private val binding = ClearDatabaseSourceItemBinding.bind(view)
+
+        fun bind(source: Source, count: Int) {
+            binding.title.text = source.toString()
+            binding.description.text = itemView.context.getString(R.string.clear_database_source_item_count, count)
+
+            itemView.post {
+                when {
+                    source.id == LocalSource.ID -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source)
+                    source is SourceManager.StubSource -> binding.thumbnail.setImageDrawable(null)
+                    source.icon() != null -> binding.thumbnail.setImageDrawable(source.icon())
+                }
+            }
+        }
+
+        fun updateCheckbox() {
+            binding.checkbox.isChecked = (bindingAdapter as FlexibleAdapter<*>).isSelected(bindingAdapterPosition)
+        }
+    }
+}
diff --git a/app/src/main/res/layout/clear_database_controller.xml b/app/src/main/res/layout/clear_database_controller.xml
new file mode 100644
index 0000000000..70027af456
--- /dev/null
+++ b/app/src/main/res/layout/clear_database_controller.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:choiceMode="multipleChoice"
+        android:clipToPadding="false"
+        android:paddingBottom="@dimen/fab_list_padding"
+        tools:listitem="@layout/clear_database_source_item" />
+
+    <eu.kanade.tachiyomi.widget.MaterialFastScroll
+        android:id="@+id/fast_scroller"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="end"
+        app:fastScrollerBubbleEnabled="false"
+        tools:visibility="visible" />
+
+    <eu.kanade.tachiyomi.widget.EmptyView
+        android:id="@+id/empty_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:visibility="gone" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/clear_database_source_item.xml b/app/src/main/res/layout/clear_database_source_item.xml
new file mode 100644
index 0000000000..823668a53b
--- /dev/null
+++ b/app/src/main/res/layout/clear_database_source_item.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="56dp"
+    android:background="@drawable/list_item_selector_background"
+    android:paddingStart="8dp"
+    android:paddingEnd="16dp">
+
+
+    <ImageView
+        android:id="@+id/thumbnail"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:paddingHorizontal="8dp"
+        app:layout_constraintDimensionRatio="1:1"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        tools:ignore="ContentDescription"
+        tools:src="@mipmap/ic_launcher" />
+
+    <TextView
+        android:id="@+id/title"
+        style="?attr/textAppearanceBodyMedium"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:paddingStart="0dp"
+        android:paddingEnd="8dp"
+        android:ellipsize="middle"
+        android:maxLines="1"
+        app:layout_constraintStart_toEndOf="@+id/thumbnail"
+        app:layout_constraintEnd_toStartOf="@id/checkbox"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/description"
+        app:layout_constraintVertical_chainStyle="packed"
+        tools:text="Source Name (LN)" />
+
+    <TextView
+        android:id="@+id/description"
+        style="?attr/textAppearanceBodySmall"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        app:layout_constraintStart_toEndOf="@+id/thumbnail"
+        app:layout_constraintEnd_toStartOf="@id/checkbox"
+        app:layout_constraintTop_toBottomOf="@id/title"
+        app:layout_constraintBottom_toBottomOf="parent"
+        tools:text="999 non-library manga in database" />
+
+    <CheckBox
+        android:id="@+id/checkbox"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:clickable="false"
+        android:background="@android:color/transparent"
+        android:longClickable="false"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintDimensionRatio="1:2"
+        />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 57dc9f0f26..a6ff4ad9b8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -461,8 +461,10 @@
     <string name="pref_auto_clear_chapter_cache">Clear chapter cache on app close</string>
     <string name="pref_clear_database">Clear database</string>
     <string name="pref_clear_database_summary">Delete history for manga that are not saved in your library</string>
+    <string name="clear_database_source_item_count">%1$d non-library manga in database</string>
     <string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string>
     <string name="clear_database_completed">Entries deleted</string>
+    <string name="database_clean">Database clean</string>
     <string name="pref_refresh_library_covers">Refresh library manga covers</string>
     <string name="pref_refresh_library_tracking">Refresh tracking</string>
     <string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>