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.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
|
|
@ -138,6 +139,11 @@ class DomainModule : InjektModule {
|
|||
addFactory { SetMangaCategories(get()) }
|
||||
addFactory { GetExcludedScanlators(get()) }
|
||||
addFactory { SetExcludedScanlators(get()) }
|
||||
addFactory {
|
||||
MigrateMangaUseCase(
|
||||
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(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.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import mihon.domain.migration.models.MigrationFlag
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
|
|
@ -13,7 +14,7 @@ class SourcePreferences(
|
|||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun sourceDisplayMode() = preferenceStore.getObject(
|
||||
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
||||
"pref_display_mode_catalogue",
|
||||
LibraryDisplayMode.default,
|
||||
LibraryDisplayMode.Serializer::serialize,
|
||||
|
|
@ -70,6 +71,13 @@ class SourcePreferences(
|
|||
false,
|
||||
)
|
||||
|
||||
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
||||
key = "migrate_flags",
|
||||
defaultValue = MigrationFlag.entries.toSet(),
|
||||
serializer = { MigrationFlag.toBit(it) },
|
||||
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||
)
|
||||
|
||||
// KMK -->
|
||||
fun globalSearchPinnedState() = preferenceStore.getEnum(
|
||||
Preference.appStateKey("global_search_pinned_toggle_state"),
|
||||
|
|
@ -112,8 +120,6 @@ class SourcePreferences(
|
|||
WSRV_NL,
|
||||
}
|
||||
|
||||
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
|
||||
|
||||
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
|
||||
|
||||
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
|
|
@ -18,10 +17,9 @@ import eu.kanade.presentation.components.AppBar
|
|||
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel.Dialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
|
@ -89,7 +87,6 @@ fun Screen.BulkFavoriteDialogs(
|
|||
dialog = dialog,
|
||||
navigator = navigator,
|
||||
state = bulkFavoriteState,
|
||||
migrateScreenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
||||
onDismiss = bulkFavoriteScreenModel::dismissDialog,
|
||||
stopRunning = bulkFavoriteScreenModel::stopRunning,
|
||||
toggleSelection = bulkFavoriteScreenModel::toggleSelection,
|
||||
|
|
@ -101,11 +98,10 @@ fun Screen.BulkFavoriteDialogs(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMigrateDialog(
|
||||
private fun Screen.ShowMigrateDialog(
|
||||
dialog: Dialog.Migrate,
|
||||
navigator: Navigator?,
|
||||
state: BulkFavoriteScreenModel.State,
|
||||
migrateScreenModel: MigrateDialogScreenModel,
|
||||
onDismiss: () -> Unit,
|
||||
stopRunning: () -> Unit,
|
||||
toggleSelection: (Manga, toSelectedState: Boolean) -> Unit,
|
||||
|
|
@ -113,14 +109,14 @@ private fun ShowMigrateDialog(
|
|||
) {
|
||||
stopRunning()
|
||||
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = migrateScreenModel,
|
||||
MigrateMangaDialog(
|
||||
current = dialog.current,
|
||||
target = dialog.target,
|
||||
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||
onClickTitle = { navigator?.push(MangaScreen(dialog.current.id)) },
|
||||
onDismissRequest = onDismiss,
|
||||
onClickTitle = { navigator?.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = {
|
||||
toggleSelection(dialog.newManga, false)
|
||||
onComplete = {
|
||||
toggleSelection(dialog.target, false)
|
||||
onDismiss()
|
||||
// `selection.size` is at current value before calling above `toggleSelection`
|
||||
if (state.selection.size > 1) {
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ class BulkFavoriteScreenModel(
|
|||
}
|
||||
|
||||
internal fun showMigrateDialog(manga: Manga, duplicate: Manga) {
|
||||
setDialog(Dialog.Migrate(newManga = manga, oldManga = duplicate))
|
||||
setDialog(Dialog.Migrate(target = manga, current = duplicate))
|
||||
}
|
||||
|
||||
private fun setDialog(dialog: Dialog?) {
|
||||
|
|
@ -406,7 +406,7 @@ class BulkFavoriteScreenModel(
|
|||
}
|
||||
|
||||
sealed interface Dialog {
|
||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||
data class AddDuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
|
||||
data class BulkAllowDuplicate(val manga: Manga, val duplicates: List<MangaWithChapterCount>, val currentIdx: Int) : 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.theme.colorscheme.AndroidViewColorScheme
|
||||
import eu.kanade.tachiyomi.databinding.MigrationBottomSheetBinding
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import mihon.domain.migration.models.MigrationFlag
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.util.lang.toLong
|
||||
|
|
@ -116,15 +116,15 @@ class MigrationBottomSheetDialogState(
|
|||
* Init general reader preferences.
|
||||
*/
|
||||
fun initPreferences(binding: MigrationBottomSheetBinding) {
|
||||
val flags = preferences.migrateFlags().get()
|
||||
val flags = preferences.migrationFlags().get()
|
||||
|
||||
with(binding) {
|
||||
migChapters.isChecked = MigrationFlags.hasChapters(flags)
|
||||
migCategories.isChecked = MigrationFlags.hasCategories(flags)
|
||||
migTracking.isChecked = MigrationFlags.hasTracks(flags)
|
||||
migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
|
||||
migExtra.isChecked = MigrationFlags.hasExtra(flags)
|
||||
migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteDownloaded(flags)
|
||||
migChapters.isChecked = MigrationFlag.CHAPTER in flags
|
||||
migCategories.isChecked = MigrationFlag.CATEGORY in flags
|
||||
migTracking.isChecked = MigrationFlag.TRACK in flags
|
||||
migCustomCover.isChecked = MigrationFlag.CUSTOM_COVER in flags
|
||||
migExtra.isChecked = MigrationFlag.EXTRA in flags
|
||||
migDeleteDownloaded.isChecked = MigrationFlag.REMOVE_DOWNLOAD in flags
|
||||
|
||||
listOf(
|
||||
migChapters,
|
||||
|
|
@ -183,17 +183,16 @@ class MigrationBottomSheetDialogState(
|
|||
}
|
||||
|
||||
private fun setFlags(binding: MigrationBottomSheetBinding) {
|
||||
var flags = 0
|
||||
val flags = mutableSetOf<MigrationFlag>()
|
||||
with(binding) {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
if (migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
||||
if (migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||
if (migTracking.isChecked) flags = flags or MigrationFlags.TRACK
|
||||
if (migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
|
||||
if (migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
|
||||
if (migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_DOWNLOADED
|
||||
if (migChapters.isChecked) flags.add(MigrationFlag.CHAPTER)
|
||||
if (migCategories.isChecked) flags.add(MigrationFlag.CATEGORY)
|
||||
if (migTracking.isChecked) flags.add(MigrationFlag.TRACK)
|
||||
if (migCustomCover.isChecked) flags.add(MigrationFlag.CUSTOM_COVER)
|
||||
if (migExtra.isChecked) flags.add(MigrationFlag.EXTRA)
|
||||
if (migDeleteDownloaded.isChecked) flags.add(MigrationFlag.REMOVE_DOWNLOAD)
|
||||
}
|
||||
preferences.migrateFlags().set(flags)
|
||||
preferences.migrationFlags().set(flags)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import tachiyomi.core.common.util.lang.withUIContext
|
|||
import tachiyomi.i18n.sy.SYMR
|
||||
|
||||
/**
|
||||
* Screen showing a list of pair of source-dest manga entries being migrated.
|
||||
* Screen showing a list of pair of current-target manga entries being migrated.
|
||||
*/
|
||||
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() {
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel.Companion.migrateMangaInternal
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.smartsearch.SmartSourceSearchEngine
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
|
|
@ -32,6 +31,7 @@ import kotlinx.coroutines.isActive
|
|||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
|
@ -57,6 +57,7 @@ class MigrationListScreenModel(
|
|||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
||||
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
|
||||
) : ScreenModel {
|
||||
|
||||
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
|
||||
|
|
@ -383,33 +384,18 @@ class MigrationListScreenModel(
|
|||
migratingProgress.value = 0f
|
||||
val items = migratingItems.value.orEmpty()
|
||||
try {
|
||||
items.forEachIndexed { index, oldManga ->
|
||||
items.forEachIndexed { index, current ->
|
||||
try {
|
||||
ensureActive()
|
||||
val newManga = oldManga.searchResult.value.let {
|
||||
val target = current.searchResult.value.let {
|
||||
if (it is SearchResult.Result) {
|
||||
getManga.await(it.id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (newManga != null) {
|
||||
val source = sourceManager.get(newManga.source) ?: return@launchIO
|
||||
val prevSource = sourceManager.get(oldManga.manga.source)
|
||||
|
||||
try {
|
||||
val chapters = source.getChapterList(newManga.toSManga())
|
||||
|
||||
migrateMangaInternal(
|
||||
oldSource = prevSource,
|
||||
newSource = source,
|
||||
oldManga = oldManga.manga,
|
||||
newManga = newManga,
|
||||
sourceChapters = chapters,
|
||||
replace = replace,
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
if (target != null) {
|
||||
migrateManga(current.manga, target, replace)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
|
@ -438,28 +424,13 @@ class MigrationListScreenModel(
|
|||
fun migrateManga(mangaId: Long, replace: Boolean) {
|
||||
manualMigrations.value++
|
||||
screenModelScope.launchIO {
|
||||
val oldManga = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
||||
val current = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
||||
?: return@launchIO
|
||||
|
||||
val newManga = getManga.await((oldManga.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO)
|
||||
val target = getManga.await((current.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO)
|
||||
?: return@launchIO
|
||||
|
||||
val source = sourceManager.get(newManga.source) ?: return@launchIO
|
||||
val prevSource = sourceManager.get(oldManga.manga.source)
|
||||
|
||||
try {
|
||||
val chapters = source.getChapterList(newManga.toSManga())
|
||||
|
||||
migrateMangaInternal(
|
||||
oldSource = prevSource,
|
||||
newSource = source,
|
||||
oldManga = oldManga.manga,
|
||||
newManga = newManga,
|
||||
sourceChapters = chapters,
|
||||
replace = replace,
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
migrateManga(current.manga, target, replace)
|
||||
removeManga(mangaId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel =
|
||||
rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) }
|
||||
val dialogState by dialogScreenModel.state.collectAsState()
|
||||
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
// KMK -->
|
||||
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
|
||||
|
|
@ -37,18 +34,14 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
|
|||
|
||||
MigrateSearchScreen(
|
||||
state = state,
|
||||
fromSourceId = state.fromSourceId,
|
||||
fromSourceId = state.from?.source,
|
||||
navigateUp = navigator::pop,
|
||||
onChangeSearchQuery = screenModel::updateSearchQuery,
|
||||
onSearch = { screenModel.search() },
|
||||
getManga = { screenModel.getManga(it) },
|
||||
onChangeSearchFilter = screenModel::setSourceFilter,
|
||||
onToggleResults = screenModel::toggleFilterResults,
|
||||
onClickSource = {
|
||||
// SY -->
|
||||
navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery))
|
||||
// SY <--
|
||||
},
|
||||
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
|
||||
onClickItem = { manga ->
|
||||
// KMK -->
|
||||
if (bulkFavoriteState.selectionMode) {
|
||||
|
|
|
|||
|
|
@ -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)!!
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
fromSourceId = manga.source,
|
||||
from = manga,
|
||||
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
|
||||
*/
|
||||
data class SourceSearchScreen(
|
||||
private val oldManga: Manga,
|
||||
data class MigrateSourceSearchScreen(
|
||||
private val currentManga: Manga,
|
||||
private val sourceId: Long,
|
||||
private val query: String?,
|
||||
) : Screen() {
|
||||
|
|
@ -135,7 +135,7 @@ data class SourceSearchScreen(
|
|||
navigator.items
|
||||
.filterIsInstance<MigrationListScreen>()
|
||||
.last()
|
||||
.newSelectedItem = oldManga.id to it.id
|
||||
.newSelectedItem = currentManga.id to it.id
|
||||
navigator.popUntil { it is MigrationListScreen }
|
||||
// SY <--
|
||||
}
|
||||
|
|
@ -56,8 +56,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
|||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
|
|
@ -73,6 +71,7 @@ import exh.ui.smartsearch.SmartSearchScreen
|
|||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
|
|
@ -462,13 +461,12 @@ data class BrowseSourceScreen(
|
|||
}
|
||||
|
||||
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
||||
MigrateMangaDialog(
|
||||
current = dialog.current,
|
||||
target = dialog.target,
|
||||
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = { onDismissRequest() },
|
||||
)
|
||||
}
|
||||
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
|
||||
|
|
|
|||
|
|
@ -530,7 +530,7 @@ open class BrowseSourceScreenModel(
|
|||
val manga: Manga,
|
||||
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
||||
) : Dialog
|
||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||
|
||||
// SY -->
|
||||
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.withContext
|
|||
import mihon.domain.manga.model.toDomainManga
|
||||
import tachiyomi.core.common.preference.toggle
|
||||
import tachiyomi.core.common.util.QuerySanitizer.sanitize
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
|
@ -223,19 +224,35 @@ abstract class SearchScreenModel(
|
|||
updateItems(newItems)
|
||||
}
|
||||
|
||||
fun setMigrateDialog(currentId: Long, target: Manga) {
|
||||
screenModelScope.launchIO {
|
||||
val current = getManga.await(currentId) ?: return@launchIO
|
||||
mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun clearDialog() {
|
||||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val fromSourceId: Long? = null,
|
||||
val from: Manga? = null,
|
||||
val searchQuery: String? = null,
|
||||
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
|
||||
val onlyShowHasResults: Boolean = false,
|
||||
val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(),
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
|
||||
val total: Int = items.size
|
||||
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
sealed interface Dialog {
|
||||
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||
}
|
||||
}
|
||||
|
||||
enum class SourceFilter {
|
||||
|
|
|
|||
|
|
@ -216,9 +216,9 @@ class HistoryScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun showMigrateDialog(currentManga: Manga, duplicate: Manga) {
|
||||
fun showMigrateDialog(target: Manga, current: Manga) {
|
||||
mutableState.update { currentState ->
|
||||
currentState.copy(dialog = Dialog.Migrate(newManga = currentManga, oldManga = duplicate))
|
||||
currentState.copy(dialog = Dialog.Migrate(target = target, current = current))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ class HistoryScreenModel(
|
|||
val manga: Manga,
|
||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||
) : Dialog
|
||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
||||
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ import eu.kanade.presentation.util.Tab
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.connections.discord.DiscordRPCService
|
||||
import eu.kanade.tachiyomi.data.connections.discord.DiscordScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
|
|
@ -38,6 +36,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
|||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
|
|
@ -47,6 +46,7 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
|
||||
data object HistoryTab : Tab {
|
||||
@Suppress("unused")
|
||||
private fun readResolve(): Any = HistoryTab
|
||||
|
||||
private val snackbarHostState = SnackbarHostState()
|
||||
|
|
@ -120,9 +120,7 @@ data object HistoryTab : Tab {
|
|||
DuplicateMangaDialog(
|
||||
duplicates = dialog.duplicates,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = {
|
||||
screenModel.addFavorite(dialog.manga)
|
||||
},
|
||||
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
||||
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
||||
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) },
|
||||
// KMK -->
|
||||
|
|
@ -141,13 +139,12 @@ data object HistoryTab : Tab {
|
|||
)
|
||||
}
|
||||
is HistoryScreenModel.Dialog.Migrate -> {
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
||||
MigrateMangaDialog(
|
||||
current = dialog.current,
|
||||
target = dialog.target,
|
||||
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = onDismissRequest,
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen
|
||||
|
|
@ -104,6 +102,7 @@ import kotlinx.coroutines.flow.take
|
|||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import mihon.feature.migration.config.MigrationConfigScreen
|
||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchUI
|
||||
|
|
@ -513,13 +512,12 @@ class MangaScreen(
|
|||
}
|
||||
|
||||
is MangaScreenModel.Dialog.Migrate -> {
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
||||
MigrateMangaDialog(
|
||||
current = dialog.current,
|
||||
target = dialog.target,
|
||||
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = onDismissRequest,
|
||||
)
|
||||
}
|
||||
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
||||
|
|
|
|||
|
|
@ -1898,7 +1898,7 @@ class MangaScreenModel(
|
|||
) : Dialog
|
||||
data class DeleteChapters(val chapters: List<Chapter>) : 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
|
||||
|
||||
// SY -->
|
||||
|
|
@ -1937,7 +1937,7 @@ class MangaScreenModel(
|
|||
|
||||
fun showMigrateDialog(duplicate: Manga) {
|
||||
val manga = successState?.manga ?: return
|
||||
updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) }
|
||||
updateSuccessState { it.copy(dialog = Dialog.Migrate(target = manga, current = duplicate)) }
|
||||
}
|
||||
|
||||
fun setExcludedScanlators(excludedScanlators: Set<String>) {
|
||||
|
|
|
|||
|
|
@ -24,11 +24,10 @@ import eu.kanade.presentation.components.BulkSelectionToolbar
|
|||
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
|
|
@ -167,13 +166,12 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
|
|||
}
|
||||
// KMK -->
|
||||
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
||||
MigrateDialog(
|
||||
oldManga = dialog.oldManga,
|
||||
newManga = dialog.newManga,
|
||||
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
||||
MigrateMangaDialog(
|
||||
current = dialog.current,
|
||||
target = dialog.target,
|
||||
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
||||
onPopScreen = { onDismissRequest() },
|
||||
)
|
||||
}
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
val serializer: (T) -> String,
|
||||
val deserializer: (String) -> T,
|
||||
private val serializer: (T) -> String,
|
||||
private val deserializer: (String) -> T,
|
||||
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
|
||||
return try {
|
||||
|
|
@ -191,4 +191,25 @@ sealed class AndroidPreference<T>(
|
|||
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.IntPrimitive
|
||||
import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive
|
||||
import tachiyomi.core.common.preference.AndroidPreference.Object
|
||||
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsInt
|
||||
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsString
|
||||
import tachiyomi.core.common.preference.AndroidPreference.StringPrimitive
|
||||
import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive
|
||||
|
||||
|
|
@ -44,13 +45,29 @@ class AndroidPreferenceStore(
|
|||
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
|
||||
}
|
||||
|
||||
override fun <T> getObject(
|
||||
override fun <T> getObjectFromString(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> 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,
|
||||
keyFlow = keyFlow,
|
||||
key = key,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class InMemoryPreferenceStore(
|
|||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> getObject(
|
||||
override fun <T> getObjectFromString(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
|
|
@ -63,6 +63,18 @@ class InMemoryPreferenceStore(
|
|||
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <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, *> {
|
||||
return preferences
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,20 @@ interface PreferenceStore {
|
|||
|
||||
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
||||
|
||||
fun <T> getObject(
|
||||
fun <T> getObjectFromString(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T,
|
||||
): Preference<T>
|
||||
|
||||
fun <T> getObjectFromInt(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> Int,
|
||||
deserializer: (Int) -> T,
|
||||
): Preference<T>
|
||||
|
||||
fun getAll(): Map<String, *>
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +35,7 @@ fun PreferenceStore.getLongArray(
|
|||
key: String,
|
||||
defaultValue: List<Long>,
|
||||
): Preference<List<Long>> {
|
||||
return getObject(
|
||||
return getObjectFromString(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
serializer = { it.joinToString(",") },
|
||||
|
|
@ -40,7 +47,7 @@ inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
|||
key: String,
|
||||
defaultValue: T,
|
||||
): Preference<T> {
|
||||
return getObject(
|
||||
return getObjectFromString(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
serializer = { it.name },
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ class LibraryPreferences(
|
|||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun displayMode() = preferenceStore.getObject(
|
||||
fun displayMode() = preferenceStore.getObjectFromString(
|
||||
"pref_display_mode_library",
|
||||
LibraryDisplayMode.default,
|
||||
LibraryDisplayMode.Serializer::serialize,
|
||||
LibraryDisplayMode.Serializer::deserialize,
|
||||
)
|
||||
|
||||
fun sortingMode() = preferenceStore.getObject(
|
||||
fun sortingMode() = preferenceStore.getObjectFromString(
|
||||
"library_sorting_mode",
|
||||
LibrarySort.default,
|
||||
LibrarySort.Serializer::serialize,
|
||||
|
|
|
|||
Loading…
Reference in a new issue