Bring back original Mihon's migration dialog (#823)

This commit is contained in:
Cuong-Tran 2025-04-08 00:01:47 +07:00 committed by GitHub
parent d89180fb10
commit 271680bd03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 551 additions and 257 deletions

View file

@ -24,7 +24,6 @@ import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
@ -126,7 +125,7 @@ fun HistoryScreen(
@Composable
private fun HistoryScreenContent(
history: ImmutableList<HistoryUiModel>,
history: List<HistoryUiModel>,
contentPadding: PaddingValues,
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,

View file

@ -1,13 +1,48 @@
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
@ -17,19 +52,61 @@ object MigrationFlags {
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 hasExtra(value: Int): Boolean {
return value and EXTRA != 0
}
fun hasDeleteDownloaded(value: Int): Boolean {
return value and DELETE_DOWNLOADED != 0
}
// KMK -->
/** Returns true if the manga has extra info, include notes. */
fun hasExtra(value: Int): Boolean {
return value and EXTRA != 0
}
// KMK <--
/** Returns information about applicable flags with default selections. */
fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MigrationFlag> {
val flags = mutableListOf<MigrationFlag>()
flags += MigrationFlag.create(CHAPTERS, defaultSelectedBitMap, MR.strings.chapters)
flags += MigrationFlag.create(CATEGORIES, defaultSelectedBitMap, MR.strings.categories)
// KMK -->
flags += MigrationFlag.create(TRACK, defaultSelectedBitMap, MR.strings.track)
// KMK <--
if (manga != null) {
if (manga.hasCustomCover(coverCache)) {
flags += MigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, MR.strings.custom_cover)
}
if (downloadCache.getDownloadCount(manga) > 0) {
flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, MR.strings.delete_downloaded)
}
}
// KMK -->
flags += MigrationFlag.create(EXTRA, defaultSelectedBitMap, SYMR.strings.log_extra)
// KMK <--
return flags
}
/** Returns a bit map of selected flags. */
fun getSelectedFlagsBitMap(
selectedFlags: List<Boolean>,
flags: List<MigrationFlag>,
): Int {
return selectedFlags
.zip(flags)
.filter { (isSelected, _) -> isSelected }
.map { (_, flag) -> flag.flag }
.reduceOrNull { acc, mask -> acc or mask } ?: 0
}
}

View file

@ -391,6 +391,7 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
)
}
/* All usages have been replaced by original Mihon's migration dialog */
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, fromMangaId: Long, toManga: Long?) {
navigator.push(
if (skipPre) {

View file

@ -6,20 +6,13 @@ import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
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.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.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
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.eh.EHentaiThrottleManager
import exh.smartsearch.SmartSearchEngine
@ -43,54 +36,30 @@ import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
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.GetHistoryByMangaId
import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMergedReferencesById
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
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.sy.SYMR
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.util.concurrent.atomic.AtomicInteger
class MigrationListScreenModel(
private val config: MigrationProcedureConfig,
private val preferences: UnsortedPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
private val getHistoryByMangaId: GetHistoryByMangaId = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
) : ScreenModel {
private val enhancedServices by lazy {
Injekt.get<TrackerManager>().trackers.filterIsInstance<EnhancedTracker>()
}
private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams)
private val throttleManager = EHentaiThrottleManager()
@ -359,153 +328,6 @@ class MigrationListScreenModel(
private fun mangasSkipped() = migratingItems.value.orEmpty().count { it.searchResult.value == SearchResult.NotFound }
private suspend fun migrateMangaInternal(
oldSource: Source?,
newSource: Source,
oldManga: Manga,
newManga: Manga,
sourceChapters: List<SChapter>,
replace: Boolean,
) {
if (oldManga.id == newManga.id) return // Nothing to migrate
val flags = preferences.migrateFlags().get()
val migrateChapters = MigrationFlags.hasChapters(flags)
val migrateCategories = MigrationFlags.hasCategories(flags)
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags)
val migrateTracks = MigrationFlags.hasTracks(flags)
val migrateExtra = MigrationFlags.hasExtra(flags)
// 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 }
val prevHistoryList = getHistoryByMangaId.await(oldManga.id)
// KMK -->
.associateBy { it.chapterId }
// KMK <--
val chapterUpdates = mutableListOf<ChapterUpdate>()
val historyUpdates = mutableListOf<HistoryUpdate>()
mangaChapters.forEach { updateChapter ->
if (updateChapter.isRecognizedNumber) {
val prevChapter = prevMangaChapters
.find { it.isRecognizedNumber && it.chapterNumber == updateChapter.chapterNumber }
if (prevChapter != null) {
// SY -->
// If chapters match then mark new manga's chapters read/unread as old one
chapterUpdates += ChapterUpdate(
id = updateChapter.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(
updateChapter.id,
prevHistory.readAt ?: return@let,
prevHistory.readDuration,
)
}
// SY <--
} else if (maxChapterRead != null && updateChapter.chapterNumber <= maxChapterRead) {
// SY -->
// If chapters which only present on new manga then mark read up to latest read chapter number
chapterUpdates += ChapterUpdate(
id = updateChapter.id,
// SY <--
read = true,
)
}
}
}
updateChapter.awaitAll(chapterUpdates)
upsertHistory.awaitAll(historyUpdates)
}
// Update categories
if (migrateCategories) {
val categoryIds = getCategories.await(oldManga.id).map { it.id }
setMangaCategories.await(newManga.id, categoryIds)
}
// Update track
if (migrateTracks) {
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 <--
),
)
}
/** Set a manga picked from manual search to be used as migration target */
fun useMangaForMigration(context: Context, newMangaId: Long, selectedMangaId: Long) {
val migratingManga = migratingItems.value.orEmpty().find { it.manga.id == selectedMangaId }
@ -578,14 +400,14 @@ class MigrationListScreenModel(
val prevSource = sourceManager.get(oldManga.manga.source)
try {
val newSourceChapters = source.getChapterList(newManga.toSManga())
val chapters = source.getChapterList(newManga.toSManga())
migrateMangaInternal(
oldSource = prevSource,
newSource = source,
oldManga = oldManga.manga,
newManga = newManga,
sourceChapters = newSourceChapters,
sourceChapters = chapters,
replace = replace,
)
} catch (_: Throwable) {
@ -628,14 +450,14 @@ class MigrationListScreenModel(
val prevSource = sourceManager.get(oldManga.manga.source)
try {
val newSourceChapters = source.getChapterList(newManga.toSManga())
val chapters = source.getChapterList(newManga.toSManga())
migrateMangaInternal(
oldSource = prevSource,
newSource = source,
oldManga = oldManga.manga,
newManga = newManga,
sourceChapters = newSourceChapters,
sourceChapters = chapters,
replace = replace,
)
} catch (_: Throwable) {

View file

@ -0,0 +1,380 @@
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.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 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.UnsortedPreferences
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.GetHistoryByMangaId
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 = {
onDismissRequest()
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 -->
preferences: UnsortedPreferences = Injekt.get(),
// KMK <--
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) {
val migrateFlags: Preference<Int> by lazy {
// KMK -->
preferences.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 (_: Throwable) {
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
// anyway
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 preferences: UnsortedPreferences = 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 getHistoryByMangaId: GetHistoryByMangaId = 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 ?: preferences.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 = getHistoryByMangaId.await(oldManga.id)
// KMK -->
.associateBy { it.chapterId }
// KMK <--
val chapterUpdates = mutableListOf<ChapterUpdate>()
val historyUpdates = mutableListOf<HistoryUpdate>()
mangaChapters.forEach { updatedChapter ->
// SY <--
if (updatedChapter.isRecognizedNumber) {
val prevChapter = prevMangaChapters
.find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber }
if (prevChapter != null) {
// SY -->
// If chapters match then mark new manga's chapters read/unread as old one
chapterUpdates += ChapterUpdate(
id = updatedChapter.id,
// Also migrate read status
read = prevChapter.read,
// SY <--
dateFetch = prevChapter.dateFetch,
bookmark = prevChapter.bookmark,
)
// SY -->
// KMK -->
prevHistoryList[prevChapter.id]?.let { prevHistory ->
// KMK <--
historyUpdates += HistoryUpdate(
updatedChapter.id,
prevHistory.readAt ?: return@let,
prevHistory.readDuration,
)
}
// SY <--
} else if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
// SY -->
// If chapters which only present on new manga then mark read up to latest read chapter number
chapterUpdates += ChapterUpdate(
id = updatedChapter.id,
// SY <--
read = true,
)
}
}
}
updateChapter.awaitAll(chapterUpdates)
// SY -->
upsertHistory.awaitAll(historyUpdates)
// SY <--
}
// Update categories
if (migrateCategories) {
val categoryIds = getCategories.await(oldManga.id).map { it.id }
setMangaCategories.await(newManga.id, categoryIds)
}
// Update track
// SY -->
if (migrateTracks) {
// SY <--
getTracks.await(oldManga.id).mapNotNull { track ->
val updatedTrack = track.copy(mangaId = newManga.id)
// Kavita, Komga, Suwayomi
val service = enhancedServices
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
if (service != null) {
service.migrateTrack(updatedTrack, newManga, newSource)
} else {
updatedTrack
}
}
.takeIf { it.isNotEmpty() }
?.let { insertTrack.awaitAll(it) }
}
// Delete downloaded
if (deleteDownloaded) {
if (oldSource != null) {
downloadManager.deleteManga(oldManga, oldSource)
}
}
if (replace) {
updateManga.awaitUpdateFavorite(oldManga.id, favorite = false)
}
// Update custom cover (recheck if custom cover exists)
if (migrateCustomCover && oldManga.hasCustomCover()) {
coverCache.setCustomCoverToCache(newManga, coverCache.getCustomCoverFile(oldManga.id).inputStream())
}
updateManga.await(
MangaUpdate(
id = newManga.id,
favorite = true,
chapterFlags = oldManga.chapterFlags
// KMK -->
.takeIf { migrateExtra },
// KMK <--
viewerFlags = oldManga.viewerFlags
// KMK -->
.takeIf { migrateExtra },
// KMK <--
dateAdded = if (replace) oldManga.dateAdded else Instant.now().toEpochMilli(),
notes = oldManga.notes
// KMK -->
.takeIf { migrateExtra },
// KMK <--
),
)
}
}
@Immutable
data class State(
val isMigrating: Boolean = false,
)
}

View file

@ -55,7 +55,8 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteDialogs
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
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
@ -434,7 +435,10 @@ data class BrowseSourceScreen(
},
openMangaDexFollows = if (screenModel.source.isMdBasedSource()) {
{
navigator.replace(MangaDexFollowsScreen(sourceId))
// KMK -->
// navigator.replace(MangaDexFollowsScreen(sourceId))
navigator.push(MangaDexFollowsScreen(sourceId))
// KMK <--
}
} else {
null
@ -448,15 +452,19 @@ data class BrowseSourceScreen(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
it.id,
dialog.manga.id,
)
// SY <--
onMigrate = { screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, it)) },
)
}
is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateDialog(
oldManga = dialog.oldManga,
newManga = dialog.newManga,
screenModel = MigrateDialogScreenModel(),
onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = {
onDismissRequest()
},
)
}

View file

@ -73,7 +73,7 @@ class HistoryScreenModel(
logcat(LogPriority.ERROR, error)
_events.send(Event.InternalError)
}
.map { it.toHistoryUiModels().toImmutableList() }
.map { it.toHistoryUiModels() }
.flowOn(Dispatchers.IO)
}
.collect { newList -> mutableState.update { it.copy(list = newList) } }
@ -215,6 +215,12 @@ class HistoryScreenModel(
}
}
fun showMigrateDialog(currentManga: Manga, duplicate: Manga) {
mutableState.update { currentState ->
currentState.copy(dialog = Dialog.Migrate(newManga = currentManga, oldManga = duplicate))
}
}
private fun showChangeCategoryDialog(manga: Manga) {
screenModelScope.launch {
val categories = getCategories()
@ -233,7 +239,7 @@ class HistoryScreenModel(
@Immutable
data class State(
val searchQuery: String? = null,
val list: ImmutableList<HistoryUiModel>? = null,
val list: List<HistoryUiModel>? = null,
val dialog: Dialog? = null,
)
@ -245,6 +251,7 @@ class HistoryScreenModel(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
}
sealed interface Event {

View file

@ -27,7 +27,8 @@ import eu.kanade.presentation.history.components.HistoryDeleteDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.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
@ -36,7 +37,6 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@ -121,16 +121,7 @@ data object HistoryTab : Tab {
screenModel.addFavorite(dialog.manga)
},
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = {
// KMK -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
it.id,
dialog.manga.id,
)
// KMK <--
},
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) },
)
}
is HistoryScreenModel.Dialog.ChangeCategory -> {
@ -143,6 +134,16 @@ data object HistoryTab : Tab {
},
)
}
is HistoryScreenModel.Dialog.Migrate -> {
MigrateDialog(
oldManga = dialog.oldManga,
newManga = dialog.newManga,
screenModel = MigrateDialogScreenModel(),
onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) },
)
}
null -> {}
}

View file

@ -68,6 +68,8 @@ 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.advanced.design.PreMigrationScreen
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
@ -327,9 +329,17 @@ class MangaScreen(
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf {
successState.manga.favorite
},
previewsRowCount = successState.previewsRowCount,
onMigrateClicked = {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
listOfNotNull(successState.manga.id),
)
// SY <--
}.takeIf { successState.manga.favorite },
// SY -->
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
previewsRowCount = successState.previewsRowCount,
onMetadataViewerClicked = {
openMetadataViewer(
navigator,
@ -433,7 +443,6 @@ class MangaScreen(
},
)
}
is MangaScreenModel.Dialog.DeleteChapters -> {
DeleteChaptersDialog(
onDismissRequest = onDismissRequest,
@ -450,14 +459,20 @@ class MangaScreen(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = {
// SY -->
migrateManga(navigator, it, screenModel.manga!!.id)
// SY <--
},
onMigrate = { screenModel.showMigrateDialog(it) },
)
}
is MangaScreenModel.Dialog.Migrate -> {
MigrateDialog(
oldManga = dialog.oldManga,
newManga = dialog.newManga,
screenModel = MigrateDialogScreenModel(),
onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) },
)
}
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
onDismissRequest = onDismissRequest,
manga = successState.manga,
@ -471,7 +486,6 @@ class MangaScreen(
scanlatorFilterActive = successState.scanlatorFilterActive,
onScanlatorFilterClicked = { showScanlatorsDialog = true },
)
MangaScreenModel.Dialog.TrackSheet -> {
NavigatorAdaptiveSheet(
screen = TrackInfoDialogHomeScreen(
@ -483,7 +497,6 @@ class MangaScreen(
onDismissRequest = onDismissRequest,
)
}
MangaScreenModel.Dialog.FullCover -> {
val sm = rememberScreenModel { MangaCoverScreenModel(successState.manga.id) }
val manga by sm.state.collectAsState()
@ -540,7 +553,6 @@ class MangaScreen(
LoadingScreen(Modifier.systemBarsPadding())
}
}
is MangaScreenModel.Dialog.SetFetchInterval -> {
SetIntervalDialog(
interval = dialog.manga.fetchInterval,
@ -739,21 +751,6 @@ class MangaScreen(
context.copyToClipboard(url, url)
}
// SY -->
/**
* Initiates source migration for the specific manga.
*/
private fun migrateManga(navigator: Navigator, manga: Manga, toMangaId: Long? = null) {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
manga.id,
toMangaId,
)
// SY <--
}
private fun openMetadataViewer(
navigator: Navigator,
manga: Manga,

View file

@ -1738,10 +1738,7 @@ class MangaScreenModel(
) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicates: List<Manga>) : Dialog
/* SY -->
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
SY <-- */
data class SetFetchInterval(val manga: Manga) : Dialog
// SY -->
@ -1774,11 +1771,10 @@ class MangaScreenModel(
updateSuccessState { it.copy(dialog = Dialog.FullCover) }
}
/* SY -->
fun showMigrateDialog(duplicate: Manga) {
val manga = successState?.manga ?: return
updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) }
} SY <-- */
}
fun setExcludedScanlators(excludedScanlators: Set<String>) {
screenModelScope.launchIO {

View file

@ -24,19 +24,17 @@ import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteDialogs
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
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.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
@ -158,23 +156,31 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = {
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
it.id,
dialog.manga.id,
)
// KMK -->
onMigrate = { screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(dialog.manga, it)) },
// KMK <--
)
}
// KMK -->
is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateDialog(
oldManga = dialog.oldManga,
newManga = dialog.newManga,
screenModel = MigrateDialogScreenModel(),
onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = {
onDismissRequest()
},
)
}
// KMK <--
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,