mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -0,0 +1,3 @@ | ||||
| package eu.kanade.tachiyomi.data.database.models | ||||
|  | ||||
| data class SourceIdMangaCount(val source: Long, val count: Int) | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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} | ||||
|     """ | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -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 } | ||||
|             } | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user