Feat: allow long click to favorite on GlobalSearch
This commit is contained in:
parent
c30a8873a2
commit
3bc673ae17
4 changed files with 156 additions and 4 deletions
|
|
@ -62,7 +62,7 @@ fun GlobalSearchScreen(
|
||||||
onToggleResults = onToggleResults,
|
onToggleResults = onToggleResults,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
toggleBulkSelectionMode = screenModel::toggleSelectionMode
|
toggleBulkSelectionMode = screenModel::toggleSelectionMode,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>>,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue