Manga cover updates (#3101)

* cover caching overhaul

* add ui for removing custom cover

* skip some loading work

* minor cleanup

* allow refresh library metadata to refresh local manga

* rename metadata_date to cover_last_modified

* rearrange removeMangaFromLibrary

* change custom cover directory
add setting for updating cover when refreshing library

* remove toggle and explicit action for updating covers
This commit is contained in:
MCAxiaz
2020-05-10 08:15:25 -07:00
committed by GitHub
parent 436253dd63
commit dc54299e24
30 changed files with 441 additions and 208 deletions
@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.library
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.DialogController
class ChangeMangaCoverDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : ChangeMangaCoverDialog.Listener {
private lateinit var manga: Manga
constructor(target: T, manga: Manga) : this() {
targetController = target
this.manga = manga
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.action_edit_cover)
.positiveButton(R.string.action_edit) {
(targetController as? Listener)?.openMangaCoverPicker(manga)
}
.negativeButton(android.R.string.cancel)
.neutralButton(R.string.action_delete) {
(targetController as? Listener)?.deleteMangaCover(manga)
}
}
interface Listener {
fun deleteMangaCover(manga: Manga)
fun openMangaCoverPicker(manga: Manga)
}
}
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@@ -22,6 +21,7 @@ import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
@@ -37,7 +37,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.visible
import java.io.IOException
import kotlinx.android.synthetic.main.main_activity.tabs
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
@@ -51,11 +50,13 @@ import uy.kohesive.injekt.api.get
class LibraryController(
bundle: Bundle? = null,
private val preferences: PreferencesHelper = Injekt.get()
private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
RootController,
TabbedController,
ActionMode.Callback,
ChangeMangaCoverDialog.Listener,
ChangeMangaCategoriesDialog.Listener,
DeleteLibraryMangasDialog.Listener {
@@ -424,10 +425,7 @@ class LibraryController(
private fun onActionItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_edit_cover -> {
changeSelectedCover()
destroyActionModeIfNeeded()
}
R.id.action_edit_cover -> handleChangeCover()
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
R.id.action_delete -> showDeleteMangaDialog()
R.id.action_select_all -> selectAllCategoryManga()
@@ -486,6 +484,23 @@ class LibraryController(
}
}
private fun handleChangeCover() {
val manga = selectedMangas.firstOrNull() ?: return
if (coverCache.getCustomCoverFile(manga).exists()) {
showEditCoverDialog(manga)
} else {
openMangaCoverPicker(manga)
}
}
/**
* Edit custom cover for selected manga.
*/
private fun showEditCoverDialog(manga: Manga) {
ChangeMangaCoverDialog(this, manga).showDialog(router)
}
/**
* Move the selected manga to a list of categories.
*/
@@ -509,21 +524,7 @@ class LibraryController(
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
}
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.moveMangasToCategories(categories, mangas)
destroyActionModeIfNeeded()
}
override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
presenter.removeMangaFromLibrary(mangas, deleteChapters)
destroyActionModeIfNeeded()
}
/**
* Changes the cover for the selected manga.
*/
private fun changeSelectedCover() {
val manga = selectedMangas.firstOrNull() ?: return
override fun openMangaCoverPicker(manga: Manga) {
selectedCoverManga = manga
if (manga.favorite) {
@@ -539,6 +540,23 @@ class LibraryController(
} else {
activity?.toast(R.string.notification_first_add_to_library)
}
destroyActionModeIfNeeded()
}
override fun deleteMangaCover(manga: Manga) {
presenter.deleteCustomCover(manga)
destroyActionModeIfNeeded()
}
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.moveMangasToCategories(categories, mangas)
destroyActionModeIfNeeded()
}
override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
presenter.removeMangaFromLibrary(mangas, deleteChapters)
destroyActionModeIfNeeded()
}
private fun selectAllCategoryManga() {
@@ -555,28 +573,25 @@ class LibraryController(
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_IMAGE_OPEN) {
if (data == null || resultCode != Activity.RESULT_OK) return
val dataUri = data?.data
if (dataUri == null || resultCode != Activity.RESULT_OK) return
val activity = activity ?: return
val manga = selectedCoverManga ?: return
try {
// Get the file's input stream from the incoming Intent
activity.contentResolver.openInputStream(data.data ?: Uri.EMPTY).use {
// Update cover to selected file, show error if something went wrong
if (it != null && presenter.editCoverWithStream(it, manga)) {
// TODO refresh cover
} else {
activity.toast(R.string.notification_cover_update_failed)
}
}
} catch (error: IOException) {
activity.toast(R.string.notification_cover_update_failed)
Timber.e(error)
}
selectedCoverManga = null
presenter.editCover(manga, activity, dataUri)
}
}
fun onSetCoverSuccess() {
activity?.toast(R.string.cover_updated)
}
fun onSetCoverError(error: Throwable) {
activity?.toast(R.string.notification_cover_update_failed)
Timber.e(error)
}
private companion object {
/**
* Key to change the cover of a manga in [onActivityResult].
@@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.source_grid_item.download_text
import kotlinx.android.synthetic.main.source_grid_item.local_text
@@ -48,7 +48,7 @@ class LibraryGridHolder(
text = item.downloadCount.toString()
}
// set local visibility if its local manga
local_text.visibleIf { item.manga.source == LocalSource.ID }
local_text.visibleIf { item.manga.isLocal() }
// Update the cover.
GlideApp.with(view.context).clear(thumbnail)
@@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.source_list_item.download_text
import kotlinx.android.synthetic.main.source_list_item.local_text
@@ -49,7 +49,7 @@ class LibraryListHolder(
text = "${item.downloadCount}"
}
// show local text badge if local manga
local_text.visibleIf { item.manga.source == LocalSource.ID }
local_text.visibleIf { item.manga.isLocal() }
// Create thumbnail onclick to simulate long click
thumbnail.setOnClickListener {
@@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.net.Uri
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.cache.CoverCache
@@ -14,11 +16,12 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.combineLatest
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
import eu.kanade.tachiyomi.util.lang.launchIO
import java.io.IOException
import java.io.InputStream
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.updateCoverLastModified
import java.util.ArrayList
import java.util.Collections
import java.util.Comparator
@@ -128,7 +131,7 @@ class LibraryPresenter(
// Filter when there are no downloads.
if (filterDownloaded) {
// Local manga are always downloaded
if (item.manga.source == LocalSource.ID) {
if (item.manga.isLocal()) {
return@f true
}
// Don't bother with directory checking if download count has been set.
@@ -318,16 +321,17 @@ class LibraryPresenter(
* @param deleteChapters whether to also delete downloaded chapters.
*/
fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
// Create a set of the list
val mangaToDelete = mangas.distinctBy { it.id }
mangaToDelete.forEach { it.favorite = false }
launchIO {
val mangaToDelete = mangas.distinctBy { it.id }
mangaToDelete.forEach {
it.favorite = false
it.removeCovers(coverCache)
}
db.insertMangas(mangaToDelete).executeAsBlocking()
mangaToDelete.forEach { manga ->
coverCache.deleteFromCache(manga.thumbnail_url)
if (deleteChapters) {
if (deleteChapters) {
mangaToDelete.forEach { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) {
downloadManager.deleteManga(manga, source)
@@ -358,21 +362,42 @@ class LibraryPresenter(
/**
* Update cover with local file.
*
* @param inputStream the new cover.
* @param manga the manga edited.
* @return true if the cover is updated, false otherwise
* @param context Context.
* @param data uri of the cover resource.
*/
@Throws(IOException::class)
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
if (manga.source == LocalSource.ID) {
LocalSource.updateCover(context, manga, inputStream)
return true
}
fun editCover(manga: Manga, context: Context, data: Uri) {
Observable
.fromCallable {
context.contentResolver.openInputStream(data)?.use {
if (manga.isLocal()) {
LocalSource.updateCover(context, manga, it)
manga.updateCoverLastModified(db)
} else if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db)
}
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ -> view.onSetCoverSuccess() },
{ view, e -> view.onSetCoverError(e) }
)
}
if (manga.thumbnail_url != null && manga.favorite) {
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
return true
}
return false
fun deleteCustomCover(manga: Manga) {
Observable
.fromCallable {
coverCache.deleteCustomCover(manga)
manga.updateCoverLastModified(db)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ -> view.onSetCoverSuccess() },
{ view, e -> view.onSetCoverError(e) }
)
}
}