Cleanup migrate manga dialog and related code (#2156)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package mihon.domain.migration.models
|
||||
|
||||
enum class MigrationFlag(val flag: Int) {
|
||||
CHAPTER(0b00001),
|
||||
CATEGORY(0b00010),
|
||||
|
||||
// 0b00100 was used for manga trackers
|
||||
CUSTOM_COVER(0b01000),
|
||||
NOTES(0b100000),
|
||||
REMOVE_DOWNLOAD(0b10000),
|
||||
;
|
||||
|
||||
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,145 @@
|
||||
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.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,
|
||||
) {
|
||||
private val enhancedServices by lazy { trackerManager.trackers.filterIsInstance<EnhancedTracker>() }
|
||||
|
||||
suspend operator fun invoke(current: Manga, target: Manga, replace: Boolean) {
|
||||
val targetSource = sourceManager.get(target.source) ?: return
|
||||
val currentSource = sourceManager.get(current.source)
|
||||
val flags = 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 }
|
||||
|
||||
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(
|
||||
dateFetch = prevChapter.dateFetch,
|
||||
bookmark = prevChapter.bookmark,
|
||||
)
|
||||
}
|
||||
|
||||
if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
|
||||
updatedChapter = updatedChapter.copy(read = true)
|
||||
}
|
||||
}
|
||||
|
||||
updatedChapter
|
||||
}
|
||||
|
||||
val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
|
||||
// Update categories
|
||||
if (MigrationFlag.CHAPTER in flags) {
|
||||
val categoryIds = getCategories.await(current.id).map { it.id }
|
||||
setMangaCategories.await(target.id, categoryIds)
|
||||
}
|
||||
|
||||
// Update track
|
||||
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,
|
||||
viewerFlags = current.viewerFlags,
|
||||
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,15 @@
|
||||
package mihon.feature.common.utils
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import mihon.domain.migration.models.MigrationFlag
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
fun MigrationFlag.getLabel(): StringResource {
|
||||
return when (this) {
|
||||
MigrationFlag.CHAPTER -> MR.strings.chapters
|
||||
MigrationFlag.CATEGORY -> MR.strings.categories
|
||||
MigrationFlag.CUSTOM_COVER -> MR.strings.custom_cover
|
||||
MigrationFlag.NOTES -> MR.strings.action_notes
|
||||
MigrationFlag.REMOVE_DOWNLOAD -> MR.strings.delete_downloaded
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
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 = {
|
||||
onDismissRequest()
|
||||
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,
|
||||
private val 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
|
||||
MigrationFlag.CUSTOM_COVER -> current.hasCustomCover(coverCache)
|
||||
MigrationFlag.NOTES -> current.notes.isNotBlank()
|
||||
MigrationFlag.REMOVE_DOWNLOAD -> downloadManager.getDownloadCount(current) > 0
|
||||
}
|
||||
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) {
|
||||
sourcePreference.migrationFlags().set(state.value.selectedFlags)
|
||||
mutableState.update { it.copy(isMigrating = true) }
|
||||
migrateManga(current, target, replace)
|
||||
mutableState.update { it.copy(isMigrating = false) }
|
||||
}
|
||||
|
||||
data class State(
|
||||
val applicableFlags: List<MigrationFlag> = emptyList(),
|
||||
val selectedFlags: Set<MigrationFlag> = emptySet(),
|
||||
val isMigrating: Boolean = false,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user