diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationProgressDialog.kt b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationProgressDialog.kt new file mode 100644 index 000000000..765412ab7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationProgressDialog.kt @@ -0,0 +1,41 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties +import eu.kanade.tachiyomi.R + +@Composable +fun MigrationProgressDialog( + progress: Float, + exitMigration: () -> Unit, +) { + AlertDialog( + onDismissRequest = {}, + confirmButton = { + TextButton(onClick = exitMigration) { + Text(text = stringResource(R.string.action_cancel)) + } + }, + text = { + if (!progress.isNaN()) { + val progressAnimated by animateFloatAsState( + targetValue = progress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + ) + LinearProgressIndicator(progressAnimated) + } + }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt index 300cdbe0c..f5b79f2ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt @@ -14,12 +14,15 @@ import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.MigrationListScreen import eu.kanade.presentation.browse.components.MigrationExitDialog import eu.kanade.presentation.browse.components.MigrationMangaDialog +import eu.kanade.presentation.browse.components.MigrationProgressDialog import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.system.toast +import exh.util.overEq +import exh.util.underEq import tachiyomi.core.util.lang.withUIContext class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() { @@ -34,6 +37,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen val migrationDone by screenModel.migrationDone.collectAsState() val unfinishedCount by screenModel.unfinishedCount.collectAsState() val dialog by screenModel.dialog.collectAsState() + val migrateProgress by screenModel.migratingProgress.collectAsState() val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current LaunchedEffect(items) { @@ -135,6 +139,13 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen null -> Unit } + if (!migrateProgress.isNaN() && migrateProgress overEq 0f && migrateProgress underEq 1f) { + MigrationProgressDialog( + progress = migrateProgress, + exitMigration = screenModel::cancelMigrate, + ) + } + BackHandler(true) { screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog } 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 89a4ba43f..79fd4d929 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 @@ -20,10 +20,12 @@ import exh.eh.EHentaiThrottleManager import exh.smartsearch.SmartSearchEngine import exh.source.MERGED_SOURCE_ID import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.isActive @@ -92,6 +94,10 @@ class MigrationListScreenModel( val dialog = MutableStateFlow(null) + val migratingProgress = MutableStateFlow(Float.MAX_VALUE) + + private var migrateJob: Job? = null + init { coroutineScope.launchIO { runMigrations( @@ -425,40 +431,56 @@ class MigrationListScreenModel( } fun migrateMangas() { - coroutineScope.launchIO { - migratingItems.value.orEmpty().forEach { manga -> - val searchResult = manga.searchResult.value - if (searchResult is SearchResult.Result) { - val toMangaObj = getManga.await(searchResult.id) ?: return@forEach - migrateMangaInternal( - manga.manga, - toMangaObj, - true, - ) - } - } - - navigateOut() - } + migrateMangas(true) } fun copyMangas() { - coroutineScope.launchIO { - migratingItems.value.orEmpty().forEach { manga -> - val searchResult = manga.searchResult.value - if (searchResult is SearchResult.Result) { - val toMangaObj = getManga.await(searchResult.id) ?: return@forEach - migrateMangaInternal( - manga.manga, - toMangaObj, - false, - ) + migrateMangas(false) + } + + private fun migrateMangas(replace: Boolean) { + dialog.value = null + migrateJob = coroutineScope.launchIO { + migratingProgress.value = 0f + val items = migratingItems.value.orEmpty() + try { + items.forEachIndexed { index, manga -> + try { + ensureActive() + val toMangaObj = manga.searchResult.value.let { + if (it is SearchResult.Result) { + getManga.await(it.id) + } else { + null + } + } + if (toMangaObj != null) { + migrateMangaInternal( + manga.manga, + toMangaObj, + replace, + ) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + logcat(LogPriority.WARN, throwable = e) + } + migratingProgress.value = index.toFloat() / items.size } + + navigateOut() + } finally { + migratingProgress.value = Float.MAX_VALUE + migrateJob = null } - navigateOut() } } + fun cancelMigrate() { + migrateJob?.cancel() + migrateJob = null + } + private suspend fun navigateOut() { navigateOut.emit(Unit) } diff --git a/app/src/main/java/exh/util/Boolean.kt b/app/src/main/java/exh/util/Boolean.kt index 7df1d6745..514581c78 100644 --- a/app/src/main/java/exh/util/Boolean.kt +++ b/app/src/main/java/exh/util/Boolean.kt @@ -1,9 +1,9 @@ package exh.util -infix fun Int.over(other: Int) = this > other +infix fun > T.over(other: T) = this > other -infix fun Int.overEq(other: Int) = this >= other +infix fun > T.overEq(other: T) = this >= other -infix fun Int.under(other: Int) = this < other +infix fun > T.under(other: T) = this < other -infix fun Int.underEq(other: Int) = this <= other +infix fun > T.underEq(other: T) = this <= other