Undo deletion from favourites

pull/163/head
Koitharu 4 years ago
parent f5db5c39c3
commit 318486d62b
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -87,6 +87,10 @@ abstract class FavouritesDao {
@Query("UPDATE favourites SET deleted_at = :now WHERE manga_id = :mangaId AND category_id = :categoryId") @Query("UPDATE favourites SET deleted_at = :now WHERE manga_id = :mangaId AND category_id = :categoryId")
abstract suspend fun delete(categoryId: Long, mangaId: Long, now: Long = System.currentTimeMillis()) abstract suspend fun delete(categoryId: Long, mangaId: Long, now: Long = System.currentTimeMillis())
suspend fun recover(mangaId: Long) = delete(mangaId, 0L)
suspend fun recover(categoryId: Long, mangaId: Long) = delete(categoryId, mangaId, 0L)
@Query("DELETE FROM favourites WHERE deleted_at != 0") @Query("DELETE FROM favourites WHERE deleted_at != 0")
abstract suspend fun gc() abstract suspend fun gc()

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.favourites.domain
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@ -117,20 +118,22 @@ class FavouritesRepository(
} }
} }
suspend fun removeFromFavourites(ids: Collection<Long>) { suspend fun removeFromFavourites(ids: Collection<Long>): ReversibleHandle {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.delete(id) db.favouritesDao.delete(id)
} }
} }
return ReversibleHandle { recoverToFavourites(ids) }
} }
suspend fun removeFromCategory(categoryId: Long, ids: Collection<Long>) { suspend fun removeFromCategory(categoryId: Long, ids: Collection<Long>): ReversibleHandle {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.delete(categoryId, id) db.favouritesDao.delete(categoryId, id)
} }
} }
return ReversibleHandle { recoverToCategory(categoryId, ids) }
} }
private fun observeOrder(categoryId: Long): Flow<SortOrder> { private fun observeOrder(categoryId: Long): Flow<SortOrder> {
@ -139,4 +142,20 @@ class FavouritesRepository(
.map { x -> SortOrder(x.order, SortOrder.NEWEST) } .map { x -> SortOrder(x.order, SortOrder.NEWEST) }
.distinctUntilChanged() .distinctUntilChanged()
} }
private suspend fun recoverToFavourites(ids: Collection<Long>) {
db.withTransaction {
for (id in ids) {
db.favouritesDao.recover(id)
}
}
}
private suspend fun recoverToCategory(categoryId: Long, ids: Collection<Long>) {
db.withTransaction {
for (id in ids) {
db.favouritesDao.recover(categoryId, id)
}
}
}
} }

@ -7,9 +7,12 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.iterator import androidx.core.view.iterator
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
@ -30,6 +33,7 @@ class FavouritesListFragment : MangaListFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
viewModel.onItemsRemoved.observe(viewLifecycleOwner, ::onItemsRemoved)
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
@ -94,6 +98,15 @@ class FavouritesListFragment : MangaListFragment() {
} }
} }
private fun onItemsRemoved(reversibleHandle: ReversibleHandle) {
val message = viewModel.categoryName?.let {
getString(R.string.removed_from_s, it)
} ?: getString(R.string.removed_from_favourites)
Snackbar.make(binding.recyclerView, message, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) { reversibleHandle.reverseAsync() }
.show()
}
companion object { companion object {
const val NO_ID = 0L const val NO_ID = 0L

@ -7,7 +7,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
@ -19,6 +21,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class FavouritesListViewModel( class FavouritesListViewModel(
@ -28,6 +31,9 @@ class FavouritesListViewModel(
settings: AppSettings, settings: AppSettings,
) : MangaListViewModel(settings), CountersProvider { ) : MangaListViewModel(settings), CountersProvider {
var categoryName: String? = null
private set
var sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) { var sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
MutableLiveData(null) MutableLiveData(null)
} else { } else {
@ -63,6 +69,20 @@ class FavouritesListViewModel(
emit(listOf(it.toErrorState(canRetry = false))) emit(listOf(it.toErrorState(canRetry = false)))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
val onItemsRemoved = SingleLiveEvent<ReversibleHandle>()
init {
if (categoryId != NO_ID) {
launchJob {
categoryName = withContext(Dispatchers.Default) {
runCatching {
repository.getCategory(categoryId).title
}.getOrNull()
}
}
}
}
override fun onRefresh() = Unit override fun onRefresh() = Unit
override fun onRetry() = Unit override fun onRetry() = Unit
@ -71,12 +91,13 @@ class FavouritesListViewModel(
if (ids.isEmpty()) { if (ids.isEmpty()) {
return return
} }
launchJob { launchJob(Dispatchers.Default) {
if (categoryId == NO_ID) { val handle = if (categoryId == NO_ID) {
repository.removeFromFavourites(ids) repository.removeFromFavourites(ids)
} else { } else {
repository.removeFromCategory(categoryId, ids) repository.removeFromCategory(categoryId, ids)
} }
onItemsRemoved.postCall(handle)
} }
} }

@ -64,6 +64,8 @@ abstract class HistoryDao {
@Query("UPDATE history SET deleted_at = :now WHERE manga_id = :mangaId") @Query("UPDATE history SET deleted_at = :now WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long, now: Long = System.currentTimeMillis()) abstract suspend fun delete(mangaId: Long, now: Long = System.currentTimeMillis())
suspend fun recover(mangaId: Long) = delete(mangaId, 0L)
@Query("DELETE FROM history WHERE deleted_at != 0") @Query("DELETE FROM history WHERE deleted_at != 0")
abstract suspend fun gc() abstract suspend fun gc()

@ -94,24 +94,14 @@ class HistoryRepository(
db.historyDao.delete(manga.id) db.historyDao.delete(manga.id)
} }
suspend fun delete(ids: Collection<Long>) { suspend fun delete(ids: Collection<Long>): ReversibleHandle {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.historyDao.delete(id) db.historyDao.delete(id)
} }
} }
}
suspend fun deleteReversible(ids: Collection<Long>): ReversibleHandle {
val entities = db.withTransaction {
val entities = db.historyDao.findAll(ids.toList()).filterNotNull()
for (id in ids) {
db.historyDao.delete(id)
}
entities
}
return ReversibleHandle { return ReversibleHandle {
db.historyDao.upsert(entities) recover(ids)
} }
} }
@ -128,4 +118,12 @@ class HistoryRepository(
suspend fun getPopularTags(limit: Int): List<MangaTag> { suspend fun getPopularTags(limit: Int): List<MangaTag> {
return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() } return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() }
} }
private suspend fun recover(ids: Collection<Long>) {
db.withTransaction {
for (id in ids) {
db.historyDao.recover(id)
}
}
}
} }

@ -80,7 +80,7 @@ class HistoryListViewModel(
return return
} }
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val handle = repository.deleteReversible(ids) + ReversibleHandle { val handle = repository.delete(ids) + ReversibleHandle {
shortcutsRepository.updateShortcuts() shortcutsRepository.updateShortcuts()
} }
shortcutsRepository.updateShortcuts() shortcutsRepository.updateShortcuts()

@ -304,4 +304,6 @@
<string name="detect_reader_mode">Autodetect reader mode</string> <string name="detect_reader_mode">Autodetect reader mode</string>
<string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string> <string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string>
<string name="enter_email_text">Enter your email to continue</string> <string name="enter_email_text">Enter your email to continue</string>
<string name="removed_from_favourites">Removed from favourites</string>
<string name="removed_from_s">Removed from \"%s\"</string>
</resources> </resources>
Loading…
Cancel
Save