Feat: allow long click to favorite on GlobalSearch

This commit is contained in:
Cuong M. Tran 2024-03-07 12:25:27 +07:00 committed by Cuong Tran
parent c30a8873a2
commit 3bc673ae17
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
4 changed files with 156 additions and 4 deletions

View file

@ -62,7 +62,7 @@ fun GlobalSearchScreen(
onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior,
// KMK -->
toggleBulkSelectionMode = screenModel::toggleSelectionMode
toggleBulkSelectionMode = screenModel::toggleSelectionMode,
// KMK <--
)
},

View file

@ -339,7 +339,7 @@ open class BrowseSourceScreenModel(
false -> Instant.now().toEpochMilli()
},
)
// TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel])
if (!new.favorite) {
new = new.removeCovers(coverCache)
} else {

View file

@ -7,17 +7,23 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.GlobalSearchScreen
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.AllowDuplicateDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.presentation.core.screens.LoadingScreen
class GlobalSearchScreen(
@ -41,6 +47,9 @@ class GlobalSearchScreen(
}
// KMK -->
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
BackHandler(enabled = state.selectionMode) {
when {
state.selectionMode -> screenModel.toggleSelectionMode()
@ -89,8 +98,28 @@ class GlobalSearchScreen(
// KMK <--
navigator.push(MangaScreen(it.id, true))
},
onLongClickItem = {
navigator.push(MangaScreen(it.id, true))
onLongClickItem = { manga ->
// KMK -->
if (state.selectionMode)
// KMK <--
navigator.push(MangaScreen(manga.id, true))
// KMK -->
else
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> screenModel.setDialog(SearchScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog(
SearchScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
)
else -> screenModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
// KMK <--
},
)
}
@ -98,6 +127,33 @@ class GlobalSearchScreen(
// KMK -->
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is SearchScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
)
}
is SearchScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is SearchScreenModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
},
)
}
is SearchScreenModel.Dialog.ChangeMangasCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,

View file

@ -11,9 +11,12 @@ import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap
@ -35,19 +38,23 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.util.concurrent.Executors
abstract class SearchScreenModel(
@ -63,6 +70,9 @@ abstract class SearchScreenModel(
private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
// KMK <--
) : StateScreenModel<SearchScreenModel.State>(initialState) {
@ -390,11 +400,97 @@ abstract class SearchScreenModel(
.orEmpty()
}
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga).getOrNull(0)
}
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
screenModelScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
)
}
}
/**
* Adds or removes a manga from the library.
*
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
val source = sourceManager.getOrStub(manga.source)
screenModelScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
true -> 0
false -> Instant.now().toEpochMilli()
},
)
// TODO: also allow deleting chapters when remove favorite (just like in [MangaScreenModel])
if (!new.favorite) {
new = new.removeCovers(coverCache)
} else {
setMangaDefaultChapterFlags.await(manga)
addTracks.bindEnhancedTrackers(manga, source)
}
updateManga.await(new.toMangaUpdate())
}
}
fun addFavorite(manga: Manga) {
screenModelScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
moveMangaToCategories(manga, defaultCategory)
changeMangaFavorite(manga)
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
moveMangaToCategories(manga)
changeMangaFavorite(manga)
}
// Choose a category
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
setDialog(
Dialog.ChangeMangaCategory(
manga,
categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(),
),
)
}
}
}
}
fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) }
}
sealed interface Dialog {
data class RemoveManga(val manga: Manga) : Dialog
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class ChangeMangaCategory(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
) : Dialog
data class ChangeMangasCategory(
val mangas: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>,