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:
parent
2196c84a0c
commit
5ab24620ec
30 changed files with 611 additions and 669 deletions
|
|
@ -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()) }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 <--
|
||||||
}
|
}
|
||||||
|
|
@ -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 -> {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 -> {}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>) {
|
||||||
|
|
|
||||||
|
|
@ -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 <--
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue