diff --git a/CHANGELOG.md b/CHANGELOG.md index d81dad19a..8f75b00c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index 1d6454ada..ca094d403 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -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 = {}, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt index 505ce2f6f..32e88debc 100644 --- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt +++ b/app/src/main/java/eu/kanade/presentation/history/components/HistoryItem.kt @@ -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, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index 172ef0088..28c98f926 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -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(State()) { private val _events: Channel = 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 { + 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) { + screenModelScope.launchIO { + setMangaCategories.await(mangaId, categoryIds) + } + } + + fun moveMangaToCategoriesAndAddToLibrary(manga: Manga, categories: List) { + moveMangaToCategory(manga.id, categories) + if (manga.favorite) return + + screenModelScope.launchIO { + updateManga.awaitUpdateFavorite(manga.id, true) + } + } + + private suspend fun getMangaCategoryIds(manga: Manga): List { + 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>, + ) : Dialog } sealed interface Event { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index c2ac624ed..2a269f1da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -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().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 -> {} }