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>
This commit is contained in:
AntsyLich 2025-05-31 21:03:39 +06:00 committed by Cuong-Tran
parent 2196c84a0c
commit 5ab24620ec
30 changed files with 611 additions and 669 deletions

View file

@ -35,6 +35,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import mihon.domain.extensionrepo.service.ExtensionRepoService import mihon.domain.extensionrepo.service.ExtensionRepoService
import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.upcoming.interactor.GetUpcomingManga import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl
@ -138,6 +139,11 @@ class DomainModule : InjektModule {
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) } addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) } addFactory { SetExcludedScanlators(get()) }
addFactory {
MigrateMangaUseCase(
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
)
}
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) } addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
addFactory { GetApplicationRelease(get(), get()) } addFactory { GetApplicationRelease(get(), get()) }

View file

@ -3,6 +3,7 @@ package eu.kanade.domain.source.service
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import mihon.domain.migration.models.MigrationFlag
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum import tachiyomi.core.common.preference.getEnum
@ -13,7 +14,7 @@ class SourcePreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun sourceDisplayMode() = preferenceStore.getObject( fun sourceDisplayMode() = preferenceStore.getObjectFromString(
"pref_display_mode_catalogue", "pref_display_mode_catalogue",
LibraryDisplayMode.default, LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::serialize,
@ -70,6 +71,13 @@ class SourcePreferences(
false, false,
) )
fun migrationFlags() = preferenceStore.getObjectFromInt(
key = "migrate_flags",
defaultValue = MigrationFlag.entries.toSet(),
serializer = { MigrationFlag.toBit(it) },
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
)
// KMK --> // KMK -->
fun globalSearchPinnedState() = preferenceStore.getEnum( fun globalSearchPinnedState() = preferenceStore.getEnum(
Preference.appStateKey("global_search_pinned_toggle_state"), Preference.appStateKey("global_search_pinned_toggle_state"),
@ -112,8 +120,6 @@ class SourcePreferences(
WSRV_NL, WSRV_NL,
} }
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false) fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false) fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)

View file

@ -9,7 +9,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator 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.presentation.manga.DuplicateMangaDialog
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel.Dialog 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.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import mihon.feature.migration.dialog.MigrateMangaDialog
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -89,7 +87,6 @@ fun Screen.BulkFavoriteDialogs(
dialog = dialog, dialog = dialog,
navigator = navigator, navigator = navigator,
state = bulkFavoriteState, state = bulkFavoriteState,
migrateScreenModel = rememberScreenModel { MigrateDialogScreenModel() },
onDismiss = bulkFavoriteScreenModel::dismissDialog, onDismiss = bulkFavoriteScreenModel::dismissDialog,
stopRunning = bulkFavoriteScreenModel::stopRunning, stopRunning = bulkFavoriteScreenModel::stopRunning,
toggleSelection = bulkFavoriteScreenModel::toggleSelection, toggleSelection = bulkFavoriteScreenModel::toggleSelection,
@ -101,11 +98,10 @@ fun Screen.BulkFavoriteDialogs(
} }
@Composable @Composable
private fun ShowMigrateDialog( private fun Screen.ShowMigrateDialog(
dialog: Dialog.Migrate, dialog: Dialog.Migrate,
navigator: Navigator?, navigator: Navigator?,
state: BulkFavoriteScreenModel.State, state: BulkFavoriteScreenModel.State,
migrateScreenModel: MigrateDialogScreenModel,
onDismiss: () -> Unit, onDismiss: () -> Unit,
stopRunning: () -> Unit, stopRunning: () -> Unit,
toggleSelection: (Manga, toSelectedState: Boolean) -> Unit, toggleSelection: (Manga, toSelectedState: Boolean) -> Unit,
@ -113,14 +109,14 @@ private fun ShowMigrateDialog(
) { ) {
stopRunning() stopRunning()
MigrateDialog( MigrateMangaDialog(
oldManga = dialog.oldManga, current = dialog.current,
newManga = dialog.newManga, target = dialog.target,
screenModel = migrateScreenModel, // Initiated from the context of [dialog.target] so we show [dialog.current].
onClickTitle = { navigator?.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
onClickTitle = { navigator?.push(MangaScreen(dialog.oldManga.id)) }, onComplete = {
onPopScreen = { toggleSelection(dialog.target, false)
toggleSelection(dialog.newManga, false)
onDismiss() onDismiss()
// `selection.size` is at current value before calling above `toggleSelection` // `selection.size` is at current value before calling above `toggleSelection`
if (state.selection.size > 1) { if (state.selection.size > 1) {

View file

@ -378,7 +378,7 @@ class BulkFavoriteScreenModel(
} }
internal fun showMigrateDialog(manga: Manga, duplicate: Manga) { 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?) { private fun setDialog(dialog: Dialog?) {
@ -406,7 +406,7 @@ class BulkFavoriteScreenModel(
} }
sealed interface Dialog { 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<MangaWithChapterCount>) : Dialog data class AddDuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
data class BulkAllowDuplicate(val manga: Manga, val duplicates: List<MangaWithChapterCount>, val currentIdx: Int) : Dialog data class BulkAllowDuplicate(val manga: Manga, val duplicates: List<MangaWithChapterCount>, val currentIdx: Int) : Dialog
data class RemoveManga(val manga: Manga) : Dialog data class RemoveManga(val manga: Manga) : Dialog

View file

@ -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<MigrationFlag> {
val flags = mutableListOf<MigrationFlag>()
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<Boolean>,
flags: List<MigrationFlag>,
): Int {
return selectedFlags
.zip(flags)
.filter { (isSelected, _) -> isSelected }
.map { (_, flag) -> flag.flag }
.reduceOrNull { acc, mask -> acc or mask } ?: 0
}
}

View file

@ -21,8 +21,8 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme import eu.kanade.presentation.theme.colorscheme.AndroidViewColorScheme
import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import mihon.domain.migration.models.MigrationFlag
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.util.lang.toLong import tachiyomi.core.common.util.lang.toLong
@ -116,15 +116,15 @@ class MigrationBottomSheetDialogState(
* Init general reader preferences. * Init general reader preferences.
*/ */
fun initPreferences(binding: MigrationBottomSheetBinding) { fun initPreferences(binding: MigrationBottomSheetBinding) {
val flags = preferences.migrateFlags().get() val flags = preferences.migrationFlags().get()
with(binding) { with(binding) {
migChapters.isChecked = MigrationFlags.hasChapters(flags) migChapters.isChecked = MigrationFlag.CHAPTER in flags
migCategories.isChecked = MigrationFlags.hasCategories(flags) migCategories.isChecked = MigrationFlag.CATEGORY in flags
migTracking.isChecked = MigrationFlags.hasTracks(flags) migTracking.isChecked = MigrationFlag.TRACK in flags
migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags) migCustomCover.isChecked = MigrationFlag.CUSTOM_COVER in flags
migExtra.isChecked = MigrationFlags.hasExtra(flags) migExtra.isChecked = MigrationFlag.EXTRA in flags
migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteDownloaded(flags) migDeleteDownloaded.isChecked = MigrationFlag.REMOVE_DOWNLOAD in flags
listOf( listOf(
migChapters, migChapters,
@ -183,17 +183,16 @@ class MigrationBottomSheetDialogState(
} }
private fun setFlags(binding: MigrationBottomSheetBinding) { private fun setFlags(binding: MigrationBottomSheetBinding) {
var flags = 0 val flags = mutableSetOf<MigrationFlag>()
with(binding) { with(binding) {
@Suppress("KotlinConstantConditions") if (migChapters.isChecked) flags.add(MigrationFlag.CHAPTER)
if (migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS if (migCategories.isChecked) flags.add(MigrationFlag.CATEGORY)
if (migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES if (migTracking.isChecked) flags.add(MigrationFlag.TRACK)
if (migTracking.isChecked) flags = flags or MigrationFlags.TRACK if (migCustomCover.isChecked) flags.add(MigrationFlag.CUSTOM_COVER)
if (migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER if (migExtra.isChecked) flags.add(MigrationFlag.EXTRA)
if (migExtra.isChecked) flags = flags or MigrationFlags.EXTRA if (migDeleteDownloaded.isChecked) flags.add(MigrationFlag.REMOVE_DOWNLOAD)
if (migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_DOWNLOADED
} }
preferences.migrateFlags().set(flags) preferences.migrationFlags().set(flags)
} }
/** /**

View file

@ -27,7 +27,7 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.i18n.sy.SYMR 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() { class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() {

View file

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.all.EHentai 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.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel.Companion.migrateMangaInternal
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.smartsearch.SmartSourceSearchEngine import exh.smartsearch.SmartSourceSearchEngine
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
@ -32,6 +31,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority import logcat.LogPriority
import mihon.domain.migration.usecases.MigrateMangaUseCase
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
@ -57,6 +57,7 @@ class MigrationListScreenModel(
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
) : ScreenModel { ) : ScreenModel {
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams) private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
@ -383,33 +384,18 @@ class MigrationListScreenModel(
migratingProgress.value = 0f migratingProgress.value = 0f
val items = migratingItems.value.orEmpty() val items = migratingItems.value.orEmpty()
try { try {
items.forEachIndexed { index, oldManga -> items.forEachIndexed { index, current ->
try { try {
ensureActive() ensureActive()
val newManga = oldManga.searchResult.value.let { val target = current.searchResult.value.let {
if (it is SearchResult.Result) { if (it is SearchResult.Result) {
getManga.await(it.id) getManga.await(it.id)
} else { } else {
null null
} }
} }
if (newManga != null) { if (target != null) {
val source = sourceManager.get(newManga.source) ?: return@launchIO migrateManga(current.manga, target, replace)
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) {
}
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@ -438,28 +424,13 @@ class MigrationListScreenModel(
fun migrateManga(mangaId: Long, replace: Boolean) { fun migrateManga(mangaId: Long, replace: Boolean) {
manualMigrations.value++ manualMigrations.value++
screenModelScope.launchIO { screenModelScope.launchIO {
val oldManga = migratingItems.value.orEmpty().find { it.manga.id == mangaId } val current = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
?: return@launchIO ?: 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 ?: return@launchIO
val source = sourceManager.get(newManga.source) ?: return@launchIO migrateManga(current.manga, target, replace)
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) {
}
removeManga(mangaId) removeManga(mangaId)
} }
} }

View file

@ -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<MigrateDialogScreenModel.State>(State()) {
val migrateFlags: Preference<Int> 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<SChapter>,
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<TrackerManager>().trackers.filterIsInstance<EnhancedTracker>()
}
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<ChapterUpdate>()
val historyUpdates = mutableListOf<HistoryUpdate>()
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,
)
}

View file

@ -19,12 +19,9 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel =
rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
val state by screenModel.state.collectAsState()
val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) } val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
val dialogState by dialogScreenModel.state.collectAsState() val state by screenModel.state.collectAsState()
// KMK --> // KMK -->
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() } val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
@ -37,18 +34,14 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
MigrateSearchScreen( MigrateSearchScreen(
state = state, state = state,
fromSourceId = state.fromSourceId, fromSourceId = state.from?.source,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery, onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = { screenModel.search() }, onSearch = { screenModel.search() },
getManga = { screenModel.getManga(it) }, getManga = { screenModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter, onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults, onToggleResults = screenModel::toggleFilterResults,
onClickSource = { onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
// SY -->
navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery))
// SY <--
},
onClickItem = { manga -> onClickItem = { manga ->
// KMK --> // KMK -->
if (bulkFavoriteState.selectionMode) { if (bulkFavoriteState.selectionMode) {

View file

@ -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<MigrateSearchScreenDialogScreenModel.State>(State()) {
init {
screenModelScope.launch {
val manga = getManga.await(mangaId)!!
mutableState.update {
it.copy(manga = manga)
}
}
}
@Immutable
data class State(
val manga: Manga? = null,
)
}

View file

@ -33,7 +33,7 @@ class MigrateSearchScreenModel(
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId)!!
mutableState.update { mutableState.update {
it.copy( it.copy(
fromSourceId = manga.source, from = manga,
searchQuery = manga.title, searchQuery = manga.title,
) )
} }

View file

@ -43,8 +43,8 @@ import tachiyomi.source.local.LocalSource
/** /**
* Opened when click on a source in [MigrateSearchScreen] while doing manual search for migration * Opened when click on a source in [MigrateSearchScreen] while doing manual search for migration
*/ */
data class SourceSearchScreen( data class MigrateSourceSearchScreen(
private val oldManga: Manga, private val currentManga: Manga,
private val sourceId: Long, private val sourceId: Long,
private val query: String?, private val query: String?,
) : Screen() { ) : Screen() {
@ -135,7 +135,7 @@ data class SourceSearchScreen(
navigator.items navigator.items
.filterIsInstance<MigrationListScreen>() .filterIsInstance<MigrationListScreen>()
.last() .last()
.newSelectedItem = oldManga.id to it.id .newSelectedItem = currentManga.id to it.id
navigator.popUntil { it is MigrationListScreen } navigator.popUntil { it is MigrationListScreen }
// SY <-- // SY <--
} }

View file

@ -56,8 +56,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen 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.SourcesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
@ -73,6 +71,7 @@ import exh.ui.smartsearch.SmartSearchScreen
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import mihon.feature.migration.dialog.MigrateMangaDialog
import mihon.presentation.core.util.collectAsLazyPagingItems import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@ -462,13 +461,12 @@ data class BrowseSourceScreen(
} }
is BrowseSourceScreenModel.Dialog.Migrate -> { is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateDialog( MigrateMangaDialog(
oldManga = dialog.oldManga, current = dialog.current,
newManga = dialog.newManga, target = dialog.target,
screenModel = rememberScreenModel { MigrateDialogScreenModel() }, // Initiated from the context of [dialog.target] so we show [dialog.current].
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = { onDismissRequest() },
) )
} }
is BrowseSourceScreenModel.Dialog.RemoveManga -> { is BrowseSourceScreenModel.Dialog.RemoveManga -> {

View file

@ -530,7 +530,7 @@ open class BrowseSourceScreenModel(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>, val initialSelection: ImmutableList<CheckboxState.State<Category>>,
) : Dialog ) : Dialog
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog data class Migrate(val target: Manga, val current: Manga) : Dialog
// SY --> // SY -->
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog

View file

@ -27,6 +27,7 @@ import kotlinx.coroutines.withContext
import mihon.domain.manga.model.toDomainManga import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
import tachiyomi.core.common.util.QuerySanitizer.sanitize 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.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -223,19 +224,35 @@ abstract class SearchScreenModel(
updateItems(newItems) 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 @Immutable
data class State( data class State(
val fromSourceId: Long? = null, val from: Manga? = null,
val searchQuery: String? = null, val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false, val onlyShowHasResults: Boolean = false,
val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(), val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(),
val dialog: Dialog? = null,
) { ) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading } val progress: Int = items.count { it.value !is SearchItemResult.Loading }
val total: Int = items.size val total: Int = items.size
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) } val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
.toImmutableMap() .toImmutableMap()
} }
sealed interface Dialog {
data class Migrate(val target: Manga, val current: Manga) : Dialog
}
} }
enum class SourceFilter { enum class SourceFilter {

View file

@ -216,9 +216,9 @@ class HistoryScreenModel(
} }
} }
fun showMigrateDialog(currentManga: Manga, duplicate: Manga) { fun showMigrateDialog(target: Manga, current: Manga) {
mutableState.update { currentState -> 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 manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog ) : Dialog
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog data class Migrate(val target: Manga, val current: Manga) : Dialog
} }
sealed interface Event { sealed interface Event {

View file

@ -29,8 +29,6 @@ import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.connections.discord.DiscordRPCService import eu.kanade.tachiyomi.data.connections.discord.DiscordRPCService
import eu.kanade.tachiyomi.data.connections.discord.DiscordScreen 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.category.CategoryScreen
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
@ -38,6 +36,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import mihon.feature.migration.dialog.MigrateMangaDialog
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
@ -47,6 +46,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
data object HistoryTab : Tab { data object HistoryTab : Tab {
@Suppress("unused")
private fun readResolve(): Any = HistoryTab private fun readResolve(): Any = HistoryTab
private val snackbarHostState = SnackbarHostState() private val snackbarHostState = SnackbarHostState()
@ -120,9 +120,7 @@ data object HistoryTab : Tab {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates, duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { onConfirm = { screenModel.addFavorite(dialog.manga) },
screenModel.addFavorite(dialog.manga)
},
onOpenManga = { navigator.push(MangaScreen(it.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) }, onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) },
// KMK --> // KMK -->
@ -141,13 +139,12 @@ data object HistoryTab : Tab {
) )
} }
is HistoryScreenModel.Dialog.Migrate -> { is HistoryScreenModel.Dialog.Migrate -> {
MigrateDialog( MigrateMangaDialog(
oldManga = dialog.oldManga, current = dialog.current,
newManga = dialog.newManga, target = dialog.target,
screenModel = rememberScreenModel { MigrateDialogScreenModel() }, // Initiated from the context of [dialog.target] so we show [dialog.current].
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = onDismissRequest,
) )
} }
null -> {} null -> {}

View file

@ -69,8 +69,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen 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.SourcesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen
@ -104,6 +102,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.feature.migration.config.MigrationConfigScreen import mihon.feature.migration.config.MigrationConfigScreen
import mihon.feature.migration.dialog.MigrateMangaDialog
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.launchUI
@ -513,13 +512,12 @@ class MangaScreen(
} }
is MangaScreenModel.Dialog.Migrate -> { is MangaScreenModel.Dialog.Migrate -> {
MigrateDialog( MigrateMangaDialog(
oldManga = dialog.oldManga, current = dialog.current,
newManga = dialog.newManga, target = dialog.target,
screenModel = rememberScreenModel { MigrateDialogScreenModel() }, // Initiated from the context of [dialog.target] so we show [dialog.current].
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = onDismissRequest,
) )
} }
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog( MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(

View file

@ -1898,7 +1898,7 @@ class MangaScreenModel(
) : Dialog ) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : 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 data class SetFetchInterval(val manga: Manga) : Dialog
// SY --> // SY -->
@ -1937,7 +1937,7 @@ class MangaScreenModel(
fun showMigrateDialog(duplicate: Manga) { fun showMigrateDialog(duplicate: Manga) {
val manga = successState?.manga ?: return 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<String>) { fun setExcludedScanlators(excludedScanlators: Set<String>) {

View file

@ -24,11 +24,10 @@ import eu.kanade.presentation.components.BulkSelectionToolbar
import eu.kanade.presentation.manga.DuplicateMangaDialog import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel 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.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import mihon.feature.migration.dialog.MigrateMangaDialog
import mihon.presentation.core.util.collectAsLazyPagingItems import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
@ -167,13 +166,12 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
} }
// KMK --> // KMK -->
is BrowseSourceScreenModel.Dialog.Migrate -> { is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateDialog( MigrateMangaDialog(
oldManga = dialog.oldManga, current = dialog.current,
newManga = dialog.newManga, target = dialog.target,
screenModel = rememberScreenModel { MigrateDialogScreenModel() }, // Initiated from the context of [dialog.target] so we show [dialog.current].
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = { onDismissRequest() },
) )
} }
// KMK <-- // KMK <--

View file

@ -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<MigrationFlag> {
return buildSet {
entries.forEach { entry ->
if (bit and entry.flag != 0) add(entry)
}
}
}
fun toBit(flags: Set<MigrationFlag>): Int {
return flags.map { it.flag }
.reduceOrNull { acc, mask -> acc or mask }
?: 0
}
}
}

View file

@ -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<EnhancedTracker>() }
suspend operator fun invoke(
current: Manga,
target: Manga,
replace: Boolean,
// KMK -->
presetFlags: Set<MigrationFlag>? = 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<HistoryUpdate>()
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
}
}
}
}

View file

@ -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 <--
}
}

View file

@ -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<MigrateDialogScreenModel.State>(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<MigrationFlag> = emptyList(),
val selectedFlags: Set<MigrationFlag> = emptySet(),
val isMigrating: Boolean = false,
)
}

View file

@ -171,13 +171,13 @@ sealed class AndroidPreference<T>(
} }
} }
class Object<T>( class ObjectAsString<T>(
preferences: SharedPreferences, preferences: SharedPreferences,
keyFlow: Flow<String?>, keyFlow: Flow<String?>,
key: String, key: String,
defaultValue: T, defaultValue: T,
val serializer: (T) -> String, private val serializer: (T) -> String,
val deserializer: (String) -> T, private val deserializer: (String) -> T,
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) { ) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
return try { return try {
@ -191,4 +191,25 @@ sealed class AndroidPreference<T>(
putString(key, serializer(value)) putString(key, serializer(value))
} }
} }
class ObjectAsInt<T>(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: T,
private val serializer: (T) -> Int,
private val deserializer: (Int) -> T,
) : AndroidPreference<T>(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))
}
}
} }

View file

@ -9,7 +9,8 @@ import tachiyomi.core.common.preference.AndroidPreference.BooleanPrimitive
import tachiyomi.core.common.preference.AndroidPreference.FloatPrimitive import tachiyomi.core.common.preference.AndroidPreference.FloatPrimitive
import tachiyomi.core.common.preference.AndroidPreference.IntPrimitive import tachiyomi.core.common.preference.AndroidPreference.IntPrimitive
import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive 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.StringPrimitive
import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive
@ -44,13 +45,29 @@ class AndroidPreferenceStore(
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue) return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
} }
override fun <T> getObject( override fun <T> getObjectFromString(
key: String, key: String,
defaultValue: T, defaultValue: T,
serializer: (T) -> String, serializer: (T) -> String,
deserializer: (String) -> T, deserializer: (String) -> T,
): Preference<T> { ): Preference<T> {
return Object( return ObjectAsString(
preferences = sharedPreferences,
keyFlow = keyFlow,
key = key,
defaultValue = defaultValue,
serializer = serializer,
deserializer = deserializer,
)
}
override fun <T> getObjectFromInt(
key: String,
defaultValue: T,
serializer: (T) -> Int,
deserializer: (Int) -> T,
): Preference<T> {
return ObjectAsInt(
preferences = sharedPreferences, preferences = sharedPreferences,
keyFlow = keyFlow, keyFlow = keyFlow,
key = key, key = key,

View file

@ -52,7 +52,7 @@ class InMemoryPreferenceStore(
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T> getObject( override fun <T> getObjectFromString(
key: String, key: String,
defaultValue: T, defaultValue: T,
serializer: (T) -> String, serializer: (T) -> String,
@ -63,6 +63,18 @@ class InMemoryPreferenceStore(
return if (data == null) default else InMemoryPreference(key, data, defaultValue) return if (data == null) default else InMemoryPreference(key, data, defaultValue)
} }
@Suppress("UNCHECKED_CAST")
override fun <T> getObjectFromInt(
key: String,
defaultValue: T,
serializer: (T) -> Int,
deserializer: (Int) -> T,
): Preference<T> {
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<String, *> { override fun getAll(): Map<String, *> {
return preferences return preferences
} }

View file

@ -14,13 +14,20 @@ interface PreferenceStore {
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>> fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
fun <T> getObject( fun <T> getObjectFromString(
key: String, key: String,
defaultValue: T, defaultValue: T,
serializer: (T) -> String, serializer: (T) -> String,
deserializer: (String) -> T, deserializer: (String) -> T,
): Preference<T> ): Preference<T>
fun <T> getObjectFromInt(
key: String,
defaultValue: T,
serializer: (T) -> Int,
deserializer: (Int) -> T,
): Preference<T>
fun getAll(): Map<String, *> fun getAll(): Map<String, *>
} }
@ -28,7 +35,7 @@ fun PreferenceStore.getLongArray(
key: String, key: String,
defaultValue: List<Long>, defaultValue: List<Long>,
): Preference<List<Long>> { ): Preference<List<Long>> {
return getObject( return getObjectFromString(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,
serializer = { it.joinToString(",") }, serializer = { it.joinToString(",") },
@ -40,7 +47,7 @@ inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
key: String, key: String,
defaultValue: T, defaultValue: T,
): Preference<T> { ): Preference<T> {
return getObject( return getObjectFromString(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,
serializer = { it.name }, serializer = { it.name },

View file

@ -14,14 +14,14 @@ class LibraryPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun displayMode() = preferenceStore.getObject( fun displayMode() = preferenceStore.getObjectFromString(
"pref_display_mode_library", "pref_display_mode_library",
LibraryDisplayMode.default, LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize, LibraryDisplayMode.Serializer::deserialize,
) )
fun sortingMode() = preferenceStore.getObject( fun sortingMode() = preferenceStore.getObjectFromString(
"library_sorting_mode", "library_sorting_mode",
LibrarySort.default, LibrarySort.default,
LibrarySort.Serializer::serialize, LibrarySort.Serializer::serialize,