diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt index b8f59ca21..c56a04a35 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt @@ -1,19 +1,44 @@ package org.koitharu.kotatsu.shelf.domain -import javax.inject.Inject -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.toFavouriteCategory +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import javax.inject.Inject class ShelfRepository @Inject constructor( + private val localMangaRepository: LocalMangaRepository, + private val historyRepository: HistoryRepository, private val db: MangaDatabase, ) { + fun observeLocalManga(sortOrder: SortOrder): Flow> { + return flow { + emit(null) + emitAll(localMangaRepository.watchReadableDirs()) + }.mapLatest { + localMangaRepository.getList(0, null, sortOrder) + } + } + fun observeFavourites(): Flow>> { return db.favouriteCategoriesDao.observeAll() .flatMapLatest { categories -> @@ -26,6 +51,23 @@ class ShelfRepository @Inject constructor( } } + suspend fun deleteLocalManga(ids: Set) { + val list = localMangaRepository.getList(0, null, null) + .filter { x -> x.id in ids } + coroutineScope { + list.map { manga -> + async { + val original = localMangaRepository.getRemoteManga(manga) + if (localMangaRepository.delete(manga)) { + runCatchingCancellable { + historyRepository.deleteOrSwap(manga, original) + } + } + } + }.awaitAll() + } + } + private fun observeCategoriesContent( categories: List, ) = combine>, Map>>( diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt index c61f866f4..b9769f1b7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt @@ -26,6 +26,8 @@ import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel @@ -104,6 +106,7 @@ class ShelfFragment : is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context) is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category) is ShelfSectionModel.Updated -> UpdatesActivity.newIntent(view.context) + is ShelfSectionModel.Local -> MangaListActivity.newIntent(view.context, MangaSource.LOCAL) } startActivity(intent) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt index 6f167897e..0eccc248e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt @@ -6,6 +6,7 @@ import android.view.MenuItem import androidx.appcompat.view.ActionMode import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration @@ -41,9 +42,10 @@ class ShelfSelectionCallback( mode: ActionMode, menu: Menu, ): Boolean { - val checkedIds = controller.peekCheckedIds() - menu.findItem(R.id.action_remove).isVisible = checkedIds.none { (key, _) -> key is ShelfSectionModel.Updated } - && checkedIds.count { (_, v) -> v.isNotEmpty() } == 1 + val checkedIds = controller.peekCheckedIds().entries + val singleKey = checkedIds.singleOrNull { (_, ids) -> ids.isNotEmpty() }?.key + menu.findItem(R.id.action_remove)?.isVisible = singleKey != null && singleKey !is ShelfSectionModel.Updated + menu.findItem(R.id.action_save)?.isVisible = singleKey !is ShelfSectionModel.Local return super.onPrepareActionMode(controller, mode, menu) } @@ -77,6 +79,10 @@ class ShelfSelectionCallback( is ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids) is ShelfSectionModel.History -> viewModel.removeFromHistory(ids) is ShelfSectionModel.Updated -> return false + is ShelfSectionModel.Local -> { + showDeletionConfirm(ids, mode) + return true + } } mode.finish() true @@ -114,4 +120,19 @@ class ShelfSelectionCallback( } return viewModel.getManga(snapshot.values.flattenTo(HashSet())) } + + private fun showDeletionConfirm(ids: Set, mode: ActionMode) { + if (ids.isEmpty()) { + return + } + MaterialAlertDialogBuilder(context ?: return) + .setTitle(R.string.delete_manga) + .setMessage(context.getString(R.string.text_delete_local_manga_batch)) + .setPositiveButton(R.string.delete) { _, _ -> + viewModel.deleteLocal(ids) + mode.finish() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 3708d089b..15081f0a9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -7,6 +7,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.util.ReversibleAction @@ -24,8 +25,8 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toUi -import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.shelf.domain.ShelfRepository import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.tracker.domain.TrackingRepository @@ -35,11 +36,10 @@ import javax.inject.Inject @HiltViewModel class ShelfViewModel @Inject constructor( - repository: ShelfRepository, + private val repository: ShelfRepository, private val historyRepository: HistoryRepository, private val favouritesRepository: FavouritesRepository, private val trackingRepository: TrackingRepository, - private val localMangaRepository: LocalMangaRepository, private val settings: AppSettings, ) : BaseViewModel(), ListExtraProvider { @@ -47,13 +47,15 @@ class ShelfViewModel @Inject constructor( val content: LiveData> = combine( historyRepository.observeAllWithHistory(), + repository.observeLocalManga(SortOrder.UPDATED), repository.observeFavourites(), trackingRepository.observeUpdatedManga(), - ) { history, favourites, updated -> - mapList(history, favourites, updated) - }.catch { e -> - emit(listOf(e.toErrorState(canRetry = false))) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + ) { history, local, favourites, updated -> + mapList(history, favourites, updated, local) + }.debounce(500) + .catch { e -> + emit(listOf(e.toErrorState(canRetry = false))) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) override suspend fun getCounter(mangaId: Long): Int { return trackingRepository.getNewChaptersCount(mangaId) @@ -87,6 +89,13 @@ class ShelfViewModel @Inject constructor( } } + fun deleteLocal(ids: Set) { + launchLoadingJob(Dispatchers.Default) { + repository.deleteLocalManga(ids) + onActionDone.postCall(ReversibleAction(R.string.removal_completed, null)) + } + } + fun clearHistory(minDate: Long) { launchJob(Dispatchers.Default) { val stringRes = if (minDate <= 0) { @@ -123,11 +132,15 @@ class ShelfViewModel @Inject constructor( history: List, favourites: Map>, updated: Map, + local: List, ): List { val result = ArrayList(favourites.keys.size + 2) if (history.isNotEmpty()) { mapHistory(result, history) } + if (local.isNotEmpty()) { + mapLocal(result, local) + } if (updated.isNotEmpty()) { mapUpdated(result, updated) } @@ -174,6 +187,16 @@ class ShelfViewModel @Inject constructor( ) } + private suspend fun mapLocal( + destination: MutableList, + local: List, + ) { + destination += ShelfSectionModel.Local( + items = local.toUi(ListMode.GRID, this), + showAllButtonText = R.string.show_all, + ) + } + private suspend fun mapFavourites( destination: MutableList, favourites: Map>, diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt index a72bf0b47..da3f0ba2c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt @@ -111,4 +111,34 @@ sealed interface ShelfSectionModel : ListModel { override fun toString(): String = key } + + class Local( + override val items: List, + override val showAllButtonText: Int, + ) : ShelfSectionModel { + + override val key = "local" + + override fun getTitle(resources: Resources) = resources.getString(R.string.local_storage) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Local + + if (items != other.items) return false + if (showAllButtonText != other.showAllButtonText) return false + + return true + } + + override fun hashCode(): Int { + var result = items.hashCode() + result = 31 * result + showAllButtonText + return result + } + + override fun toString(): String = key + } }