More use of Snackbars

No longer double confirming to see if it's ok to remove manga in library and catalouge, snackbar has an undo button
This commit is contained in:
Jay 2019-10-29 19:22:54 -07:00
parent 25bf5602ba
commit a7e349b1b2
14 changed files with 162 additions and 76 deletions

View File

@ -1,7 +1,11 @@
package eu.kanade.tachiyomi.data.cache package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.launchUI
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -60,8 +64,30 @@ class CoverCache(private val context: Context) {
return false return false
// Remove file. // Remove file.
val file = getCoverFile(thumbnailUrl!!) val file = getCoverFile(thumbnailUrl)
return file.exists() && file.delete() return file.exists() && file.delete()
} }
/**
* Delete the cover file from the cache.
*
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
fun deleteFromCache(manga: Manga, delayBy:Long) {
val thumbnailUrl = manga.thumbnail_url
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty()) return
launchUI {
if (delayBy > 0) {
delay(delayBy)
if (manga.favorite) cancel()
}
// Remove file.
val file = getCoverFile(thumbnailUrl)
if (file.exists())
file.delete()
}
}
} }

View File

@ -9,6 +9,9 @@ import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.launchNow
import eu.kanade.tachiyomi.util.launchUI
import kotlinx.coroutines.delay
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -181,11 +184,28 @@ class DownloadManager(context: Context) {
* @param source the source of the manga. * @param source the source of the manga.
*/ */
fun deleteManga(manga: Manga, source: Source) { fun deleteManga(manga: Manga, source: Source) {
downloader.clearQueue(manga, true)
queue.remove(manga) queue.remove(manga)
provider.findMangaDir(manga, source)?.delete() provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga) cache.removeManga(manga)
} }
/**
* Deletes the directory of a downloaded manga.
*
* @param manga the manga to delete.
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source, delayBy: Long) {
launchUI {
delay(delayBy)
if (!manga.favorite) {
deleteManga(manga, source)
}
}
}
/** /**
* Adds a list of chapters to be deleted later. * Adds a list of chapters to be deleted later.
* *

View File

@ -90,6 +90,22 @@ class DownloadPendingDeleter(context: Context) {
} }
} }
/**
* Returns the list of chapters to be deleted grouped by its manga.
*
* Note: the returned list of manga and chapters only contain basic information needed by the
* downloader, so don't use them for anything else.
*/
@Synchronized
fun getPendingChapters(manga: Manga): List<Chapter>? {
val entries = decodeAll()
prefs.edit().clear().apply()
lastAddedEntry = null
val entry = entries.find { it.manga.id == manga.id }
return entry?.chapters?.map { it.toModel() }
}
/** /**
* Decodes all the chapters from preferences. * Decodes all the chapters from preferences.
*/ */

View File

@ -157,6 +157,26 @@ class Downloader(
notifier.dismiss() notifier.dismiss()
} }
/**
* Removes everything from the queue for a certain manga
*
* @param isNotification value that determines if status is set (needed for view updates)
*/
fun clearQueue(manga: Manga, isNotification: Boolean = false) {
//Needed to update the chapter view
if (isNotification) {
queue
.filter { it.status == Download.QUEUE && it.manga.id == manga.id }
.forEach { it.status = Download.NOT_DOWNLOADED }
}
queue.remove(manga)
if (queue.isEmpty()) {
DownloadService.stop(context)
stop()
}
notifier.dismiss()
}
/** /**
* Prepares the subscriptions to start downloading. * Prepares the subscriptions to start downloading.
*/ */

View File

@ -503,38 +503,40 @@ open class BrowseCatalogueController(bundle: Bundle) :
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
val activity = activity ?: return val activity = activity ?: return
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
snack?.dismiss()
if (manga.favorite) { if (manga.favorite) {
MaterialDialog.Builder(activity)
.items(activity.getString(R.string.remove_from_library))
.itemsCallback { _, _, which, _ ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
activity?.toast(activity?.getString(R.string.manga_removed_library))
}
}
}.show()
} else {
presenter.changeMangaFavorite(manga) presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position) adapter?.notifyItemChanged(position)
snack =
catalogue_view?.snack(activity.getString(R.string.manga_removed_library), 5000) {
setAction(R.string.action_undo) {
if (!manga.favorite) addManga(manga, position)
}
}
} else {
addManga(manga, position)
snack =
catalogue_view?.snack(activity.getString(R.string.manga_added_library), Snackbar.LENGTH_SHORT)
}
}
val categories = presenter.getCategories() private fun addManga(manga: Manga, position: Int) {
val defaultCategory = categories.find { it.id == preferences.defaultCategory() } presenter.changeMangaFavorite(manga)
if (defaultCategory != null) { adapter?.notifyItemChanged(position)
presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) val categories = presenter.getCategories()
.showDialog(router) val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
} if (defaultCategory != null) {
activity?.toast(activity?.getString(R.string.manga_added_library)) presenter.moveMangaToCategory(manga, defaultCategory)
} else if (categories.size <= 1) { // default or the one from the user
presenter.moveMangaToCategory(manga, categories.firstOrNull())
} else {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected).showDialog(router)
} }
} }

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
@ -254,7 +255,9 @@ open class BrowseCataloguePresenter(
fun changeMangaFavorite(manga: Manga) { fun changeMangaFavorite(manga: Manga) {
manga.favorite = !manga.favorite manga.favorite = !manga.favorite
if (!manga.favorite) { if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url) coverCache.deleteFromCache(manga, 5000)
val downloadManager: DownloadManager = Injekt.get()
downloadManager.deleteManga(manga,source,5000)
} }
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }

View File

@ -8,11 +8,11 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import android.view.* import android.view.*
import androidx.core.view.GravityCompat
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
@ -177,7 +177,8 @@ class LibraryController(
override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup { override fun createSecondaryDrawer(drawer: androidx.drawerlayout.widget.DrawerLayout): ViewGroup {
val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
navView = view navView = view
drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.END) drawer.setDrawerLockMode(androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED,
GravityCompat.END)
navView?.onGroupClicked = { group -> navView?.onGroupClicked = { group ->
when (group) { when (group) {
@ -365,7 +366,7 @@ class LibraryController(
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_filter -> { R.id.action_filter -> {
navView?.let { activity?.drawer?.openDrawer(Gravity.END) } navView?.let { activity?.drawer?.openDrawer(GravityCompat.END) }
} }
R.id.action_update_library -> { R.id.action_update_library -> {
activity?.let { LibraryUpdateService.start(it) } activity?.let { LibraryUpdateService.start(it) }
@ -413,7 +414,7 @@ class LibraryController(
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }
R.id.action_move_to_category -> showChangeMangaCategoriesDialog() R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
R.id.action_delete -> showDeleteMangaDialog() R.id.action_delete -> deleteMangasFromLibrary()
else -> return false else -> return false
} }
return true return true
@ -470,8 +471,15 @@ class LibraryController(
.showDialog(router) .showDialog(router)
} }
private fun showDeleteMangaDialog() { private fun deleteMangasFromLibrary() {
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) val mangas = selectedMangas.toList()
presenter.removeMangaFromLibrary(mangas, true)
destroyActionModeIfNeeded()
view?.snack(activity?.getString(R.string.remove_from_library) ?: "", 5000) {
setAction(R.string.action_undo) {
presenter.addMangas(mangas)
}
}
} }
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {

View File

@ -222,16 +222,13 @@ class LibraryPresenter(
* @return an observable of the categories and its manga. * @return an observable of the categories and its manga.
*/ */
private fun getLibraryObservable(): Observable<Library> { private fun getLibraryObservable(): Observable<Library> {
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga ->
{ dbCategories, libraryManga -> val categories = if (libraryManga.containsKey(0)) arrayListOf(Category.createDefault()) + dbCategories
val categories = if (libraryManga.containsKey(0)) else dbCategories
arrayListOf(Category.createDefault()) + dbCategories
else
dbCategories
this.categories = categories this.categories = categories
Library(categories, libraryManga) Library(categories, libraryManga)
}) }
} }
/** /**
@ -316,11 +313,11 @@ class LibraryPresenter(
Observable.fromCallable { Observable.fromCallable {
mangaToDelete.forEach { manga -> mangaToDelete.forEach { manga ->
coverCache.deleteFromCache(manga.thumbnail_url) coverCache.deleteFromCache(manga, 5000)
if (deleteChapters) { if (deleteChapters) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) { if (source != null) {
downloadManager.deleteManga(manga, source) downloadManager.deleteManga(manga, source, 5000)
} }
} }
} }
@ -329,6 +326,17 @@ class LibraryPresenter(
.subscribe() .subscribe()
} }
fun addMangas(mangas: List<Manga>) {
val mangaToAdd = mangas.distinctBy { it.id }
mangaToAdd.forEach { it.favorite = true }
Observable.fromCallable { db.insertMangas(mangaToAdd).executeAsBlocking() }
.onErrorResumeNext { Observable.empty() }
.subscribeOn(Schedulers.io())
.subscribe()
mangaToAdd.forEach { db.insertManga(it).executeAsBlocking() }
}
/** /**
* Move the given list of manga to categories. * Move the given list of manga to categories.
* *

View File

@ -421,24 +421,18 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
private fun showAddedSnack() { private fun showAddedSnack() {
val view = container val view = container
snack?.dismiss() snack?.dismiss()
snack = view?.snack(view.context.getString(R.string.manga_added_library), Snackbar snack = view?.snack(view.context.getString(R.string.manga_added_library), Snackbar.LENGTH_SHORT)
.LENGTH_SHORT)
} }
private fun showRemovedSnack() { private fun showRemovedSnack() {
val view = container val view = container
val hasDownloads = presenter.hasDownloads()
snack?.dismiss() snack?.dismiss()
if (view != null) { if (view != null) {
val message = view.context.getString(R.string.manga_removed_library) + snack = view.snack(view.context.getString(R.string.manga_removed_library), 5000) {
(if (hasDownloads) "\n" + view.context.getString(R.string setAction(R.string.action_undo) {
.delete_downloads_for_manga) else "") presenter.setFavorite(true)
snack = view.snack(message, (if (hasDownloads) Snackbar.LENGTH_INDEFINITE
else Snackbar.LENGTH_SHORT)) {
if (hasDownloads) setAction(R.string.action_delete) {
presenter.deleteDownloads()
}
} }
}
} }
} }

View File

@ -101,34 +101,21 @@ class MangaInfoPresenter(
fun toggleFavorite(): Boolean { fun toggleFavorite(): Boolean {
manga.favorite = !manga.favorite manga.favorite = !manga.favorite
if (!manga.favorite) { if (!manga.favorite) {
coverCache.deleteFromCache(manga.thumbnail_url) coverCache.deleteFromCache(manga, 5000)
downloadManager.deleteManga(manga, source, 5000)
} }
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
sendMangaToView() sendMangaToView()
return manga.favorite return manga.favorite
} }
private fun setFavorite(favorite: Boolean) { fun setFavorite(favorite: Boolean) {
if (manga.favorite == favorite) { if (manga.favorite == favorite) {
return return
} }
toggleFavorite() toggleFavorite()
} }
/**
* Returns true if the manga has any downloads.
*/
fun hasDownloads(): Boolean {
return downloadManager.getDownloadCount(manga) > 0
}
/**
* Deletes all the downloads for the manga.
*/
fun deleteDownloads() {
downloadManager.deleteManga(manga, source)
}
/** /**
* Get the default, and user categories. * Get the default, and user categories.
* *

View File

@ -40,7 +40,7 @@ fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: (Snackbar
Unit)? = null): Snackbar { Unit)? = null): Snackbar {
val snack = Snackbar.make(this, message, length) val snack = Snackbar.make(this, message, length)
val textView: TextView = snack.view.findViewById(com.google.android.material.R.id.snackbar_text) val textView: TextView = snack.view.findViewById(com.google.android.material.R.id.snackbar_text)
textView.setTextColor(Color.WHITE) textView.setTextColor(context.getResourceColor(android.R.attr.textColorPrimaryInverse))
when { when {
Build.VERSION.SDK_INT >= 23 -> snack.config(context, rootWindowInsets.systemWindowInsetBottom) Build.VERSION.SDK_INT >= 23 -> snack.config(context, rootWindowInsets.systemWindowInsetBottom)
else -> snack.config(context) else -> snack.config(context)

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#323232" /> <solid android:color="@color/snackbarBackground"/>
<corners android:radius="4dp" /> <corners android:radius="4dp"/>
</shape> </shape>

View File

@ -3,5 +3,6 @@
<color name="drawerHighlight">@color/md_white_1000_12</color> <color name="drawerHighlight">@color/md_white_1000_12</color>
<color name="drawerPrimary">@color/colorAccentDark</color> <color name="drawerPrimary">@color/colorAccentDark</color>
<color name="oldNavBarBackground">#B3000000</color> <color name="oldNavBarBackground">#B3000000</color>
<color name="snackbarBackground">#FFFFFF</color>
<color name="cardBackground">@color/colorDarkPrimary</color> <color name="cardBackground">@color/colorDarkPrimary</color>
</resources> </resources>

View File

@ -6,6 +6,7 @@
<color name="drawerHighlight">@color/md_black_1000_12</color> <color name="drawerHighlight">@color/md_black_1000_12</color>
<color name="drawerPrimary">@color/colorPrimary</color> <color name="drawerPrimary">@color/colorPrimary</color>
<color name="cardBackground">#FFFFFF</color> <color name="cardBackground">#FFFFFF</color>
<color name="snackbarBackground">#323232</color>
<!-- Dark Application Colors --> <!-- Dark Application Colors -->
<color name="colorDarkPrimary">#212121</color> <color name="colorDarkPrimary">#212121</color>
<color name="colorDarkPrimaryDark">#212121</color> <color name="colorDarkPrimaryDark">#212121</color>