parent
c54d77333f
commit
2df0236669
2 changed files with 306 additions and 270 deletions
|
@ -0,0 +1,304 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
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.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.util.fastForEachIndexed
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import eu.kanade.domain.category.interactor.GetCategories
|
||||||
|
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||||
|
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||||
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
|
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||||
|
import eu.kanade.domain.chapter.model.toChapterUpdate
|
||||||
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.domain.manga.model.MangaUpdate
|
||||||
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
|
import eu.kanade.domain.track.interactor.GetTracks
|
||||||
|
import eu.kanade.domain.track.interactor.InsertTrack
|
||||||
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.core.preference.Preference
|
||||||
|
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MigrateDialog(
|
||||||
|
oldManga: Manga,
|
||||||
|
newManga: Manga,
|
||||||
|
screenModel: MigrateDialogScreenModel,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onClickTitle: () -> Unit,
|
||||||
|
onPopScreen: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
val activeFlags = remember { MigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
|
||||||
|
val items = remember {
|
||||||
|
MigrationFlags.titles(oldManga)
|
||||||
|
.map { context.getString(it) }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
val selected = remember {
|
||||||
|
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isMigrating) {
|
||||||
|
LoadingScreen(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.migration_dialog_what_to_include))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
items.forEachIndexed { index, title ->
|
||||||
|
val onChange: () -> Unit = {
|
||||||
|
selected[index] = !selected[index]
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onChange),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
|
||||||
|
Text(text = title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Row {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onClickTitle()
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.action_show_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launchIO {
|
||||||
|
screenModel.migrateManga(oldManga, newManga, false)
|
||||||
|
withUIContext { onPopScreen() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.copy))
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launchIO {
|
||||||
|
val selectedIndices = mutableListOf<Int>()
|
||||||
|
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
||||||
|
val newValue =
|
||||||
|
MigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
||||||
|
screenModel.migrateFlags.set(newValue)
|
||||||
|
screenModel.migrateManga(oldManga, newManga, true)
|
||||||
|
withUIContext { onPopScreen() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.migrate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MigrateDialogScreenModel(
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
|
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
||||||
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||||
|
private val updateChapter: UpdateChapter = 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(),
|
||||||
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
|
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) {
|
||||||
|
|
||||||
|
val migrateFlags: Preference<Int> by lazy {
|
||||||
|
preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val enhancedServices by lazy {
|
||||||
|
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun migrateManga(oldManga: Manga, newManga: Manga, replace: Boolean) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
||||||
|
// anyway
|
||||||
|
mutableState.update { it.copy(isMigrating = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun migrateMangaInternal(
|
||||||
|
oldSource: Source?,
|
||||||
|
newSource: Source,
|
||||||
|
oldManga: Manga,
|
||||||
|
newManga: Manga,
|
||||||
|
sourceChapters: List<SChapter>,
|
||||||
|
replace: Boolean,
|
||||||
|
) {
|
||||||
|
val flags = migrateFlags.get()
|
||||||
|
|
||||||
|
val migrateChapters = MigrationFlags.hasChapters(flags)
|
||||||
|
val migrateCategories = MigrationFlags.hasCategories(flags)
|
||||||
|
val migrateTracks = MigrationFlags.hasTracks(flags)
|
||||||
|
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
|
||||||
|
|
||||||
|
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 = getChapterByMangaId.await(oldManga.id)
|
||||||
|
val mangaChapters = getChapterByMangaId.await(newManga.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 (migrateCategories) {
|
||||||
|
val categoryIds = getCategories.await(oldManga.id).map { it.id }
|
||||||
|
setMangaCategories.await(newManga.id, categoryIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update track
|
||||||
|
if (migrateTracks) {
|
||||||
|
val tracks = getTracks.await(oldManga.id).mapNotNull { track ->
|
||||||
|
val updatedTrack = track.copy(mangaId = newManga.id)
|
||||||
|
|
||||||
|
val service = enhancedServices
|
||||||
|
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
||||||
|
|
||||||
|
if (service != null) {
|
||||||
|
service.migrateTrack(updatedTrack, newManga, newSource)
|
||||||
|
} else {
|
||||||
|
updatedTrack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insertTrack.awaitAll(tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replace) {
|
||||||
|
updateManga.await(MangaUpdate(oldManga.id, favorite = false, dateAdded = 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update custom cover (recheck if custom cover exists)
|
||||||
|
if (migrateCustomCover && oldManga.hasCustomCover()) {
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
coverCache.setCustomCoverToCache(newManga, coverCache.getCustomCoverFile(oldManga.id).inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
updateManga.await(
|
||||||
|
MangaUpdate(
|
||||||
|
id = newManga.id,
|
||||||
|
favorite = true,
|
||||||
|
chapterFlags = oldManga.chapterFlags,
|
||||||
|
viewerFlags = oldManga.viewerFlags,
|
||||||
|
dateAdded = if (replace) oldManga.dateAdded else Date().time,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val isMigrating: Boolean = false,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,67 +1,21 @@
|
||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.domain.category.interactor.GetCategories
|
|
||||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
|
||||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
|
||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
|
||||||
import eu.kanade.domain.chapter.model.toChapterUpdate
|
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
|
||||||
import eu.kanade.domain.manga.model.MangaUpdate
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
|
||||||
import eu.kanade.domain.track.interactor.GetTracks
|
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
|
||||||
import eu.kanade.presentation.browse.MigrateSearchScreen
|
import eu.kanade.presentation.browse.MigrateSearchScreen
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.core.preference.Preference
|
|
||||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class MigrateSearchScreen(private val mangaId: Long) : Screen {
|
class MigrateSearchScreen(private val mangaId: Long) : Screen {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
|
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
@ -84,7 +38,6 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen {
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val dialog = state.dialog) {
|
when (val dialog = state.dialog) {
|
||||||
null -> {}
|
|
||||||
is MigrateSearchDialog.Migrate -> {
|
is MigrateSearchDialog.Migrate -> {
|
||||||
MigrateDialog(
|
MigrateDialog(
|
||||||
oldManga = state.manga!!,
|
oldManga = state.manga!!,
|
||||||
|
@ -105,228 +58,7 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MigrateDialog(
|
|
||||||
oldManga: Manga,
|
|
||||||
newManga: Manga,
|
|
||||||
screenModel: MigrateDialogScreenModel,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onClickTitle: () -> Unit,
|
|
||||||
onPopScreen: () -> Unit,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val activeFlags = remember { MigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
|
|
||||||
val items = remember {
|
|
||||||
MigrationFlags.titles(oldManga)
|
|
||||||
.map { context.getString(it) }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
val selected = remember {
|
|
||||||
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
|
|
||||||
}
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(R.string.migration_dialog_what_to_include))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
items.forEachIndexed { index, title ->
|
|
||||||
val onChange: () -> Unit = {
|
|
||||||
selected[index] = !selected[index]
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onChange),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
|
|
||||||
Text(text = title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
Row {
|
|
||||||
TextButton(onClick = {
|
|
||||||
onClickTitle()
|
|
||||||
onDismissRequest()
|
|
||||||
},) {
|
|
||||||
Text(text = stringResource(R.string.action_show_manga))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
TextButton(onClick = {
|
|
||||||
scope.launchIO {
|
|
||||||
screenModel.migrateManga(oldManga, newManga, false)
|
|
||||||
launchUI {
|
|
||||||
onPopScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},) {
|
|
||||||
Text(text = stringResource(R.string.copy))
|
|
||||||
}
|
|
||||||
TextButton(onClick = {
|
|
||||||
scope.launchIO {
|
|
||||||
val selectedIndices = mutableListOf<Int>()
|
|
||||||
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
|
||||||
val newValue = MigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
|
||||||
screenModel.migrateFlags.set(newValue)
|
|
||||||
screenModel.migrateManga(oldManga, newManga, true)
|
|
||||||
launchUI {
|
|
||||||
onPopScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},) {
|
|
||||||
Text(text = stringResource(R.string.migrate))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MigrateDialogScreenModel(
|
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
|
||||||
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
|
||||||
private val updateChapter: UpdateChapter = 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(),
|
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
|
||||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
|
||||||
) : ScreenModel {
|
|
||||||
|
|
||||||
val migrateFlags: Preference<Int> by lazy {
|
|
||||||
preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val enhancedServices by lazy { Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>() }
|
|
||||||
|
|
||||||
suspend fun migrateManga(oldManga: Manga, newManga: Manga, replace: Boolean) {
|
|
||||||
val source = sourceManager.get(newManga.source) ?: return
|
|
||||||
val prevSource = sourceManager.get(oldManga.source)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val chapters = source.getChapterList(newManga.toSManga())
|
|
||||||
|
|
||||||
migrateMangaInternal(
|
|
||||||
oldSource = prevSource,
|
|
||||||
newSource = source,
|
|
||||||
oldManga = oldManga,
|
|
||||||
newManga = newManga,
|
|
||||||
sourceChapters = chapters,
|
|
||||||
replace = replace,
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun migrateMangaInternal(
|
|
||||||
oldSource: Source?,
|
|
||||||
newSource: Source,
|
|
||||||
oldManga: Manga,
|
|
||||||
newManga: Manga,
|
|
||||||
sourceChapters: List<SChapter>,
|
|
||||||
replace: Boolean,
|
|
||||||
) {
|
|
||||||
val flags = migrateFlags.get()
|
|
||||||
|
|
||||||
val migrateChapters = MigrationFlags.hasChapters(flags)
|
|
||||||
val migrateCategories = MigrationFlags.hasCategories(flags)
|
|
||||||
val migrateTracks = MigrationFlags.hasTracks(flags)
|
|
||||||
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
|
|
||||||
|
|
||||||
try {
|
|
||||||
syncChaptersWithSource.await(sourceChapters, newManga, newSource)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Worst case, chapters won't be synced
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update chapters read, bookmark and dateFetch
|
|
||||||
if (migrateChapters) {
|
|
||||||
val prevMangaChapters = getChapterByMangaId.await(oldManga.id)
|
|
||||||
val mangaChapters = getChapterByMangaId.await(newManga.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 (migrateCategories) {
|
|
||||||
val categoryIds = getCategories.await(oldManga.id).map { it.id }
|
|
||||||
setMangaCategories.await(newManga.id, categoryIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update track
|
|
||||||
if (migrateTracks) {
|
|
||||||
val tracks = getTracks.await(oldManga.id).mapNotNull { track ->
|
|
||||||
val updatedTrack = track.copy(mangaId = newManga.id)
|
|
||||||
|
|
||||||
val service = enhancedServices
|
|
||||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
|
||||||
|
|
||||||
if (service != null) {
|
|
||||||
service.migrateTrack(updatedTrack, newManga, newSource)
|
|
||||||
} else {
|
|
||||||
updatedTrack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertTrack.awaitAll(tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
updateManga.await(MangaUpdate(oldManga.id, favorite = false, dateAdded = 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update custom cover (recheck if custom cover exists)
|
|
||||||
if (migrateCustomCover && oldManga.hasCustomCover()) {
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
coverCache.setCustomCoverToCache(newManga, coverCache.getCustomCoverFile(oldManga.id).inputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
updateManga.await(
|
|
||||||
MangaUpdate(
|
|
||||||
id = newManga.id,
|
|
||||||
favorite = true,
|
|
||||||
chapterFlags = oldManga.chapterFlags,
|
|
||||||
viewerFlags = oldManga.viewerFlags,
|
|
||||||
dateAdded = if (replace) oldManga.dateAdded else Date().time,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Reference in a new issue