Show local section in shelf

This commit is contained in:
Koitharu
2022-10-14 15:34:58 +03:00
parent 7b36c64b34
commit 9b54ed6bc7
5 changed files with 132 additions and 13 deletions

View File

@@ -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<List<Manga>> {
return flow {
emit(null)
emitAll(localMangaRepository.watchReadableDirs())
}.mapLatest {
localMangaRepository.getList(0, null, sortOrder)
}
}
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
return db.favouriteCategoriesDao.observeAll()
.flatMapLatest { categories ->
@@ -26,6 +51,23 @@ class ShelfRepository @Inject constructor(
}
}
suspend fun deleteLocalManga(ids: Set<Long>) {
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<FavouriteCategoryEntity>,
) = combine<Pair<FavouriteCategory, List<Manga>>, Map<FavouriteCategory, List<Manga>>>(

View File

@@ -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)
}

View File

@@ -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<Long>, 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()
}
}

View File

@@ -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<List<ListModel>> = 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<Long>) {
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<MangaWithHistory>,
favourites: Map<FavouriteCategory, List<Manga>>,
updated: Map<Manga, Int>,
local: List<Manga>,
): List<ListModel> {
val result = ArrayList<ListModel>(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<in ShelfSectionModel.Local>,
local: List<Manga>,
) {
destination += ShelfSectionModel.Local(
items = local.toUi(ListMode.GRID, this),
showAllButtonText = R.string.show_all,
)
}
private suspend fun mapFavourites(
destination: MutableList<in ShelfSectionModel.Favourites>,
favourites: Map<FavouriteCategory, List<Manga>>,

View File

@@ -111,4 +111,34 @@ sealed interface ShelfSectionModel : ListModel {
override fun toString(): String = key
}
class Local(
override val items: List<MangaItemModel>,
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
}
}