Add button to favorite manga from history screen (mihonapp/mihon#1733)

Co-authored-by: cuong-tran <cuongtran.tm@gmail.com>
(cherry picked from commit 7e71a34256e79b03a8a8ea50334b1ccece4b7154)
This commit is contained in:
Roshan Varughese 2025-02-25 05:20:20 +13:00 committed by Cuong-Tran
parent 405e1b8640
commit 3dded3568a
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
5 changed files with 178 additions and 0 deletions

View file

@ -14,6 +14,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Added
- 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))
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
### 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))

View file

@ -44,6 +44,7 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) {
// KMK -->
@ -113,6 +114,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
// KMK -->
usePanoramaCover = usePanoramaCover.value,
// KMK <--
@ -129,6 +131,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
// KMK -->
usePanoramaCover: Boolean,
// KMK <--
@ -161,6 +164,7 @@ private fun HistoryScreenContent(
onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
// KMK -->
usePanoramaCover = usePanoramaCover,
// KMK <--
@ -190,6 +194,7 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
onClickFavorite = {},
)
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -43,6 +44,7 @@ fun HistoryItem(
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier,
// KMK -->
usePanoramaCover: Boolean,
@ -122,6 +124,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) {
Icon(
imageVector = Icons.Outlined.Delete,
@ -145,6 +157,7 @@ private fun HistoryItemPreviews(
onClickCover = {},
onClickResume = {},
onClickDelete = {},
onClickFavorite = {},
usePanoramaCover = false,
)
}

View file

@ -1,9 +1,12 @@
package eu.kanade.tachiyomi.ui.history
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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.tachiyomi.util.lang.toLocalDate
import kotlinx.collections.immutable.ImmutableList
@ -20,21 +23,40 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
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.withIOContext
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.history.interactor.GetHistory
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.history.interactor.RemoveHistory
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.api.get
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 getManga: GetManga = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val libraryPreferences: LibraryPreferences = 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()) {
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
@ -114,6 +136,100 @@ class HistoryScreenModel(
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))
}
}
private 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
data class State(
val searchQuery: String? = null,
@ -124,6 +240,11 @@ class HistoryScreenModel(
sealed interface Dialog {
data object DeleteAll : 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
}
sealed interface Event {

View file

@ -20,11 +20,15 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.core.preference.asState
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.history.HistoryScreen
import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
import eu.kanade.presentation.history.components.HistoryDeleteDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@ -32,6 +36,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@ -85,6 +90,7 @@ data object HistoryTab : Tab {
onClickCover = { navigator.push(MangaScreen(it)) },
onClickResume = screenModel::getNextChapterForManga,
onDialogChange = screenModel::setDialog,
onClickFavorite = screenModel::addFavorite,
)
val onDismissRequest = { screenModel.setDialog(null) }
@ -107,6 +113,38 @@ data object HistoryTab : Tab {
onDelete = screenModel::removeAllHistory,
)
}
is HistoryScreenModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.addFavorite(dialog.manga)
},
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) },
onMigrate = {
// KMK -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
dialog.duplicate.id,
dialog.manga.id,
)
// KMK <--
},
// KMK -->
duplicate = dialog.duplicate,
// KMK <--
)
}
is HistoryScreenModel.Dialog.ChangeCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = { navigator.push(CategoryScreen()) },
onConfirm = { include, _ ->
screenModel.moveMangaToCategoriesAndAddToLibrary(dialog.manga, include)
},
)
}
null -> {}
}