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, onToggleResults = onToggleResults,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
// KMK --> // KMK -->
toggleBulkSelectionMode = screenModel::toggleSelectionMode toggleBulkSelectionMode = screenModel::toggleSelectionMode,
// KMK <-- // KMK <--
) )
}, },

View file

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

View file

@ -7,17 +7,23 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.GlobalSearchScreen import eu.kanade.presentation.browse.GlobalSearchScreen
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.category.components.ChangeCategoryDialog import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.AllowDuplicateDialog import eu.kanade.presentation.manga.AllowDuplicateDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
class GlobalSearchScreen( class GlobalSearchScreen(
@ -41,6 +47,9 @@ class GlobalSearchScreen(
} }
// KMK --> // KMK -->
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
BackHandler(enabled = state.selectionMode) { BackHandler(enabled = state.selectionMode) {
when { when {
state.selectionMode -> screenModel.toggleSelectionMode() state.selectionMode -> screenModel.toggleSelectionMode()
@ -89,8 +98,28 @@ class GlobalSearchScreen(
// KMK <-- // KMK <--
navigator.push(MangaScreen(it.id, true)) navigator.push(MangaScreen(it.id, true))
}, },
onLongClickItem = { onLongClickItem = { manga ->
navigator.push(MangaScreen(it.id, true)) // 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 --> // KMK -->
val onDismissRequest = { screenModel.setDialog(null) } val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) { 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 -> { is SearchScreenModel.Dialog.ChangeMangasCategory -> {
ChangeCategoryDialog( ChangeCategoryDialog(
initialSelection = dialog.initialSelection, 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.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.PersistentMap
@ -35,19 +38,23 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import tachiyomi.core.common.preference.CheckboxState 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.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.Instant
import java.util.concurrent.Executors import java.util.concurrent.Executors
abstract class SearchScreenModel( abstract class SearchScreenModel(
@ -63,6 +70,9 @@ abstract class SearchScreenModel(
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val updateManga: UpdateManga = 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 <-- // KMK <--
) : StateScreenModel<SearchScreenModel.State>(initialState) { ) : StateScreenModel<SearchScreenModel.State>(initialState) {
@ -390,11 +400,97 @@ abstract class SearchScreenModel(
.orEmpty() .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?) { fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) } mutableState.update { it.copy(dialog = dialog) }
} }
sealed interface 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( data class ChangeMangasCategory(
val mangas: List<Manga>, val mangas: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,