From 5ab24620ec9dbd4094e7af569d8308c955208ea4 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 31 May 2025 21:03:39 +0600 Subject: [PATCH] Cleanup migrate manga dialog and related code (mihonapp/mihon#2156) (cherry picked from commit 2b126f1ff56b63e470b48a04149e28c609f01148) Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../java/eu/kanade/domain/DomainModule.kt | 6 + .../source/service/SourcePreferences.kt | 12 +- .../browse/components/BulkFavoriteDialogs.kt | 22 +- .../ui/browse/BulkFavoriteScreenModel.kt | 4 +- .../ui/browse/migration/MigrationFlags.kt | 112 ----- .../design/MigrationBottomSheetDialog.kt | 33 +- .../advanced/process/MigrationListScreen.kt | 2 +- .../process/MigrationListScreenModel.kt | 47 +-- .../browse/migration/search/MigrateDialog.kt | 382 ------------------ .../migration/search/MigrateSearchScreen.kt | 15 +- .../MigrateSearchScreenDialogScreenModel.kt | 32 -- .../search/MigrateSearchScreenModel.kt | 2 +- ...Screen.kt => MigrateSourceSearchScreen.kt} | 6 +- .../source/browse/BrowseSourceScreen.kt | 14 +- .../source/browse/BrowseSourceScreenModel.kt | 2 +- .../source/globalsearch/SearchScreenModel.kt | 19 +- .../ui/history/HistoryScreenModel.kt | 6 +- .../kanade/tachiyomi/ui/history/HistoryTab.kt | 19 +- .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 14 +- .../tachiyomi/ui/manga/MangaScreenModel.kt | 4 +- .../exh/md/follows/MangaDexFollowsScreen.kt | 14 +- .../domain/migration/models/MigrationFlag.kt | 36 ++ .../migration/usecases/MigrateMangaUseCase.kt | 196 +++++++++ .../feature/common/utils/MigrationFlag.kt | 22 + .../migration/dialog/MigrateMangaDialog.kt | 178 ++++++++ .../common/preference/AndroidPreference.kt | 27 +- .../preference/AndroidPreferenceStore.kt | 23 +- .../preference/InMemoryPreferenceStore.kt | 14 +- .../core/common/preference/PreferenceStore.kt | 13 +- .../library/service/LibraryPreferences.kt | 4 +- 30 files changed, 611 insertions(+), 669 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/{SourceSearchScreen.kt => MigrateSourceSearchScreen.kt} (98%) create mode 100644 app/src/main/java/mihon/domain/migration/models/MigrationFlag.kt create mode 100644 app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt create mode 100644 app/src/main/java/mihon/feature/common/utils/MigrationFlag.kt create mode 100644 app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 9ed2be9b8..30c3244be 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -35,6 +35,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import mihon.domain.extensionrepo.service.ExtensionRepoService +import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.upcoming.interactor.GetUpcomingManga import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl @@ -138,6 +139,11 @@ class DomainModule : InjektModule { addFactory { SetMangaCategories(get()) } addFactory { GetExcludedScanlators(get()) } addFactory { SetExcludedScanlators(get()) } + addFactory { + MigrateMangaUseCase( + get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), + ) + } addSingletonFactory { ReleaseServiceImpl(get(), get()) } addFactory { GetApplicationRelease(get(), get()) } diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index 2dfb000d8..85dba78a2 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -3,6 +3,7 @@ package eu.kanade.domain.source.service import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.util.system.LocaleHelper +import mihon.domain.migration.models.MigrationFlag import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.getEnum @@ -13,7 +14,7 @@ class SourcePreferences( private val preferenceStore: PreferenceStore, ) { - fun sourceDisplayMode() = preferenceStore.getObject( + fun sourceDisplayMode() = preferenceStore.getObjectFromString( "pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, @@ -70,6 +71,13 @@ class SourcePreferences( false, ) + fun migrationFlags() = preferenceStore.getObjectFromInt( + key = "migrate_flags", + defaultValue = MigrationFlag.entries.toSet(), + serializer = { MigrationFlag.toBit(it) }, + deserializer = { value: Int -> MigrationFlag.fromBit(value) }, + ) + // KMK --> fun globalSearchPinnedState() = preferenceStore.getEnum( Preference.appStateKey("global_search_pinned_toggle_state"), @@ -112,8 +120,6 @@ class SourcePreferences( WSRV_NL, } - fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE) - fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false) fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt index 38a8ac438..2dfce18a0 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BulkFavoriteDialogs.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator @@ -18,10 +17,9 @@ import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel.Dialog -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.manga.MangaScreen +import mihon.feature.migration.dialog.MigrateMangaDialog import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.i18n.stringResource @@ -89,7 +87,6 @@ fun Screen.BulkFavoriteDialogs( dialog = dialog, navigator = navigator, state = bulkFavoriteState, - migrateScreenModel = rememberScreenModel { MigrateDialogScreenModel() }, onDismiss = bulkFavoriteScreenModel::dismissDialog, stopRunning = bulkFavoriteScreenModel::stopRunning, toggleSelection = bulkFavoriteScreenModel::toggleSelection, @@ -101,11 +98,10 @@ fun Screen.BulkFavoriteDialogs( } @Composable -private fun ShowMigrateDialog( +private fun Screen.ShowMigrateDialog( dialog: Dialog.Migrate, navigator: Navigator?, state: BulkFavoriteScreenModel.State, - migrateScreenModel: MigrateDialogScreenModel, onDismiss: () -> Unit, stopRunning: () -> Unit, toggleSelection: (Manga, toSelectedState: Boolean) -> Unit, @@ -113,14 +109,14 @@ private fun ShowMigrateDialog( ) { stopRunning() - MigrateDialog( - oldManga = dialog.oldManga, - newManga = dialog.newManga, - screenModel = migrateScreenModel, + MigrateMangaDialog( + current = dialog.current, + target = dialog.target, + // Initiated from the context of [dialog.target] so we show [dialog.current]. + onClickTitle = { navigator?.push(MangaScreen(dialog.current.id)) }, onDismissRequest = onDismiss, - onClickTitle = { navigator?.push(MangaScreen(dialog.oldManga.id)) }, - onPopScreen = { - toggleSelection(dialog.newManga, false) + onComplete = { + toggleSelection(dialog.target, false) onDismiss() // `selection.size` is at current value before calling above `toggleSelection` if (state.selection.size > 1) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt index bb9d2f8cb..0d0d06047 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BulkFavoriteScreenModel.kt @@ -378,7 +378,7 @@ class BulkFavoriteScreenModel( } internal fun showMigrateDialog(manga: Manga, duplicate: Manga) { - setDialog(Dialog.Migrate(newManga = manga, oldManga = duplicate)) + setDialog(Dialog.Migrate(target = manga, current = duplicate)) } private fun setDialog(dialog: Dialog?) { @@ -406,7 +406,7 @@ class BulkFavoriteScreenModel( } sealed interface Dialog { - data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog + data class Migrate(val target: Manga, val current: Manga) : Dialog data class AddDuplicateManga(val manga: Manga, val duplicates: List) : Dialog data class BulkAllowDuplicate(val manga: Manga, val duplicates: List, val currentIdx: Int) : Dialog data class RemoveManga(val manga: Manga) : Dialog diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt deleted file mode 100644 index 6bfd62654..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt +++ /dev/null @@ -1,112 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration - -import dev.icerock.moko.resources.StringResource -import eu.kanade.domain.manga.model.hasCustomCover -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.download.DownloadCache -import tachiyomi.domain.manga.model.Manga -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import uy.kohesive.injekt.injectLazy - -data class MigrationFlag( - val flag: Int, - val isDefaultSelected: Boolean, - val titleId: StringResource, -) { - companion object { - fun create(flag: Int, defaultSelectionMap: Int, titleId: StringResource): MigrationFlag { - return MigrationFlag( - flag = flag, - isDefaultSelected = defaultSelectionMap and flag != 0, - titleId = titleId, - ) - } - } -} - -object MigrationFlags { - - const val CHAPTERS = 0b00001 - const val CATEGORIES = 0b00010 - - // KMK --> - const val TRACK = 0b00100 - // KMK <-- - - const val CUSTOM_COVER = 0b01000 - const val DELETE_DOWNLOADED = 0b10000 - - // KMK --> - const val EXTRA = 0b1000000 - // KMK <-- - - private val coverCache: CoverCache by injectLazy() - private val downloadCache: DownloadCache by injectLazy() - - fun hasChapters(value: Int): Boolean { - return value and CHAPTERS != 0 - } - - fun hasCategories(value: Int): Boolean { - return value and CATEGORIES != 0 - } - - // KMK --> - fun hasTracks(value: Int): Boolean { - return value and TRACK != 0 - } - // KMK <-- - - fun hasCustomCover(value: Int): Boolean { - return value and CUSTOM_COVER != 0 - } - - fun hasDeleteDownloaded(value: Int): Boolean { - return value and DELETE_DOWNLOADED != 0 - } - - // KMK --> - /** Returns true if the manga has extra info, include notes. */ - fun hasExtra(value: Int): Boolean { - return value and EXTRA != 0 - } - // KMK <-- - - /** Returns information about applicable flags with default selections. */ - fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List { - val flags = mutableListOf() - flags += MigrationFlag.create(CHAPTERS, defaultSelectedBitMap, MR.strings.chapters) - flags += MigrationFlag.create(CATEGORIES, defaultSelectedBitMap, MR.strings.categories) - // KMK --> - flags += MigrationFlag.create(TRACK, defaultSelectedBitMap, MR.strings.track) - // KMK <-- - - if (manga != null) { - if (manga.hasCustomCover(coverCache)) { - flags += MigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, MR.strings.custom_cover) - } - if (downloadCache.getDownloadCount(manga) > 0) { - flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, MR.strings.delete_downloaded) - } - } - - // KMK --> - flags += MigrationFlag.create(EXTRA, defaultSelectedBitMap, SYMR.strings.log_extra) - // KMK <-- - - return flags - } - - /** Returns a bit map of selected flags. */ - fun getSelectedFlagsBitMap( - selectedFlags: List, - flags: List, - ): Int { - return selectedFlags - .zip(flags) - .filter { (isSelected, _) -> isSelected } - .map { (_, flag) -> flag.flag } - .reduceOrNull { acc, mask -> acc or mask } ?: 0 - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt index 422fd5ddc..15f3f96eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt @@ -21,8 +21,8 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding -import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags import eu.kanade.tachiyomi.util.system.toast +import mihon.domain.migration.models.MigrationFlag import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.util.lang.toLong @@ -116,15 +116,15 @@ class MigrationBottomSheetDialogState( * Init general reader preferences. */ fun initPreferences(binding: MigrationBottomSheetBinding) { - val flags = preferences.migrateFlags().get() + val flags = preferences.migrationFlags().get() with(binding) { - migChapters.isChecked = MigrationFlags.hasChapters(flags) - migCategories.isChecked = MigrationFlags.hasCategories(flags) - migTracking.isChecked = MigrationFlags.hasTracks(flags) - migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags) - migExtra.isChecked = MigrationFlags.hasExtra(flags) - migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteDownloaded(flags) + migChapters.isChecked = MigrationFlag.CHAPTER in flags + migCategories.isChecked = MigrationFlag.CATEGORY in flags + migTracking.isChecked = MigrationFlag.TRACK in flags + migCustomCover.isChecked = MigrationFlag.CUSTOM_COVER in flags + migExtra.isChecked = MigrationFlag.EXTRA in flags + migDeleteDownloaded.isChecked = MigrationFlag.REMOVE_DOWNLOAD in flags listOf( migChapters, @@ -183,17 +183,16 @@ class MigrationBottomSheetDialogState( } private fun setFlags(binding: MigrationBottomSheetBinding) { - var flags = 0 + val flags = mutableSetOf() with(binding) { - @Suppress("KotlinConstantConditions") - if (migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS - if (migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES - if (migTracking.isChecked) flags = flags or MigrationFlags.TRACK - if (migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER - if (migExtra.isChecked) flags = flags or MigrationFlags.EXTRA - if (migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_DOWNLOADED + if (migChapters.isChecked) flags.add(MigrationFlag.CHAPTER) + if (migCategories.isChecked) flags.add(MigrationFlag.CATEGORY) + if (migTracking.isChecked) flags.add(MigrationFlag.TRACK) + if (migCustomCover.isChecked) flags.add(MigrationFlag.CUSTOM_COVER) + if (migExtra.isChecked) flags.add(MigrationFlag.EXTRA) + if (migDeleteDownloaded.isChecked) flags.add(MigrationFlag.REMOVE_DOWNLOAD) } - preferences.migrateFlags().set(flags) + preferences.migrationFlags().set(flags) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt index 7e82bece6..9c4bad310 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt @@ -27,7 +27,7 @@ import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.i18n.sy.SYMR /** - * Screen showing a list of pair of source-dest manga entries being migrated. + * Screen showing a list of pair of current-target manga entries being migrated. */ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt index 33e0c929f..49607cf62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt @@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel.Companion.migrateMangaInternal import eu.kanade.tachiyomi.util.system.toast import exh.smartsearch.SmartSourceSearchEngine import exh.source.MERGED_SOURCE_ID @@ -32,6 +31,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import logcat.LogPriority +import mihon.domain.migration.usecases.MigrateMangaUseCase import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat @@ -57,6 +57,7 @@ class MigrationListScreenModel( private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), + private val migrateManga: MigrateMangaUseCase = Injekt.get(), ) : ScreenModel { private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams) @@ -383,33 +384,18 @@ class MigrationListScreenModel( migratingProgress.value = 0f val items = migratingItems.value.orEmpty() try { - items.forEachIndexed { index, oldManga -> + items.forEachIndexed { index, current -> try { ensureActive() - val newManga = oldManga.searchResult.value.let { + val target = current.searchResult.value.let { if (it is SearchResult.Result) { getManga.await(it.id) } else { null } } - if (newManga != null) { - val source = sourceManager.get(newManga.source) ?: return@launchIO - val prevSource = sourceManager.get(oldManga.manga.source) - - try { - val chapters = source.getChapterList(newManga.toSManga()) - - migrateMangaInternal( - oldSource = prevSource, - newSource = source, - oldManga = oldManga.manga, - newManga = newManga, - sourceChapters = chapters, - replace = replace, - ) - } catch (_: Throwable) { - } + if (target != null) { + migrateManga(current.manga, target, replace) } } catch (e: Exception) { if (e is CancellationException) throw e @@ -438,28 +424,13 @@ class MigrationListScreenModel( fun migrateManga(mangaId: Long, replace: Boolean) { manualMigrations.value++ screenModelScope.launchIO { - val oldManga = migratingItems.value.orEmpty().find { it.manga.id == mangaId } + val current = migratingItems.value.orEmpty().find { it.manga.id == mangaId } ?: return@launchIO - val newManga = getManga.await((oldManga.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO) + val target = getManga.await((current.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO) ?: return@launchIO - val source = sourceManager.get(newManga.source) ?: return@launchIO - val prevSource = sourceManager.get(oldManga.manga.source) - - try { - val chapters = source.getChapterList(newManga.toSManga()) - - migrateMangaInternal( - oldSource = prevSource, - newSource = source, - oldManga = oldManga.manga, - newManga = newManga, - sourceChapters = chapters, - replace = replace, - ) - } catch (_: Throwable) { - } + migrateManga(current.manga, target, replace) removeManga(mangaId) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt deleted file mode 100644 index 964770a8b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt +++ /dev/null @@ -1,382 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.search - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Modifier -import cafe.adriel.voyager.core.model.StateScreenModel -import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource -import eu.kanade.domain.manga.interactor.UpdateManga -import eu.kanade.domain.manga.model.hasCustomCover -import eu.kanade.domain.manga.model.toSManga -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.track.EnhancedTracker -import eu.kanade.tachiyomi.data.track.TrackerManager -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags -import exh.log.LogLevel -import exh.log.xLog -import kotlinx.coroutines.flow.update -import tachiyomi.core.common.preference.Preference -import tachiyomi.core.common.util.lang.launchIO -import tachiyomi.core.common.util.lang.withUIContext -import tachiyomi.domain.category.interactor.GetCategories -import tachiyomi.domain.category.interactor.SetMangaCategories -import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId -import tachiyomi.domain.chapter.interactor.UpdateChapter -import tachiyomi.domain.chapter.model.ChapterUpdate -import tachiyomi.domain.history.interactor.GetHistory -import tachiyomi.domain.history.interactor.UpsertHistory -import tachiyomi.domain.history.model.HistoryUpdate -import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.manga.model.MangaUpdate -import tachiyomi.domain.source.service.SourceManager -import tachiyomi.domain.track.interactor.GetTracks -import tachiyomi.domain.track.interactor.InsertTrack -import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.LabeledCheckbox -import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.screens.LoadingScreen -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.time.Instant - -@Composable -internal fun MigrateDialog( - oldManga: Manga, - newManga: Manga, - screenModel: MigrateDialogScreenModel, - onDismissRequest: () -> Unit, - onClickTitle: () -> Unit, - onPopScreen: () -> Unit, -) { - val scope = rememberCoroutineScope() - val state by screenModel.state.collectAsState() - - val flags = remember { MigrationFlags.getFlags(oldManga, screenModel.migrateFlags.get()) } - val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() } - - if (state.isMigrating) { - LoadingScreen( - modifier = Modifier - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), - ) - } else { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { - Text(text = stringResource(MR.strings.migration_dialog_what_to_include)) - }, - text = { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - flags.forEachIndexed { index, flag -> - LabeledCheckbox( - label = stringResource(flag.titleId), - checked = selectedFlags[index], - onCheckedChange = { selectedFlags[index] = it }, - ) - } - } - }, - confirmButton = { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), - ) { - TextButton( - onClick = { onClickTitle() }, - ) { - Text(text = stringResource(MR.strings.action_show_manga)) - } - - Spacer(modifier = Modifier.weight(1f)) - - TextButton( - onClick = { - scope.launchIO { - screenModel.migrateManga( - oldManga, - newManga, - false, - MigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags), - ) - withUIContext { onPopScreen() } - } - }, - ) { - Text(text = stringResource(MR.strings.copy)) - } - TextButton( - onClick = { - scope.launchIO { - screenModel.migrateManga( - oldManga, - newManga, - true, - MigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags), - ) - - withUIContext { onPopScreen() } - } - }, - ) { - Text(text = stringResource(MR.strings.migrate)) - } - } - }, - ) - } -} - -internal class MigrateDialogScreenModel( - private val sourceManager: SourceManager = Injekt.get(), - // KMK --> - sourcePreferences: SourcePreferences = Injekt.get(), - // KMK <-- -) : StateScreenModel(State()) { - - val migrateFlags: Preference by lazy { - // KMK --> - sourcePreferences.migrateFlags() - // KMK <-- - } - - suspend fun migrateManga( - oldManga: Manga, - newManga: Manga, - replace: Boolean, - flags: Int, - ) { - migrateFlags.set(flags) - val source = sourceManager.get(newManga.source) ?: return - val prevSource = sourceManager.get(oldManga.source) - - mutableState.update { it.copy(isMigrating = true) } - - try { - val chapters = source.getChapterList(newManga.toSManga()) - - migrateMangaInternal( - oldSource = prevSource, - newSource = source, - oldManga = oldManga, - newManga = newManga, - sourceChapters = chapters, - replace = replace, - presetFlags = flags, - ) - } catch (e: Throwable) { - // Explicitly stop if an error occurred; the dialog normally gets popped at the end anyway - // KMK --> - xLog(LogLevel.Error, "Failed to migrate manga ${oldManga.title} to ${newManga.title}", e) - } finally { - // KMK <-- - mutableState.update { it.copy(isMigrating = false) } - } - } - - companion object { - suspend fun migrateMangaInternal( - oldSource: Source?, - newSource: Source, - oldManga: Manga, - newManga: Manga, - sourceChapters: List, - replace: Boolean, - // KMK --> - presetFlags: Int? = null, - // KMK <-- - ) { - // KMK --> - if (oldManga.id == newManga.id) return // Nothing to migrate - - val sourcePreferences: SourcePreferences = Injekt.get() - val downloadManager: DownloadManager = Injekt.get() - val coverCache: CoverCache = Injekt.get() - val updateManga: UpdateManga = Injekt.get() - val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get() - val updateChapter: UpdateChapter = Injekt.get() - val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get() - val getHistory: GetHistory = Injekt.get() - val upsertHistory: UpsertHistory = Injekt.get() - val getCategories: GetCategories = Injekt.get() - val setMangaCategories: SetMangaCategories = Injekt.get() - val getTracks: GetTracks = Injekt.get() - val insertTrack: InsertTrack = Injekt.get() - val enhancedServices by lazy { - Injekt.get().trackers.filterIsInstance() - } - - val flags = presetFlags ?: sourcePreferences.migrateFlags().get() - // KMK <-- - - val migrateChapters = MigrationFlags.hasChapters(flags) - val migrateCategories = MigrationFlags.hasCategories(flags) - val migrateCustomCover = MigrationFlags.hasCustomCover(flags) - val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags) - // KMK --> - val migrateTracks = MigrationFlags.hasTracks(flags) - val migrateExtra = MigrationFlags.hasExtra(flags) - // KMK <-- - - // Update newManga's chapters first to ensure it has latest chapter list - try { - syncChaptersWithSource.await(sourceChapters, newManga, newSource) - } catch (_: Exception) { - // Worst case, chapters won't be synced - } - - // Update chapters read, bookmark and dateFetch - if (migrateChapters) { - val prevMangaChapters = getChaptersByMangaId.await(oldManga.id) - val mangaChapters = getChaptersByMangaId.await(newManga.id) - - val maxChapterRead = prevMangaChapters - .filter { it.read } - .maxOfOrNull { it.chapterNumber } - - // SY --> - val prevHistoryList = getHistory.await(oldManga.id) - // KMK --> - .associateBy { it.chapterId } - // KMK <-- - - val chapterUpdates = mutableListOf() - val historyUpdates = mutableListOf() - - mangaChapters.forEach { updatedChapter -> - // SY <-- - if (updatedChapter.isRecognizedNumber) { - val prevChapter = prevMangaChapters - .find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber } - - if (prevChapter != null) { - // SY --> - // If chapters match then mark new manga's chapters read/unread as old one - chapterUpdates += ChapterUpdate( - id = updatedChapter.id, - // Also migrate read status - read = prevChapter.read, - // SY <-- - dateFetch = prevChapter.dateFetch, - bookmark = prevChapter.bookmark, - ) - // SY --> - // KMK --> - prevHistoryList[prevChapter.id]?.let { prevHistory -> - // KMK <-- - historyUpdates += HistoryUpdate( - updatedChapter.id, - prevHistory.readAt ?: return@let, - prevHistory.readDuration, - ) - } - // SY <-- - } else if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) { - // SY --> - // If chapters which only present on new manga then mark read up to latest read chapter number - chapterUpdates += ChapterUpdate( - id = updatedChapter.id, - // SY <-- - read = true, - ) - } - } - } - - updateChapter.awaitAll(chapterUpdates) - // SY --> - upsertHistory.awaitAll(historyUpdates) - // SY <-- - } - - // Update categories - if (migrateCategories) { - val categoryIds = getCategories.await(oldManga.id).map { it.id } - setMangaCategories.await(newManga.id, categoryIds) - } - - // Update track - // SY --> - if (migrateTracks) { - // SY <-- - getTracks.await(oldManga.id).mapNotNull { track -> - val updatedTrack = track.copy(mangaId = newManga.id) - - // Kavita, Komga, Suwayomi - val service = enhancedServices - .firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) } - - if (service != null) { - service.migrateTrack(updatedTrack, newManga, newSource) - } else { - updatedTrack - } - } - .takeIf { it.isNotEmpty() } - ?.let { insertTrack.awaitAll(it) } - } - - // Delete downloaded - if (deleteDownloaded) { - if (oldSource != null) { - downloadManager.deleteManga(oldManga, oldSource) - } - } - - if (replace) { - updateManga.awaitUpdateFavorite(oldManga.id, favorite = false) - } - - // Update custom cover (recheck if custom cover exists) - if (migrateCustomCover && oldManga.hasCustomCover()) { - coverCache.setCustomCoverToCache(newManga, coverCache.getCustomCoverFile(oldManga.id).inputStream()) - } - - updateManga.await( - MangaUpdate( - id = newManga.id, - favorite = true, - chapterFlags = oldManga.chapterFlags - // KMK --> - .takeIf { migrateExtra }, - // KMK <-- - viewerFlags = oldManga.viewerFlags - // KMK --> - .takeIf { migrateExtra }, - // KMK <-- - dateAdded = if (replace) oldManga.dateAdded else Instant.now().toEpochMilli(), - notes = oldManga.notes - // KMK --> - .takeIf { migrateExtra }, - // KMK <-- - ), - ) - } - } - - @Immutable - data class State( - val isMigrating: Boolean = false, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index 000f61fe7..6d128e6ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -19,12 +19,9 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow - val screenModel = - rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) } - val state by screenModel.state.collectAsState() - val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) } - val dialogState by dialogScreenModel.state.collectAsState() + val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) } + val state by screenModel.state.collectAsState() // KMK --> val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } @@ -37,18 +34,14 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() { MigrateSearchScreen( state = state, - fromSourceId = state.fromSourceId, + fromSourceId = state.from?.source, navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, onSearch = { screenModel.search() }, getManga = { screenModel.getManga(it) }, onChangeSearchFilter = screenModel::setSourceFilter, onToggleResults = screenModel::toggleFilterResults, - onClickSource = { - // SY --> - navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery)) - // SY <-- - }, + onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) }, onClickItem = { manga -> // KMK --> if (bulkFavoriteState.selectionMode) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt deleted file mode 100644 index 9ef99f520..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenDialogScreenModel.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.search - -import androidx.compose.runtime.Immutable -import cafe.adriel.voyager.core.model.StateScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import tachiyomi.domain.manga.interactor.GetManga -import tachiyomi.domain.manga.model.Manga -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class MigrateSearchScreenDialogScreenModel( - val mangaId: Long, - getManga: GetManga = Injekt.get(), -) : StateScreenModel(State()) { - - init { - screenModelScope.launch { - val manga = getManga.await(mangaId)!! - - mutableState.update { - it.copy(manga = manga) - } - } - } - - @Immutable - data class State( - val manga: Manga? = null, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index af2907f3f..005ec6f6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -33,7 +33,7 @@ class MigrateSearchScreenModel( val manga = getManga.await(mangaId)!! mutableState.update { it.copy( - fromSourceId = manga.source, + from = manga, searchQuery = manga.title, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt index f8a8fd34b..100915cd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt @@ -43,8 +43,8 @@ import tachiyomi.source.local.LocalSource /** * Opened when click on a source in [MigrateSearchScreen] while doing manual search for migration */ -data class SourceSearchScreen( - private val oldManga: Manga, +data class MigrateSourceSearchScreen( + private val currentManga: Manga, private val sourceId: Long, private val query: String?, ) : Screen() { @@ -135,7 +135,7 @@ data class SourceSearchScreen( navigator.items .filterIsInstance() .last() - .newSelectedItem = oldManga.id to it.id + .newSelectedItem = currentManga.id to it.id navigator.popUntil { it is MigrationListScreen } // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index f745105e4..cc875e266 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -56,8 +56,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.category.CategoryScreen @@ -73,6 +71,7 @@ import exh.ui.smartsearch.SmartSearchScreen import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow +import mihon.feature.migration.dialog.MigrateMangaDialog import mihon.presentation.core.util.collectAsLazyPagingItems import tachiyomi.core.common.Constants import tachiyomi.core.common.util.lang.launchIO @@ -462,13 +461,12 @@ data class BrowseSourceScreen( } is BrowseSourceScreenModel.Dialog.Migrate -> { - MigrateDialog( - oldManga = dialog.oldManga, - newManga = dialog.newManga, - screenModel = rememberScreenModel { MigrateDialogScreenModel() }, + MigrateMangaDialog( + current = dialog.current, + target = dialog.target, + // Initiated from the context of [dialog.target] so we show [dialog.current]. + onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) }, onDismissRequest = onDismissRequest, - onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) }, - onPopScreen = { onDismissRequest() }, ) } is BrowseSourceScreenModel.Dialog.RemoveManga -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index a03e758e3..9e6c79425 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -530,7 +530,7 @@ open class BrowseSourceScreenModel( val manga: Manga, val initialSelection: ImmutableList>, ) : Dialog - data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog + data class Migrate(val target: Manga, val current: Manga) : Dialog // SY --> data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 5c91d0291..2b5e79a37 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.withContext import mihon.domain.manga.model.toDomainManga import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.util.QuerySanitizer.sanitize +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.model.Manga @@ -223,19 +224,35 @@ abstract class SearchScreenModel( updateItems(newItems) } + fun setMigrateDialog(currentId: Long, target: Manga) { + screenModelScope.launchIO { + val current = getManga.await(currentId) ?: return@launchIO + mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) } + } + } + + fun clearDialog() { + mutableState.update { it.copy(dialog = null) } + } + @Immutable data class State( - val fromSourceId: Long? = null, + val from: Manga? = null, val searchQuery: String? = null, val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val onlyShowHasResults: Boolean = false, val items: PersistentMap = persistentMapOf(), + val dialog: Dialog? = null, ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } val total: Int = items.size val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) } .toImmutableMap() } + + sealed interface Dialog { + data class Migrate(val target: Manga, val current: Manga) : Dialog + } } enum class SourceFilter { 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 5f9fe9967..7db4fdec3 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 @@ -216,9 +216,9 @@ class HistoryScreenModel( } } - fun showMigrateDialog(currentManga: Manga, duplicate: Manga) { + fun showMigrateDialog(target: Manga, current: Manga) { mutableState.update { currentState -> - currentState.copy(dialog = Dialog.Migrate(newManga = currentManga, oldManga = duplicate)) + currentState.copy(dialog = Dialog.Migrate(target = target, current = current)) } } @@ -252,7 +252,7 @@ class HistoryScreenModel( val manga: Manga, val initialSelection: ImmutableList>, ) : Dialog - data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog + data class Migrate(val target: Manga, val current: Manga) : 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 125af6f68..d2634bdd5 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 @@ -29,8 +29,6 @@ import eu.kanade.presentation.util.Tab import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.connections.discord.DiscordRPCService import eu.kanade.tachiyomi.data.connections.discord.DiscordScreen -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.manga.MangaScreen @@ -38,6 +36,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow +import mihon.feature.migration.dialog.MigrateMangaDialog import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.domain.chapter.model.Chapter @@ -47,6 +46,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get data object HistoryTab : Tab { + @Suppress("unused") private fun readResolve(): Any = HistoryTab private val snackbarHostState = SnackbarHostState() @@ -120,9 +120,7 @@ data object HistoryTab : Tab { DuplicateMangaDialog( duplicates = dialog.duplicates, onDismissRequest = onDismissRequest, - onConfirm = { - screenModel.addFavorite(dialog.manga) - }, + onConfirm = { screenModel.addFavorite(dialog.manga) }, onOpenManga = { navigator.push(MangaScreen(it.id)) }, onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) }, // KMK --> @@ -141,13 +139,12 @@ data object HistoryTab : Tab { ) } is HistoryScreenModel.Dialog.Migrate -> { - MigrateDialog( - oldManga = dialog.oldManga, - newManga = dialog.newManga, - screenModel = rememberScreenModel { MigrateDialogScreenModel() }, + MigrateMangaDialog( + current = dialog.current, + target = dialog.target, + // Initiated from the context of [dialog.target] so we show [dialog.current]. + onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) }, onDismissRequest = onDismissRequest, - onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) }, - onPopScreen = onDismissRequest, ) } null -> {} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 34d09060e..4e1504fc3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -69,8 +69,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen @@ -104,6 +102,7 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import logcat.LogPriority import mihon.feature.migration.config.MigrationConfigScreen +import mihon.feature.migration.dialog.MigrateMangaDialog import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchUI @@ -513,13 +512,12 @@ class MangaScreen( } is MangaScreenModel.Dialog.Migrate -> { - MigrateDialog( - oldManga = dialog.oldManga, - newManga = dialog.newManga, - screenModel = rememberScreenModel { MigrateDialogScreenModel() }, + MigrateMangaDialog( + current = dialog.current, + target = dialog.target, + // Initiated from the context of [dialog.target] so we show [dialog.current]. + onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) }, onDismissRequest = onDismissRequest, - onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) }, - onPopScreen = onDismissRequest, ) } MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index ea6a14688..f2d6f4934 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -1898,7 +1898,7 @@ class MangaScreenModel( ) : Dialog data class DeleteChapters(val chapters: List) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List) : Dialog - data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog + data class Migrate(val target: Manga, val current: Manga) : Dialog data class SetFetchInterval(val manga: Manga) : Dialog // SY --> @@ -1937,7 +1937,7 @@ class MangaScreenModel( fun showMigrateDialog(duplicate: Manga) { val manga = successState?.manga ?: return - updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) } + updateSuccessState { it.copy(dialog = Dialog.Migrate(target = manga, current = duplicate)) } } fun setExcludedScanlators(excludedScanlators: Set) { diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index db4cdbe2a..71b907bb6 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -24,11 +24,10 @@ import eu.kanade.presentation.components.BulkSelectionToolbar import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen +import mihon.feature.migration.dialog.MigrateMangaDialog import mihon.presentation.core.util.collectAsLazyPagingItems import tachiyomi.core.common.util.lang.launchIO import tachiyomi.i18n.sy.SYMR @@ -167,13 +166,12 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() { } // KMK --> is BrowseSourceScreenModel.Dialog.Migrate -> { - MigrateDialog( - oldManga = dialog.oldManga, - newManga = dialog.newManga, - screenModel = rememberScreenModel { MigrateDialogScreenModel() }, + MigrateMangaDialog( + current = dialog.current, + target = dialog.target, + // Initiated from the context of [dialog.target] so we show [dialog.current]. + onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) }, onDismissRequest = onDismissRequest, - onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) }, - onPopScreen = { onDismissRequest() }, ) } // KMK <-- diff --git a/app/src/main/java/mihon/domain/migration/models/MigrationFlag.kt b/app/src/main/java/mihon/domain/migration/models/MigrationFlag.kt new file mode 100644 index 000000000..332a81ac5 --- /dev/null +++ b/app/src/main/java/mihon/domain/migration/models/MigrationFlag.kt @@ -0,0 +1,36 @@ +package mihon.domain.migration.models + +enum class MigrationFlag(val flag: Int) { + CHAPTER(0b00001), + CATEGORY(0b00010), + + // KMK --> + TRACK(0b00100), + // KMK <-- + + // 0b00100 was used for manga trackers + CUSTOM_COVER(0b01000), + NOTES(0b100000), + REMOVE_DOWNLOAD(0b10000), + + // KMK --> + EXTRA(0b1000000), + // KMK <-- + ; + + companion object { + fun fromBit(bit: Int): Set { + return buildSet { + entries.forEach { entry -> + if (bit and entry.flag != 0) add(entry) + } + } + } + + fun toBit(flags: Set): Int { + return flags.map { it.flag } + .reduceOrNull { acc, mask -> acc or mask } + ?: 0 + } + } +} diff --git a/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt new file mode 100644 index 000000000..4de4a5fc8 --- /dev/null +++ b/app/src/main/java/mihon/domain/migration/usecases/MigrateMangaUseCase.kt @@ -0,0 +1,196 @@ +package mihon.domain.migration.usecases + +import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.hasCustomCover +import eu.kanade.domain.manga.model.toSManga +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.track.EnhancedTracker +import eu.kanade.tachiyomi.data.track.TrackerManager +import kotlinx.coroutines.CancellationException +import mihon.domain.migration.models.MigrationFlag +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.interactor.SetMangaCategories +import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId +import tachiyomi.domain.chapter.interactor.UpdateChapter +import tachiyomi.domain.chapter.model.toChapterUpdate +import tachiyomi.domain.history.interactor.GetHistory +import tachiyomi.domain.history.interactor.UpsertHistory +import tachiyomi.domain.history.model.HistoryUpdate +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.manga.model.MangaUpdate +import tachiyomi.domain.source.service.SourceManager +import tachiyomi.domain.track.interactor.GetTracks +import tachiyomi.domain.track.interactor.InsertTrack +import java.time.Instant + +class MigrateMangaUseCase( + private val sourcePreferences: SourcePreferences, + private val trackerManager: TrackerManager, + private val sourceManager: SourceManager, + private val downloadManager: DownloadManager, + private val updateManga: UpdateManga, + private val getChaptersByMangaId: GetChaptersByMangaId, + private val syncChaptersWithSource: SyncChaptersWithSource, + private val updateChapter: UpdateChapter, + private val getCategories: GetCategories, + private val setMangaCategories: SetMangaCategories, + private val getTracks: GetTracks, + private val insertTrack: InsertTrack, + private val coverCache: CoverCache, + // KMK --> + private val getHistory: GetHistory, + private val upsertHistory: UpsertHistory, + // KMK <-- +) { + private val enhancedServices by lazy { trackerManager.trackers.filterIsInstance() } + + suspend operator fun invoke( + current: Manga, + target: Manga, + replace: Boolean, + // KMK --> + presetFlags: Set? = null, + // KMK <-- + ) { + val targetSource = sourceManager.get(target.source) ?: return + val currentSource = sourceManager.get(current.source) + val flags = /* KMK --> */ presetFlags ?: /* KMK <-- */ sourcePreferences.migrationFlags().get() + + try { + val chapters = targetSource.getChapterList(target.toSManga()) + + try { + syncChaptersWithSource.await(chapters, target, targetSource) + } catch (_: Exception) { + // Worst case, chapters won't be synced + } + + // Update chapters read, bookmark and dateFetch + if (MigrationFlag.CHAPTER in flags) { + val prevMangaChapters = getChaptersByMangaId.await(current.id) + val mangaChapters = getChaptersByMangaId.await(target.id) + + val maxChapterRead = prevMangaChapters + .filter { it.read } + .maxOfOrNull { it.chapterNumber } + + // SY --> + val historyUpdates = mutableListOf() + val prevHistoryList = getHistory.await(current.id) + // SY <-- + // KMK --> + .associateBy { it.chapterId } + // KMK <-- + + val updatedMangaChapters = mangaChapters.map { mangaChapter -> + var updatedChapter = mangaChapter + if (updatedChapter.isRecognizedNumber) { + val prevChapter = prevMangaChapters + .find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber } + + if (prevChapter != null) { + updatedChapter = updatedChapter.copy( + // SY --> + // If chapters match then mark new manga's chapters read/unread as old one + read = prevChapter.read, + // SY <-- + dateFetch = prevChapter.dateFetch, + bookmark = prevChapter.bookmark, + ) + // SY --> + // KMK --> + prevHistoryList[prevChapter.id]?.let { prevHistory -> + // KMK <-- + historyUpdates += HistoryUpdate( + mangaChapter.id, + prevHistory.readAt ?: return@let, + prevHistory.readDuration, + ) + } + // SY <-- + } + // KMK --> + // If chapters which only present on new manga then mark read up to latest read chapter number + else /* KMK <-- */ if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) { + updatedChapter = updatedChapter.copy(read = true) + } + } + + updatedChapter + } + + val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() } + updateChapter.awaitAll(chapterUpdates) + // SY --> + upsertHistory.awaitAll(historyUpdates) + // SY <-- + } + + // Update categories + if (MigrationFlag.CATEGORY in flags) { + val categoryIds = getCategories.await(current.id).map { it.id } + setMangaCategories.await(target.id, categoryIds) + } + + // Update track + // SY --> + if (MigrationFlag.TRACK in flags) { + // SY <-- + getTracks.await(current.id).mapNotNull { track -> + val updatedTrack = track.copy(mangaId = target.id) + + val service = enhancedServices + .firstOrNull { it.isTrackFrom(updatedTrack, current, currentSource) } + + if (service != null) { + service.migrateTrack(updatedTrack, target, targetSource) + } else { + updatedTrack + } + } + .takeIf { it.isNotEmpty() } + ?.let { insertTrack.awaitAll(it) } + } + + // Delete downloaded + if (MigrationFlag.REMOVE_DOWNLOAD in flags && currentSource != null) { + downloadManager.deleteManga(current, currentSource) + } + + // Update custom cover (recheck if custom cover exists) + if (MigrationFlag.CUSTOM_COVER in flags && current.hasCustomCover()) { + coverCache.setCustomCoverToCache(target, coverCache.getCustomCoverFile(current.id).inputStream()) + } + + val currentMangaUpdate = MangaUpdate( + id = current.id, + favorite = false, + dateAdded = 0, + ) + .takeIf { replace } + val targetMangaUpdate = MangaUpdate( + id = target.id, + favorite = true, + chapterFlags = current.chapterFlags + // KMK --> + .takeIf { MigrationFlag.EXTRA in flags }, + // KMK <-- + viewerFlags = current.viewerFlags + // KMK --> + .takeIf { MigrationFlag.EXTRA in flags }, + // KMK <-- + dateAdded = if (replace) current.dateAdded else Instant.now().toEpochMilli(), + notes = if (MigrationFlag.NOTES in flags) current.notes else null, + ) + + updateManga.awaitAll(listOfNotNull(currentMangaUpdate, targetMangaUpdate)) + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + } + } +} diff --git a/app/src/main/java/mihon/feature/common/utils/MigrationFlag.kt b/app/src/main/java/mihon/feature/common/utils/MigrationFlag.kt new file mode 100644 index 000000000..da1427aed --- /dev/null +++ b/app/src/main/java/mihon/feature/common/utils/MigrationFlag.kt @@ -0,0 +1,22 @@ +package mihon.feature.common.utils + +import dev.icerock.moko.resources.StringResource +import mihon.domain.migration.models.MigrationFlag +import tachiyomi.i18n.MR +import tachiyomi.i18n.sy.SYMR + +fun MigrationFlag.getLabel(): StringResource { + return when (this) { + MigrationFlag.CHAPTER -> MR.strings.chapters + MigrationFlag.CATEGORY -> MR.strings.categories + // KMK --> + MigrationFlag.TRACK -> MR.strings.track + // KMK <-- + MigrationFlag.CUSTOM_COVER -> MR.strings.custom_cover + MigrationFlag.NOTES -> MR.strings.action_notes + MigrationFlag.REMOVE_DOWNLOAD -> MR.strings.delete_downloaded + // KMK --> + MigrationFlag.EXTRA -> SYMR.strings.log_extra + // KMK <-- + } +} diff --git a/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt new file mode 100644 index 000000000..d4756291b --- /dev/null +++ b/app/src/main/java/mihon/feature/migration/dialog/MigrateMangaDialog.kt @@ -0,0 +1,178 @@ +package mihon.feature.migration.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.util.fastForEach +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import eu.kanade.domain.manga.model.hasCustomCover +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.download.DownloadManager +import kotlinx.coroutines.flow.update +import mihon.domain.migration.models.MigrationFlag +import mihon.domain.migration.usecases.MigrateMangaUseCase +import mihon.feature.common.utils.getLabel +import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.lang.withUIContext +import tachiyomi.domain.manga.model.Manga +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.screens.LoadingScreen +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +@Composable +internal fun Screen.MigrateMangaDialog( + current: Manga, + target: Manga, + onClickTitle: () -> Unit, + onDismissRequest: () -> Unit, + onComplete: () -> Unit = onDismissRequest, +) { + val scope = rememberCoroutineScope() + + val screenModel = rememberScreenModel { MigrateDialogScreenModel(current, target) } + val state by screenModel.state.collectAsState() + + if (state.isMigrating) { + LoadingScreen( + modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)), + ) + return + } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.migration_dialog_what_to_include)) + }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + state.applicableFlags.fastForEach { flag -> + LabeledCheckbox( + label = stringResource(flag.getLabel()), + checked = flag in state.selectedFlags, + onCheckedChange = { screenModel.toggleSelection(flag) }, + ) + } + } + }, + confirmButton = { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), + ) { + TextButton( + onClick = { + // KMK --> + // Allow `migrate` mangas when using `Bulk-favorite` + // onDismissRequest() + // KMK <-- + onClickTitle() + }, + ) { + Text(text = stringResource(MR.strings.action_show_manga)) + } + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = { + scope.launchIO { + screenModel.migrateManga(replace = false) + withUIContext { onComplete() } + } + }, + ) { + Text(text = stringResource(MR.strings.copy)) + } + TextButton( + onClick = { + scope.launchIO { + screenModel.migrateManga(replace = true) + withUIContext { onComplete() } + } + }, + ) { + Text(text = stringResource(MR.strings.migrate)) + } + } + }, + ) +} + +private class MigrateDialogScreenModel( + private val current: Manga, + private val target: Manga, + sourcePreference: SourcePreferences = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val migrateManga: MigrateMangaUseCase = Injekt.get(), +) : StateScreenModel(State()) { + + init { + val applicableFlags = buildList { + MigrationFlag.entries.forEach { + val applicable = when (it) { + MigrationFlag.CHAPTER -> true + MigrationFlag.CATEGORY -> true + // KMK --> + MigrationFlag.TRACK -> true + // KMK <-- + MigrationFlag.CUSTOM_COVER -> current.hasCustomCover(coverCache) + MigrationFlag.NOTES -> current.notes.isNotBlank() + MigrationFlag.REMOVE_DOWNLOAD -> downloadManager.getDownloadCount(current) > 0 + // KMK --> + MigrationFlag.EXTRA -> true + // KMK <-- + } + if (applicable) add(it) + } + } + val selectedFlags = sourcePreference.migrationFlags().get() + mutableState.update { it.copy(applicableFlags = applicableFlags, selectedFlags = selectedFlags) } + } + + fun toggleSelection(flag: MigrationFlag) { + mutableState.update { + val selectedFlags = it.selectedFlags.toMutableSet() + .apply { if (contains(flag)) remove(flag) else add(flag) } + .toSet() + it.copy(selectedFlags = selectedFlags) + } + } + + suspend fun migrateManga(replace: Boolean) { + // KMK --> + // sourcePreference.migrationFlags().set(state.value.selectedFlags) + // KMK <-- + mutableState.update { it.copy(isMigrating = true) } + migrateManga(current, target, replace, /* KMK --> */ state.value.selectedFlags /* KMK <-- */) + mutableState.update { it.copy(isMigrating = false) } + } + + data class State( + val applicableFlags: List = emptyList(), + val selectedFlags: Set = emptySet(), + val isMigrating: Boolean = false, + ) +} diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt index 577d83687..026245d82 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreference.kt @@ -171,13 +171,13 @@ sealed class AndroidPreference( } } - class Object( + class ObjectAsString( preferences: SharedPreferences, keyFlow: Flow, key: String, defaultValue: T, - val serializer: (T) -> String, - val deserializer: (String) -> T, + private val serializer: (T) -> String, + private val deserializer: (String) -> T, ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { return try { @@ -191,4 +191,25 @@ sealed class AndroidPreference( putString(key, serializer(value)) } } + + class ObjectAsInt( + preferences: SharedPreferences, + keyFlow: Flow, + key: String, + defaultValue: T, + private val serializer: (T) -> Int, + private val deserializer: (Int) -> T, + ) : AndroidPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { + return try { + if (preferences.contains(key)) preferences.getInt(key, 0).let(deserializer) else defaultValue + } catch (e: Exception) { + defaultValue + } + } + + override fun write(key: String, value: T): Editor.() -> Unit = { + putInt(key, serializer(value)) + } + } } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt index 6bdb120cd..78f98f204 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/AndroidPreferenceStore.kt @@ -9,7 +9,8 @@ import tachiyomi.core.common.preference.AndroidPreference.BooleanPrimitive import tachiyomi.core.common.preference.AndroidPreference.FloatPrimitive import tachiyomi.core.common.preference.AndroidPreference.IntPrimitive import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive -import tachiyomi.core.common.preference.AndroidPreference.Object +import tachiyomi.core.common.preference.AndroidPreference.ObjectAsInt +import tachiyomi.core.common.preference.AndroidPreference.ObjectAsString import tachiyomi.core.common.preference.AndroidPreference.StringPrimitive import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive @@ -44,13 +45,29 @@ class AndroidPreferenceStore( return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue) } - override fun getObject( + override fun getObjectFromString( key: String, defaultValue: T, serializer: (T) -> String, deserializer: (String) -> T, ): Preference { - return Object( + return ObjectAsString( + preferences = sharedPreferences, + keyFlow = keyFlow, + key = key, + defaultValue = defaultValue, + serializer = serializer, + deserializer = deserializer, + ) + } + + override fun getObjectFromInt( + key: String, + defaultValue: T, + serializer: (T) -> Int, + deserializer: (Int) -> T, + ): Preference { + return ObjectAsInt( preferences = sharedPreferences, keyFlow = keyFlow, key = key, diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt index 96e8644ad..1dae7fff9 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/InMemoryPreferenceStore.kt @@ -52,7 +52,7 @@ class InMemoryPreferenceStore( } @Suppress("UNCHECKED_CAST") - override fun getObject( + override fun getObjectFromString( key: String, defaultValue: T, serializer: (T) -> String, @@ -63,6 +63,18 @@ class InMemoryPreferenceStore( return if (data == null) default else InMemoryPreference(key, data, defaultValue) } + @Suppress("UNCHECKED_CAST") + override fun getObjectFromInt( + key: String, + defaultValue: T, + serializer: (T) -> Int, + deserializer: (Int) -> T, + ): Preference { + val default = InMemoryPreference(key, null, defaultValue) + val data: T? = preferences[key]?.get() as? T + return if (data == null) default else InMemoryPreference(key, data, defaultValue) + } + override fun getAll(): Map { return preferences } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt index 0b1f11d27..c1fc885e0 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt @@ -14,13 +14,20 @@ interface PreferenceStore { fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference> - fun getObject( + fun getObjectFromString( key: String, defaultValue: T, serializer: (T) -> String, deserializer: (String) -> T, ): Preference + fun getObjectFromInt( + key: String, + defaultValue: T, + serializer: (T) -> Int, + deserializer: (Int) -> T, + ): Preference + fun getAll(): Map } @@ -28,7 +35,7 @@ fun PreferenceStore.getLongArray( key: String, defaultValue: List, ): Preference> { - return getObject( + return getObjectFromString( key = key, defaultValue = defaultValue, serializer = { it.joinToString(",") }, @@ -40,7 +47,7 @@ inline fun > PreferenceStore.getEnum( key: String, defaultValue: T, ): Preference { - return getObject( + return getObjectFromString( key = key, defaultValue = defaultValue, serializer = { it.name }, diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index 5bbec2bab..8873986e8 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -14,14 +14,14 @@ class LibraryPreferences( private val preferenceStore: PreferenceStore, ) { - fun displayMode() = preferenceStore.getObject( + fun displayMode() = preferenceStore.getObjectFromString( "pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize, ) - fun sortingMode() = preferenceStore.getObject( + fun sortingMode() = preferenceStore.getObjectFromString( "library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize,