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

(cherry picked from commit 9fe1a7e2ae14c4d7e70bfd85516d91c44514f04a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
This commit is contained in:
Hunter Nickel 2021-11-19 09:24:46 -07:00 committed by Jobobby04
parent 475dc87604
commit 7efde9c74c
11 changed files with 450 additions and 48 deletions

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.data.database.models
data class SourceIdMangaCount(val source: Long, val count: Int)

View File

@ -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
@ -18,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
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
@ -76,6 +79,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()
// SY -->
fun getMangas() = db.get()
.listOfObjects(Manga::class.java)
@ -176,30 +190,32 @@ 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)
// SY -->
.where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
)
""".trimIndent()
)
.whereArgs(0)
// SY <--
.whereArgs(0, *sourceIds.toTypedArray())
.build()
)
.prepare()
// SY -->
fun deleteMangasNotInLibraryAndNotRead() = db.delete()
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.database.queries
import exh.source.MERGED_SOURCE_ID
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
@ -241,3 +242,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}
"""

View File

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

View File

@ -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 android.widget.Toast
import androidx.appcompat.app.AlertDialog
@ -29,6 +27,7 @@ import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
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
@ -152,9 +151,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())
}
}
}
@ -501,44 +498,6 @@ class SettingsAdvancedController : SettingsController() {
}
}
class ClearDatabaseDialogController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val item = arrayOf(
activity!!.getString(R.string.clear_database_confirmation),
activity!!.getString(R.string.clear_db_exclude_read)
)
val selected = booleanArrayOf(true, true)
return MaterialAlertDialogBuilder(activity!!)
// .setMessage(R.string.clear_database_confirmation)
// SY -->
.setMultiChoiceItems(item, selected) { _, which, checked ->
if (which == 0) {
(dialog as AlertDialog).listView.setItemChecked(which, true)
} else {
selected[which] = checked
}
}
// SY <--
.setPositiveButton(android.R.string.ok) { _, _ ->
(targetController as? SettingsAdvancedController)?.clearDatabase(selected.last())
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
private fun clearDatabase(keepReadManga: Boolean) {
// SY -->
if (keepReadManga) {
db.deleteMangasNotInLibraryAndNotRead().executeAsBlocking()
} else {
db.deleteMangasNotInLibrary().executeAsBlocking()
}
// SY <--
db.deleteHistoryNoLastRead().executeAsBlocking()
activity?.toast(R.string.clear_database_completed)
}
private companion object {
// SY -->
private var job: Job? = null

View File

@ -0,0 +1,187 @@
package eu.kanade.tachiyomi.ui.setting.database
import android.annotation.SuppressLint
import android.app.AlertDialog
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 {
val item = arrayOf(
activity!!.getString(R.string.clear_database_confirmation),
activity!!.getString(R.string.clear_db_exclude_read)
)
val selected = booleanArrayOf(true, true)
return MaterialAlertDialogBuilder(activity!!)
//.setMessage(R.string.clear_database_confirmation)
// SY -->
.setMultiChoiceItems(item, selected) { _, which, checked ->
if (which == 0) {
(dialog as AlertDialog).listView.setItemChecked(which, true)
} else {
selected[which] = checked
}
}
// SY <--
.setPositiveButton(android.R.string.ok) { _, _ ->
(targetController as? ClearDatabaseController)?.clearDatabaseForSelectedSources(selected.last())
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun clearDatabaseForSelectedSources(/* SY --> */ keepReadManga: Boolean/* SY <-- */ ) {
val adapter = adapter ?: return
val selectedSourceIds = adapter.selectedPositions.mapNotNull { position ->
adapter.getItem(position)?.source?.id
}
presenter.clearDatabaseForSourceIds(selectedSourceIds, /* SY --> */ keepReadManga /* SY <-- */)
actionFab!!.isVisible = false
adapter.clearSelection()
adapter.notifyDataSetChanged()
activity?.toast(R.string.clear_database_completed)
}
}

View File

@ -0,0 +1,45 @@
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>, /* SY --> */ keepReadManga: Boolean /* SY <-- */) {
// SY -->
if (keepReadManga) {
db.deleteMangasNotInLibraryAndNotReadBySourceIds(sources).executeAsBlocking()
} else {
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
}
// SY <--
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 }
}
}
}

View File

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

View File

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

View File

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

View File

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