mirror of
https://github.com/mihonapp/mihon.git
synced 2025-03-01 18:34:13 +01:00
Add button to favorite manga from history screen (#1733)
This commit is contained in:
parent
29ee53f461
commit
7e71a34256
@ -14,6 +14,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
### Added
|
### Added
|
||||||
- Add option to always decode long strip images with SSIV
|
- Add option to always decode long strip images with SSIV
|
||||||
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
|
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
|
||||||
|
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
|
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
|
||||||
|
@ -38,6 +38,7 @@ fun HistoryScreen(
|
|||||||
onSearchQueryChange: (String?) -> Unit,
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
onClickCover: (mangaId: Long) -> Unit,
|
onClickCover: (mangaId: Long) -> Unit,
|
||||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||||
|
onClickFavorite: (mangaId: Long) -> Unit,
|
||||||
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -84,6 +85,7 @@ fun HistoryScreen(
|
|||||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||||
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
||||||
|
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,6 +99,7 @@ private fun HistoryScreenContent(
|
|||||||
onClickCover: (HistoryWithRelations) -> Unit,
|
onClickCover: (HistoryWithRelations) -> Unit,
|
||||||
onClickResume: (HistoryWithRelations) -> Unit,
|
onClickResume: (HistoryWithRelations) -> Unit,
|
||||||
onClickDelete: (HistoryWithRelations) -> Unit,
|
onClickDelete: (HistoryWithRelations) -> Unit,
|
||||||
|
onClickFavorite: (HistoryWithRelations) -> Unit,
|
||||||
) {
|
) {
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
@ -126,6 +129,7 @@ private fun HistoryScreenContent(
|
|||||||
onClickCover = { onClickCover(value) },
|
onClickCover = { onClickCover(value) },
|
||||||
onClickResume = { onClickResume(value) },
|
onClickResume = { onClickResume(value) },
|
||||||
onClickDelete = { onClickDelete(value) },
|
onClickDelete = { onClickDelete(value) },
|
||||||
|
onClickFavorite = { onClickFavorite(value) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,6 +156,7 @@ internal fun HistoryScreenPreviews(
|
|||||||
onClickCover = {},
|
onClickCover = {},
|
||||||
onClickResume = { _, _ -> run {} },
|
onClickResume = { _, _ -> run {} },
|
||||||
onDialogChange = {},
|
onDialogChange = {},
|
||||||
|
onClickFavorite = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -39,6 +40,7 @@ fun HistoryItem(
|
|||||||
onClickCover: () -> Unit,
|
onClickCover: () -> Unit,
|
||||||
onClickResume: () -> Unit,
|
onClickResume: () -> Unit,
|
||||||
onClickDelete: () -> Unit,
|
onClickDelete: () -> Unit,
|
||||||
|
onClickFavorite: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@ -82,6 +84,16 @@ fun HistoryItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!history.coverData.isMangaFavorite) {
|
||||||
|
IconButton(onClick = onClickFavorite) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.FavoriteBorder,
|
||||||
|
contentDescription = stringResource(MR.strings.add_to_library),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IconButton(onClick = onClickDelete) {
|
IconButton(onClick = onClickDelete) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Delete,
|
imageVector = Icons.Outlined.Delete,
|
||||||
@ -105,6 +117,7 @@ private fun HistoryItemPreviews(
|
|||||||
onClickCover = {},
|
onClickCover = {},
|
||||||
onClickResume = {},
|
onClickResume = {},
|
||||||
onClickDelete = {},
|
onClickDelete = {},
|
||||||
|
onClickFavorite = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
package eu.kanade.tachiyomi.ui.history
|
package eu.kanade.tachiyomi.ui.history
|
||||||
|
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.core.util.insertSeparators
|
import eu.kanade.core.util.insertSeparators
|
||||||
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.presentation.history.HistoryUiModel
|
import eu.kanade.presentation.history.HistoryUiModel
|
||||||
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -18,21 +23,41 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
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.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
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 kotlin.collections.map
|
||||||
|
|
||||||
class HistoryScreenModel(
|
class HistoryScreenModel(
|
||||||
|
private val addTracks: AddTracks = Injekt.get(),
|
||||||
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
|
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
||||||
private val getHistory: GetHistory = Injekt.get(),
|
private val getHistory: GetHistory = Injekt.get(),
|
||||||
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
private val removeHistory: RemoveHistory = Injekt.get(),
|
private val removeHistory: RemoveHistory = Injekt.get(),
|
||||||
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
) : StateScreenModel<HistoryScreenModel.State>(State()) {
|
) : StateScreenModel<HistoryScreenModel.State>(State()) {
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
|
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
|
||||||
@ -112,6 +137,106 @@ class HistoryScreenModel(
|
|||||||
mutableState.update { it.copy(dialog = dialog) }
|
mutableState.update { it.copy(dialog = dialog) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user categories.
|
||||||
|
*
|
||||||
|
* @return List of categories, not including the default category
|
||||||
|
*/
|
||||||
|
suspend fun getCategories(): List<Category> {
|
||||||
|
return getCategories.await().filterNot { it.isSystemCategory }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveMangaToCategory(mangaId: Long, categories: Category?) {
|
||||||
|
val categoryIds = listOfNotNull(categories).map { it.id }
|
||||||
|
moveMangaToCategory(mangaId, categoryIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveMangaToCategory(mangaId: Long, categoryIds: List<Long>) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
setMangaCategories.await(mangaId, categoryIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List<Long>) {
|
||||||
|
moveMangaToCategory(manga.id, categories)
|
||||||
|
if (manga.favorite) return
|
||||||
|
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
updateManga.awaitUpdateFavorite(manga.id, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getMangaCategoryIds(manga: Manga): List<Long> {
|
||||||
|
return getCategories.await(manga.id)
|
||||||
|
.map { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFavorite(mangaId: Long) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
val manga = getManga.await(mangaId) ?: return@launchIO
|
||||||
|
|
||||||
|
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0)
|
||||||
|
if (duplicate != null) {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) }
|
||||||
|
return@launchIO
|
||||||
|
}
|
||||||
|
|
||||||
|
addFavorite(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFavorite(manga: Manga) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
// Move to default category if applicable
|
||||||
|
val categories = getCategories()
|
||||||
|
val defaultCategoryId = libraryPreferences.defaultCategory().get().toLong()
|
||||||
|
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
||||||
|
|
||||||
|
when {
|
||||||
|
// Default category set
|
||||||
|
defaultCategory != null -> {
|
||||||
|
val result = updateManga.awaitUpdateFavorite(manga.id, true)
|
||||||
|
if (!result) return@launchIO
|
||||||
|
moveMangaToCategory(manga.id, defaultCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatic 'Default' or no categories
|
||||||
|
defaultCategoryId == 0L || categories.isEmpty() -> {
|
||||||
|
val result = updateManga.awaitUpdateFavorite(manga.id, true)
|
||||||
|
if (!result) return@launchIO
|
||||||
|
moveMangaToCategory(manga.id, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a category
|
||||||
|
else -> showChangeCategoryDialog(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync with tracking services if applicable
|
||||||
|
addTracks.bindEnhancedTrackers(manga, sourceManager.getOrStub(manga.source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMigrateDialog(currentManga: Manga, duplicate: Manga) {
|
||||||
|
mutableState.update { currentState ->
|
||||||
|
currentState.copy(dialog = Dialog.Migrate(newManga = currentManga, oldManga = duplicate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showChangeCategoryDialog(manga: Manga) {
|
||||||
|
screenModelScope.launch {
|
||||||
|
val categories = getCategories()
|
||||||
|
val selection = getMangaCategoryIds(manga)
|
||||||
|
mutableState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
dialog = Dialog.ChangeCategory(
|
||||||
|
manga = manga,
|
||||||
|
initialSelection = categories.mapAsCheckboxState { it.id in selection }.toImmutableList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val searchQuery: String? = null,
|
val searchQuery: String? = null,
|
||||||
@ -122,6 +247,12 @@ class HistoryScreenModel(
|
|||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object DeleteAll : Dialog
|
data object DeleteAll : Dialog
|
||||||
data class Delete(val history: HistoryWithRelations) : Dialog
|
data class Delete(val history: HistoryWithRelations) : Dialog
|
||||||
|
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||||
|
data class ChangeCategory(
|
||||||
|
val manga: Manga,
|
||||||
|
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
|
) : Dialog
|
||||||
|
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
|
@ -16,11 +16,16 @@ import cafe.adriel.voyager.navigator.Navigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.history.HistoryScreen
|
import eu.kanade.presentation.history.HistoryScreen
|
||||||
import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
|
import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
|
||||||
import eu.kanade.presentation.history.components.HistoryDeleteDialog
|
import eu.kanade.presentation.history.components.HistoryDeleteDialog
|
||||||
|
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||||
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
@ -68,6 +73,7 @@ data object HistoryTab : Tab {
|
|||||||
onClickCover = { navigator.push(MangaScreen(it)) },
|
onClickCover = { navigator.push(MangaScreen(it)) },
|
||||||
onClickResume = screenModel::getNextChapterForManga,
|
onClickResume = screenModel::getNextChapterForManga,
|
||||||
onDialogChange = screenModel::setDialog,
|
onDialogChange = screenModel::setDialog,
|
||||||
|
onClickFavorite = screenModel::addFavorite,
|
||||||
)
|
)
|
||||||
|
|
||||||
val onDismissRequest = { screenModel.setDialog(null) }
|
val onDismissRequest = { screenModel.setDialog(null) }
|
||||||
@ -90,6 +96,38 @@ data object HistoryTab : Tab {
|
|||||||
onDelete = screenModel::removeAllHistory,
|
onDelete = screenModel::removeAllHistory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is HistoryScreenModel.Dialog.DuplicateManga -> {
|
||||||
|
DuplicateMangaDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onConfirm = {
|
||||||
|
screenModel.addFavorite(dialog.manga)
|
||||||
|
},
|
||||||
|
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
|
||||||
|
onMigrate = {
|
||||||
|
screenModel.showMigrateDialog(dialog.manga, dialog.duplicate)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HistoryScreenModel.Dialog.ChangeCategory -> {
|
||||||
|
ChangeCategoryDialog(
|
||||||
|
initialSelection = dialog.initialSelection,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onEditCategories = { navigator.push(CategoryScreen()) },
|
||||||
|
onConfirm = { include, _ ->
|
||||||
|
screenModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HistoryScreenModel.Dialog.Migrate -> {
|
||||||
|
MigrateDialog(
|
||||||
|
oldManga = dialog.oldManga,
|
||||||
|
newManga = dialog.newManga,
|
||||||
|
screenModel = MigrateDialogScreenModel(),
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||||
|
onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user