diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index ca094d403..a8fb38b69 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -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, + history: List, contentPadding: PaddingValues, onClickCover: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt index d10398b45..6bfd62654 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt @@ -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 { + val flags = mutableListOf() + flags += MigrationFlag.create(CHAPTERS, defaultSelectedBitMap, MR.strings.chapters) + flags += MigrationFlag.create(CATEGORIES, defaultSelectedBitMap, MR.strings.categories) + // KMK --> + flags += MigrationFlag.create(TRACK, defaultSelectedBitMap, MR.strings.track) + // KMK <-- + + if (manga != null) { + if (manga.hasCustomCover(coverCache)) { + flags += MigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, MR.strings.custom_cover) + } + if (downloadCache.getDownloadCount(manga) > 0) { + flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, MR.strings.delete_downloaded) + } + } + + // KMK --> + flags += MigrationFlag.create(EXTRA, defaultSelectedBitMap, SYMR.strings.log_extra) + // KMK <-- + + return flags + } + + /** Returns a bit map of selected flags. */ + fun getSelectedFlagsBitMap( + selectedFlags: List, + flags: List, + ): Int { + return selectedFlags + .zip(flags) + .filter { (isSelected, _) -> isSelected } + .map { (_, flag) -> flag.flag } + .reduceOrNull { acc, mask -> acc or mask } ?: 0 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreen.kt index 120711a89..4571bb2fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreen.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt index 0cd7e0820..6d5b77b3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt @@ -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().trackers.filterIsInstance() - } - 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, - 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() - val historyUpdates = mutableListOf() - - 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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt new file mode 100644 index 000000000..4fa6d677a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt @@ -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(State()) { + + val migrateFlags: Preference 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, + 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().trackers.filterIsInstance() + } + + 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() + val historyUpdates = mutableListOf() + + mangaChapters.forEach { updatedChapter -> + // SY <-- + if (updatedChapter.isRecognizedNumber) { + val prevChapter = prevMangaChapters + .find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber } + + if (prevChapter != null) { + // SY --> + // If chapters match then mark new manga's chapters read/unread as old one + chapterUpdates += ChapterUpdate( + id = updatedChapter.id, + // Also migrate read status + read = prevChapter.read, + // SY <-- + dateFetch = prevChapter.dateFetch, + bookmark = prevChapter.bookmark, + ) + // SY --> + // KMK --> + prevHistoryList[prevChapter.id]?.let { prevHistory -> + // KMK <-- + historyUpdates += HistoryUpdate( + updatedChapter.id, + prevHistory.readAt ?: return@let, + prevHistory.readDuration, + ) + } + // SY <-- + } else if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) { + // SY --> + // If chapters which only present on new manga then mark read up to latest read chapter number + chapterUpdates += ChapterUpdate( + id = updatedChapter.id, + // SY <-- + read = true, + ) + } + } + } + + updateChapter.awaitAll(chapterUpdates) + // SY --> + upsertHistory.awaitAll(historyUpdates) + // SY <-- + } + + // Update categories + if (migrateCategories) { + val categoryIds = getCategories.await(oldManga.id).map { it.id } + setMangaCategories.await(newManga.id, categoryIds) + } + + // Update track + // SY --> + if (migrateTracks) { + // SY <-- + getTracks.await(oldManga.id).mapNotNull { track -> + val updatedTrack = track.copy(mangaId = newManga.id) + + // Kavita, Komga, Suwayomi + val service = enhancedServices + .firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) } + + if (service != null) { + service.migrateTrack(updatedTrack, newManga, newSource) + } else { + updatedTrack + } + } + .takeIf { it.isNotEmpty() } + ?.let { insertTrack.awaitAll(it) } + } + + // Delete downloaded + if (deleteDownloaded) { + if (oldSource != null) { + downloadManager.deleteManga(oldManga, oldSource) + } + } + + if (replace) { + updateManga.awaitUpdateFavorite(oldManga.id, favorite = false) + } + + // Update custom cover (recheck if custom cover exists) + if (migrateCustomCover && oldManga.hasCustomCover()) { + coverCache.setCustomCoverToCache(newManga, coverCache.getCustomCoverFile(oldManga.id).inputStream()) + } + + updateManga.await( + MangaUpdate( + id = newManga.id, + favorite = true, + chapterFlags = oldManga.chapterFlags + // KMK --> + .takeIf { migrateExtra }, + // KMK <-- + viewerFlags = oldManga.viewerFlags + // KMK --> + .takeIf { migrateExtra }, + // KMK <-- + dateAdded = if (replace) oldManga.dateAdded else Instant.now().toEpochMilli(), + notes = oldManga.notes + // KMK --> + .takeIf { migrateExtra }, + // KMK <-- + ), + ) + } + } + + @Immutable + data class State( + val isMigrating: Boolean = false, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index d6b4ddb44..e77bccff0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -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().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() }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt index cdc72585f..a84771d65 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt @@ -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? = null, + val list: List? = null, val dialog: Dialog? = null, ) @@ -245,6 +251,7 @@ class HistoryScreenModel( val manga: Manga, val initialSelection: ImmutableList>, ) : Dialog + data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog } sealed interface Event { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index 0d309d667..3ad45df57 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -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().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 -> {} } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index ee31c386f..6f75d3cc1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -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().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().skipPreMigration().get(), - navigator, - manga.id, - toMangaId, - ) - // SY <-- - } - private fun openMetadataViewer( navigator: Navigator, manga: Manga, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index 88b551206..6b4a7bddc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -1738,10 +1738,7 @@ class MangaScreenModel( ) : Dialog data class DeleteChapters(val chapters: List) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List) : 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) { screenModelScope.launchIO { diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt index b6b119f3e..3b62f4411 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -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().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,