From d59d960c6aa67a0d3f1bf2c167cc683b26af43a4 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Mon, 28 Nov 2022 19:41:04 -0500 Subject: [PATCH] Use Voyager for migration --- .../browse/MigrationListScreen.kt | 144 ++++++ .../browse/components/MigrationActionIcon.kt | 88 ++++ .../browse/components/MigrationExitDialog.kt | 31 ++ .../browse/components/MigrationItem.kt | 118 +++++ .../browse/components/MigrationItemResult.kt | 84 +++ .../browse/components/MigrationMangaDialog.kt | 51 ++ .../browse/migration/MigrationMangaDialog.kt | 44 -- .../design/MigrationBottomSheetDialog.kt | 6 +- .../advanced/design/MigrationSourceAdapter.kt | 33 +- .../advanced/design/PreMigrationController.kt | 321 +----------- .../advanced/design/PreMigrationScreen.kt | 217 ++++++++ .../design/PreMigrationScreenModel.kt | 143 ++++++ .../process/MigrationListController.kt | 485 +----------------- .../advanced/process/MigrationListScreen.kt | 177 +++++++ ...esenter.kt => MigrationListScreenModel.kt} | 140 ++--- .../process/MigrationProcedureConfig.kt | 6 +- .../migration/search/SearchController.kt | 16 +- .../search/SourceSearchController.kt | 16 +- 18 files changed, 1155 insertions(+), 965 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/MigrationActionIcon.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/MigrationExitDialog.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/MigrationItem.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/MigrationItemResult.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/MigrationMangaDialog.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationMangaDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreenModel.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/{MigrationListPresenter.kt => MigrationListScreenModel.kt} (86%) diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt new file mode 100644 index 000000000..fa6b08a6c --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrationListScreen.kt @@ -0,0 +1,144 @@ +package eu.kanade.presentation.browse + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowForward +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.CopyAll +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.DoneAll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.browse.components.MigrationActionIcon +import eu.kanade.presentation.browse.components.MigrationItem +import eu.kanade.presentation.browse.components.MigrationItemResult +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.components.ScrollbarLazyColumn +import eu.kanade.presentation.util.plus +import eu.kanade.presentation.util.topSmallPaddingValues +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga +import eu.kanade.tachiyomi.util.lang.withIOContext + +@Composable +fun MigrationListScreen( + items: List, + migrationDone: Boolean, + unfinishedCount: Int, + getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?, + getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo, + getSourceName: (Manga) -> String, + onMigrationItemClick: (Manga) -> Unit, + openMigrationDialog: (Boolean) -> Unit, + skipManga: (Long) -> Unit, + searchManually: (MigratingManga) -> Unit, + migrateNow: (Long) -> Unit, + copyNow: (Long) -> Unit, +) { + Scaffold( + topBar = { scrollBehavior -> + val titleString = stringResource(R.string.migration) + val title by produceState(initialValue = titleString, items, unfinishedCount, titleString) { + withIOContext { + value = "$titleString ($unfinishedCount/${items.size})" + } + } + AppBar( + title = title, + actions = { + IconButton( + onClick = { openMigrationDialog(true) }, + enabled = migrationDone, + ) { + Icon( + imageVector = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll, + contentDescription = stringResource(R.string.copy), + ) + } + IconButton( + onClick = { openMigrationDialog(false) }, + enabled = migrationDone, + ) { + Icon( + imageVector = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll, + contentDescription = stringResource(R.string.migrate), + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { contentPadding -> + ScrollbarLazyColumn( + contentPadding = contentPadding + topSmallPaddingValues, + ) { + items(items, key = { it.manga.id }) { migrationItem -> + Row( + Modifier + .fillMaxWidth() + .animateItemPlacement() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + val result by migrationItem.searchResult.collectAsState() + MigrationItem( + modifier = Modifier + .padding(top = 8.dp) + .weight(1f), + manga = migrationItem.manga, + sourcesString = migrationItem.sourcesString, + chapterInfo = migrationItem.chapterInfo, + onClick = { onMigrationItemClick(migrationItem.manga) }, + ) + + Icon( + Icons.Outlined.ArrowForward, + contentDescription = stringResource(R.string.migrating_to), + modifier = Modifier.weight(0.2f), + ) + + MigrationItemResult( + modifier = Modifier + .padding(top = 8.dp) + .weight(1f), + migrationItem = migrationItem, + result = result, + getManga = getManga, + getChapterInfo = getChapterInfo, + getSourceName = getSourceName, + onMigrationItemClick = onMigrationItemClick, + ) + + MigrationActionIcon( + modifier = Modifier + .weight(0.2f), + result = result, + skipManga = { skipManga(migrationItem.manga.id) }, + searchManually = { searchManually(migrationItem) }, + migrateNow = { + migrateNow(migrationItem.manga.id) + }, + copyNow = { + copyNow(migrationItem.manga.id) + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationActionIcon.kt b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationActionIcon.kt new file mode 100644 index 000000000..91987a825 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationActionIcon.kt @@ -0,0 +1,88 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga +import me.saket.cascade.CascadeDropdownMenu + +@Composable +fun MigrationActionIcon( + modifier: Modifier, + result: MigratingManga.SearchResult, + skipManga: () -> Unit, + searchManually: () -> Unit, + migrateNow: () -> Unit, + copyNow: () -> Unit, +) { + var moreExpanded by remember { mutableStateOf(false) } + val closeMenu = { moreExpanded = false } + + Box(modifier) { + if (result is MigratingManga.SearchResult.Searching) { + IconButton(onClick = skipManga) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.action_stop), + ) + } + } else if (result is MigratingManga.SearchResult.Result || result is MigratingManga.SearchResult.NotFound) { + IconButton(onClick = { moreExpanded = !moreExpanded }) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.abc_action_menu_overflow_description), + ) + } + CascadeDropdownMenu( + expanded = moreExpanded, + onDismissRequest = closeMenu, + offset = DpOffset(8.dp, (-56).dp), + ) { + androidx.compose.material3.DropdownMenuItem( + text = { Text(stringResource(R.string.action_search_manually)) }, + onClick = { + searchManually() + closeMenu() + }, + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text(stringResource(R.string.action_skip_manga)) }, + onClick = { + skipManga() + closeMenu() + }, + ) + if (result is MigratingManga.SearchResult.Result) { + androidx.compose.material3.DropdownMenuItem( + text = { Text(stringResource(R.string.action_migrate_now)) }, + onClick = { + migrateNow() + closeMenu() + }, + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text(stringResource(R.string.action_copy_now)) }, + onClick = { + copyNow() + closeMenu() + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationExitDialog.kt b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationExitDialog.kt new file mode 100644 index 000000000..4a1898953 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationExitDialog.kt @@ -0,0 +1,31 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.kanade.tachiyomi.R + +@Composable +fun MigrationExitDialog( + onDismissRequest: () -> Unit, + exitMigration: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = exitMigration) { + Text(text = stringResource(R.string.action_stop)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + title = { + Text(text = stringResource(R.string.stop_migrating)) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItem.kt new file mode 100644 index 000000000..7398ea47a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItem.kt @@ -0,0 +1,118 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.Badge +import eu.kanade.presentation.components.BadgeGroup +import eu.kanade.presentation.components.MangaCover +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga +import eu.kanade.tachiyomi.util.lang.withIOContext + +@Composable +fun MigrationItem( + modifier: Modifier, + manga: Manga, + sourcesString: String, + chapterInfo: MigratingManga.ChapterInfo, + onClick: () -> Unit, +) { + Column( + modifier + .widthIn(max = 150.dp) + .fillMaxWidth() + .clip(MaterialTheme.shapes.small) + .clickable(onClick = onClick) + .padding(4.dp), + ) { + val context = LocalContext.current + Box( + Modifier.fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio), + ) { + MangaCover.Book( + modifier = Modifier + .fillMaxWidth(), + data = manga, + ) + Box( + modifier = Modifier + .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) + .background( + Brush.verticalGradient( + 0f to Color.Transparent, + 1f to Color(0xAA000000), + ), + ) + .fillMaxHeight(0.33f) + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + Text( + modifier = Modifier + .padding(8.dp) + .align(Alignment.BottomStart), + text = manga.title.ifBlank { stringResource(R.string.unknown) }, + fontSize = 12.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall.copy( + color = Color.White, + shadow = Shadow( + color = Color.Black, + blurRadius = 4f, + ), + ), + ) + BadgeGroup(modifier = Modifier.padding(4.dp)) { + Badge(text = "${chapterInfo.chapterCount}") + } + } + Text( + text = sourcesString, + modifier = Modifier.padding(top = 4.dp, bottom = 1.dp, start = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.titleSmall, + ) + + val formattedLatestChapter by produceState(initialValue = "") { + value = withIOContext { + chapterInfo.getFormattedLatestChapter(context) + } + } + Text( + text = formattedLatestChapter, + modifier = Modifier.padding(top = 1.dp, bottom = 4.dp, start = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.bodyMedium, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItemResult.kt b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItemResult.kt new file mode 100644 index 000000000..668d71a35 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItemResult.kt @@ -0,0 +1,84 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.MangaCover +import eu.kanade.presentation.util.rememberResourceBitmapPainter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga +import eu.kanade.tachiyomi.util.lang.withIOContext + +@Composable +fun MigrationItemResult( + modifier: Modifier, + migrationItem: MigratingManga, + result: MigratingManga.SearchResult, + getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?, + getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo, + getSourceName: (Manga) -> String, + onMigrationItemClick: (Manga) -> Unit, +) { + Box(modifier) { + when (result) { + MigratingManga.SearchResult.Searching -> Box( + modifier = Modifier + .widthIn(max = 150.dp) + .fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + MigratingManga.SearchResult.NotFound -> Image( + painter = rememberResourceBitmapPainter(id = R.drawable.cover_error), + contentDescription = null, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop, + ) + is MigratingManga.SearchResult.Result -> { + val item by produceState?>( + initialValue = null, + migrationItem, + result, + ) { + value = withIOContext { + val manga = getManga(result) ?: return@withIOContext null + Triple( + manga, + getChapterInfo(result), + getSourceName(manga), + ) + } + } + if (item != null) { + val (manga, chapterInfo, source) = item!! + MigrationItem( + modifier = Modifier, + manga = manga, + sourcesString = source, + chapterInfo = chapterInfo, + onClick = { + onMigrationItemClick(manga) + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationMangaDialog.kt new file mode 100644 index 000000000..db1f4ca0d --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/MigrationMangaDialog.kt @@ -0,0 +1,51 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import eu.kanade.tachiyomi.R + +@Composable +fun MigrationMangaDialog( + onDismissRequest: () -> Unit, + copy: Boolean, + mangaSet: Int, + mangaSkipped: Int, + copyManga: () -> Unit, + migrateManga: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = { + if (copy) { + copyManga() + } else { + migrateManga() + } + }, + ) { + Text(text = stringResource(if (copy) R.string.copy else R.string.migrate)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + text = { + Text( + text = pluralStringResource( + if (copy) R.plurals.copy_manga else R.plurals.migrate_manga, + count = mangaSet, + mangaSet, + (if (mangaSkipped > 0) " " + stringResource(R.string.skipping_, mangaSkipped) else ""), + ), + ) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationMangaDialog.kt deleted file mode 100644 index 9ade55106..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationMangaDialog.kt +++ /dev/null @@ -1,44 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController - -class MigrationMangaDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller { - - var copy = false - var mangaSet = 0 - var mangaSkipped = 0 - constructor(target: T, copy: Boolean, mangaSet: Int, mangaSkipped: Int) : this() { - targetController = target - this.copy = copy - this.mangaSet = mangaSet - this.mangaSkipped = mangaSkipped - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val confirmRes = if (copy) R.plurals.copy_manga else R.plurals.migrate_manga - val confirmString = applicationContext?.resources?.getQuantityString( - confirmRes, - mangaSet, - mangaSet, - (if (mangaSkipped > 0) " " + applicationContext?.getString(R.string.skipping_, mangaSkipped) else ""), - ).orEmpty() - return MaterialAlertDialogBuilder(activity!!) - .setMessage(confirmString) - .setPositiveButton(if (copy) R.string.copy else R.string.migrate) { _, _ -> - if (copy) { - (targetController as? MigrationListController)?.copyMangas() - } else { - (targetController as? MigrationListController)?.migrateMangas() - } - } - .setNegativeButton(android.R.string.cancel, null) - .create() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt index c63c6216e..643d799cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationBottomSheetDialog.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.design -import android.app.Activity +import android.content.Context import android.content.res.Resources import android.os.Bundle import android.view.LayoutInflater @@ -21,13 +21,13 @@ import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog import uy.kohesive.injekt.injectLazy -class MigrationBottomSheetDialog(private val activity: Activity, private val listener: StartMigrationListener) : BaseBottomSheetDialog(activity) { +class MigrationBottomSheetDialog(private val baseContext: Context, private val listener: StartMigrationListener) : BaseBottomSheetDialog(baseContext) { private val preferences: UnsortedPreferences by injectLazy() lateinit var binding: MigrationBottomSheetBinding override fun createView(inflater: LayoutInflater): View { - binding = MigrationBottomSheetBinding.inflate(activity.layoutInflater) + binding = MigrationBottomSheetBinding.inflate(LayoutInflater.from(baseContext)) return binding.root } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationSourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationSourceAdapter.kt index 1e45b08aa..793b56cf5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationSourceAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/MigrationSourceAdapter.kt @@ -1,16 +1,15 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.design -import android.os.Bundle import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.source.SourceManager import uy.kohesive.injekt.injectLazy class MigrationSourceAdapter( - controllerPre: PreMigrationController, + listener: FlexibleAdapter.OnItemClickListener, ) : FlexibleAdapter( null, - controllerPre, + listener, true, ) { val sourceManager: SourceManager by injectLazy() @@ -18,32 +17,4 @@ class MigrationSourceAdapter( // SY _-> val sourcePreferences: SourcePreferences by injectLazy() // SY <-- - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - - outState.putParcelableArrayList( - SELECTED_SOURCES_KEY, - ArrayList( - currentItems.map { - it.asParcelable() - }, - ), - ) - } - - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - val selectedSources = savedInstanceState - .getParcelableArrayList(SELECTED_SOURCES_KEY) - - if (selectedSources != null) { - updateDataSet(selectedSources.map { MigrationSourceItem.fromParcelable(sourceManager, it) }) - } - - super.onRestoreInstanceState(savedInstanceState) - } - - companion object { - private const val SELECTED_SOURCES_KEY = "selected_sources" - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt index 4773ade7b..25b7e26a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationController.kt @@ -1,65 +1,16 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.design import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowForward -import androidx.compose.material.icons.outlined.Deselect -import androidx.compose.material.icons.outlined.SelectAll -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Velocity -import androidx.compose.ui.viewinterop.AndroidView import androidx.core.os.bundleOf -import androidx.core.view.ViewCompat -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding -import androidx.recyclerview.widget.LinearLayoutManager +import cafe.adriel.voyager.navigator.Navigator import com.bluelinelabs.conductor.Router -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.UnsortedPreferences -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.components.ExtendedFloatingActionButton -import eu.kanade.presentation.components.OverflowMenu -import eu.kanade.presentation.components.Scaffold -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.PreMigrationListBinding -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig -import eu.kanade.tachiyomi.util.lang.launchIO -import uy.kohesive.injekt.injectLazy -import kotlin.math.roundToInt -class PreMigrationController(bundle: Bundle? = null) : - BasicFullComposeController(bundle), - FlexibleAdapter.OnItemClickListener, - StartMigrationListener { +class PreMigrationController(bundle: Bundle? = null) : BasicFullComposeController(bundle) { constructor(mangaIds: List) : this( bundleOf( @@ -67,275 +18,11 @@ class PreMigrationController(bundle: Bundle? = null) : ), ) - private val sourceManager: SourceManager by injectLazy() - private val prefs: UnsortedPreferences by injectLazy() - private val sourcePreferences: SourcePreferences by injectLazy() - - private var adapter: MigrationSourceAdapter? = null - private val config: LongArray = args.getLongArray(MANGA_IDS_EXTRA) ?: LongArray(0) - private lateinit var dialog: MigrationBottomSheetDialog - - private lateinit var controllerBinding: PreMigrationListBinding - - var items by mutableStateOf(emptyList()) - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - dialog = MigrationBottomSheetDialog(activity!!, this) - - viewScope.launchIO { - items = getEnabledSources() - } - } - @Composable override fun ComposeContent() { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - var fabExpanded by remember { mutableStateOf(true) } - val nestedScrollConnection = remember { - // All this lines just for fab state :/ - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - fabExpanded = available.y >= 0 - return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) - } - - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) - } - - override suspend fun onPreFling(available: Velocity): Velocity { - return scrollBehavior.nestedScrollConnection.onPreFling(available) - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) - } - } - } - Scaffold( - topBar = { - AppBar( - title = stringResource(R.string.select_sources), - navigateUp = router::popCurrentController, - scrollBehavior = scrollBehavior, - actions = { - IconButton(onClick = { massSelect(false) }) { - Icon( - imageVector = Icons.Outlined.Deselect, - contentDescription = stringResource(R.string.select_none), - ) - } - IconButton(onClick = { massSelect(true) }) { - Icon( - imageVector = Icons.Outlined.SelectAll, - contentDescription = stringResource(R.string.action_select_all), - ) - } - OverflowMenu { closeMenu -> - DropdownMenuItem( - text = { Text(stringResource(R.string.match_enabled_sources)) }, - onClick = { - matchSelection(true) - closeMenu() - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.match_pinned_sources)) }, - onClick = { - matchSelection(false) - closeMenu() - }, - ) - } - }, - ) - }, - floatingActionButton = { - ExtendedFloatingActionButton( - text = { Text(text = stringResource(R.string.action_migrate)) }, - icon = { - Icon( - imageVector = Icons.Outlined.ArrowForward, - contentDescription = stringResource(R.string.action_migrate), - ) - }, - onClick = { - if (!dialog.isShowing) { - dialog.show() - } - }, - expanded = fabExpanded, - modifier = Modifier.navigationBarsPadding(), - ) - }, - ) { contentPadding -> - val density = LocalDensity.current - val layoutDirection = LocalLayoutDirection.current - val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() } - val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() } - val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() } - val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() } - Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) { - AndroidView( - factory = { context -> - controllerBinding = PreMigrationListBinding.inflate(LayoutInflater.from(context)) - adapter = MigrationSourceAdapter(this@PreMigrationController) - controllerBinding.recycler.adapter = adapter - adapter?.isHandleDragEnabled = true - adapter?.fastScroller = controllerBinding.fastScroller - controllerBinding.recycler.layoutManager = LinearLayoutManager(context) - - ViewCompat.setNestedScrollingEnabled(controllerBinding.root, true) - - controllerBinding.root - }, - update = { - controllerBinding.recycler - .updatePadding( - left = left, - top = top, - right = right, - bottom = bottom, - ) - - controllerBinding.fastScroller - .updateLayoutParams { - leftMargin = left - topMargin = top - rightMargin = right - bottomMargin = bottom - } - - adapter?.updateDataSet(items) - }, - ) - } - } - } - - override fun startMigration(extraParam: String?) { - val listOfSources = adapter?.currentItems - ?.filterIsInstance() - ?.filter { - it.sourceEnabled - } - ?.joinToString("/") { it.source.id.toString() } - .orEmpty() - - prefs.migrationSources().set(listOfSources) - - router.replaceTopController( - MigrationListController( - MigrationProcedureConfig( - config.toList(), - extraSearchParams = extraParam, - ), - ).withFadeTransaction().tag(MigrationListController.TAG), - ) - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - adapter?.onSaveInstanceState(outState) - } - - // TODO Still incorrect, why is this called before onViewCreated? - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - adapter?.onRestoreInstanceState(savedInstanceState) - } - - override fun onItemClick(view: View, position: Int): Boolean { - val adapter = adapter ?: return false - adapter.getItem(position)?.let { - it.sourceEnabled = !it.sourceEnabled - } - adapter.notifyItemChanged(position) - return false - } - - /** - * Returns a list of enabled sources ordered by language and name. - * - * @return list containing enabled sources. - */ - private fun getEnabledSources(): List { - val languages = sourcePreferences.enabledLanguages().get() - val sourcesSaved = prefs.migrationSources().get().split("/") - .mapNotNull { it.toLongOrNull() } - val disabledSources = sourcePreferences.disabledSources().get() - .mapNotNull { it.toLongOrNull() } - val sources = sourceManager.getVisibleCatalogueSources() - .asSequence() - .filterIsInstance() - .filter { it.lang in languages } - .sortedBy { "(${it.lang}) ${it.name}" } - .map { - MigrationSourceItem( - it, - isEnabled( - sourcesSaved, - disabledSources, - it.id, - ), - ) - } - .toList() - - return sources - .filter { it.sourceEnabled } - .sortedBy { sourcesSaved.indexOf(it.source.id) } - .plus( - sources.filterNot { it.sourceEnabled }, - ) - } - - fun isEnabled( - sourcesSaved: List, - disabledSources: List, - id: Long, - ): Boolean { - return if (sourcesSaved.isEmpty()) { - id !in disabledSources - } else { - id in sourcesSaved - } - } - - private fun massSelect(selectAll: Boolean) { - val adapter = adapter ?: return - adapter.currentItems.forEach { - it.sourceEnabled = selectAll - } - adapter.notifyDataSetChanged() - } - - private fun matchSelection(matchEnabled: Boolean) { - val adapter = adapter ?: return - val enabledSources = if (matchEnabled) { - sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() } - } else { - sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() } - } - val items = adapter.currentItems.toList() - items.forEach { - it.sourceEnabled = if (matchEnabled) { - it.source.id !in enabledSources - } else { - it.source.id in enabledSources - } - } - val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled } - adapter.updateDataSet(sortedItems) + Navigator(screen = PreMigrationScreen(config.toList())) } companion object { @@ -349,7 +36,7 @@ class PreMigrationController(bundle: Bundle? = null) : ) } else { PreMigrationController(mangaIds) - }.withFadeTransaction().tag(if (skipPre) MigrationListController.TAG else null), + }.withFadeTransaction().tag(MigrationListController.TAG), ) } } 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 new file mode 100644 index 000000000..369e0954b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreen.kt @@ -0,0 +1,217 @@ +package eu.kanade.tachiyomi.ui.browse.migration.advanced.design + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowForward +import androidx.compose.material.icons.outlined.Deselect +import androidx.compose.material.icons.outlined.SelectAll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.ViewCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.LinearLayoutManager +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.ExtendedFloatingActionButton +import eu.kanade.presentation.components.OverflowMenu +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.PreMigrationListBinding +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig +import kotlin.math.roundToInt + +class PreMigrationScreen(val mangaIds: List) : Screen { + + @Composable + override fun Content() { + val screenModel = rememberScreenModel { PreMigrationScreenModel() } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val router = LocalRouter.currentOrThrow + val navigator = LocalNavigator.currentOrThrow + var fabExpanded by remember { mutableStateOf(true) } + val items by screenModel.state.collectAsState() + val context = LocalContext.current + DisposableEffect(screenModel) { + screenModel.dialog = MigrationBottomSheetDialog(context, screenModel.listener) + onDispose {} + } + + LaunchedEffect(screenModel) { + screenModel.startMigration.collect { extraParam -> + navigator replace MigrationListScreen(MigrationProcedureConfig(mangaIds, extraParam)) + } + } + + val nestedScrollConnection = remember { + // All this lines just for fab state :/ + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + fabExpanded = available.y >= 0 + return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + return scrollBehavior.nestedScrollConnection.onPreFling(available) + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) + } + } + } + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.select_sources), + navigateUp = { + when { + navigator.canPop -> navigator.pop() + else -> router.popCurrentController() + } + }, + scrollBehavior = scrollBehavior, + actions = { + IconButton(onClick = { screenModel.massSelect(false) }) { + Icon( + imageVector = Icons.Outlined.Deselect, + contentDescription = stringResource(R.string.select_none), + ) + } + IconButton(onClick = { screenModel.massSelect(true) }) { + Icon( + imageVector = Icons.Outlined.SelectAll, + contentDescription = stringResource(R.string.action_select_all), + ) + } + OverflowMenu { closeMenu -> + androidx.compose.material3.DropdownMenuItem( + text = { Text(stringResource(R.string.match_enabled_sources)) }, + onClick = { + screenModel.matchSelection(true) + closeMenu() + }, + ) + androidx.compose.material3.DropdownMenuItem( + text = { Text(stringResource(R.string.match_pinned_sources)) }, + onClick = { + screenModel.matchSelection(false) + closeMenu() + }, + ) + } + }, + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text(text = stringResource(R.string.action_migrate)) }, + icon = { + Icon( + imageVector = Icons.Outlined.ArrowForward, + contentDescription = stringResource(R.string.action_migrate), + ) + }, + onClick = { + if (!screenModel.dialog.isShowing) { + screenModel.dialog.show() + } + }, + expanded = fabExpanded, + modifier = Modifier.navigationBarsPadding(), + ) + }, + ) { contentPadding -> + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() } + val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() } + val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() } + val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() } + Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) { + AndroidView( + factory = { context -> + screenModel.controllerBinding = PreMigrationListBinding.inflate(LayoutInflater.from(context)) + screenModel.adapter = MigrationSourceAdapter(screenModel.clickListener) + screenModel.controllerBinding.recycler.adapter = screenModel.adapter + screenModel.adapter?.isHandleDragEnabled = true + screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller + screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(context) + + ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true) + + screenModel.controllerBinding.root + }, + update = { + screenModel.controllerBinding.recycler + .updatePadding( + left = left, + top = top, + right = right, + bottom = bottom, + ) + + screenModel.controllerBinding.fastScroller + .updateLayoutParams { + leftMargin = left + topMargin = top + rightMargin = right + bottomMargin = bottom + } + + screenModel.adapter?.updateDataSet(items) + }, + ) + } + } + } + + companion object { + fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List) { + navigator.push( + if (skipPre) { + MigrationListScreen( + MigrationProcedureConfig(mangaIds, null), + ) + } else { + PreMigrationScreen(mangaIds) + }, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreenModel.kt new file mode 100644 index 000000000..f1e4f1169 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/design/PreMigrationScreenModel.kt @@ -0,0 +1,143 @@ +package eu.kanade.tachiyomi.ui.browse.migration.advanced.design + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.domain.UnsortedPreferences +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.databinding.PreMigrationListBinding +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.lang.launchIO +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class PreMigrationScreenModel( + private val sourceManager: SourceManager = Injekt.get(), + private val prefs: UnsortedPreferences = Injekt.get(), + private val sourcePreferences: SourcePreferences = Injekt.get(), +) : ScreenModel { + + private val _state = MutableStateFlow(emptyList()) + val state = _state.asStateFlow() + + lateinit var controllerBinding: PreMigrationListBinding + var adapter: MigrationSourceAdapter? = null + + val startMigration = MutableSharedFlow() + + val listener = object : StartMigrationListener { + override fun startMigration(extraParam: String?) { + val listOfSources = adapter?.currentItems + ?.filterIsInstance() + ?.filter { + it.sourceEnabled + } + ?.joinToString("/") { it.source.id.toString() } + .orEmpty() + + prefs.migrationSources().set(listOfSources) + + coroutineScope.launch { + startMigration.emit(extraParam) + } + } + } + val clickListener = FlexibleAdapter.OnItemClickListener { _, position -> + val adapter = adapter ?: return@OnItemClickListener false + adapter.getItem(position)?.let { + it.sourceEnabled = !it.sourceEnabled + } + adapter.notifyItemChanged(position) + false + } + + lateinit var dialog: MigrationBottomSheetDialog + + init { + coroutineScope.launchIO { + val enabledSources = getEnabledSources() + _state.update { enabledSources } + } + } + + /** + * Returns a list of enabled sources ordered by language and name. + * + * @return list containing enabled sources. + */ + private fun getEnabledSources(): List { + val languages = sourcePreferences.enabledLanguages().get() + val sourcesSaved = prefs.migrationSources().get().split("/") + .mapNotNull { it.toLongOrNull() } + val disabledSources = sourcePreferences.disabledSources().get() + .mapNotNull { it.toLongOrNull() } + val sources = sourceManager.getVisibleCatalogueSources() + .asSequence() + .filterIsInstance() + .filter { it.lang in languages } + .sortedBy { "(${it.lang}) ${it.name}" } + .map { + MigrationSourceItem( + it, + isEnabled( + sourcesSaved, + disabledSources, + it.id, + ), + ) + } + .toList() + + return sources + .filter { it.sourceEnabled } + .sortedBy { sourcesSaved.indexOf(it.source.id) } + .plus( + sources.filterNot { it.sourceEnabled }, + ) + } + + fun isEnabled( + sourcesSaved: List, + disabledSources: List, + id: Long, + ): Boolean { + return if (sourcesSaved.isEmpty()) { + id !in disabledSources + } else { + id in sourcesSaved + } + } + + fun massSelect(selectAll: Boolean) { + val adapter = adapter ?: return + adapter.currentItems.forEach { + it.sourceEnabled = selectAll + } + adapter.notifyDataSetChanged() + } + + fun matchSelection(matchEnabled: Boolean) { + val adapter = adapter ?: return + val enabledSources = if (matchEnabled) { + sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() } + } else { + sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() } + } + val items = adapter.currentItems.toList() + items.forEach { + it.sourceEnabled = if (matchEnabled) { + it.source.id !in enabledSources + } else { + it.source.id in enabledSources + } + } + val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled } + adapter.updateDataSet(sortedItems) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt index 47a1c83dc..a557f8488 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListController.kt @@ -1,84 +1,14 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process import android.os.Bundle -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowForward -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.ContentCopy -import androidx.compose.material.icons.outlined.CopyAll -import androidx.compose.material.icons.outlined.Done -import androidx.compose.material.icons.outlined.DoneAll -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shadow -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.os.bundleOf -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.domain.manga.model.Manga -import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.components.Badge -import eu.kanade.presentation.components.BadgeGroup -import eu.kanade.presentation.components.MangaCover -import eu.kanade.presentation.components.Scaffold -import eu.kanade.presentation.components.ScrollbarLazyColumn -import eu.kanade.presentation.util.plus -import eu.kanade.presentation.util.rememberResourceBitmapPainter -import eu.kanade.presentation.util.topSmallPaddingValues -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler -import eu.kanade.tachiyomi.ui.base.controller.FullComposeController -import eu.kanade.tachiyomi.ui.base.controller.pushController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.migration.MigrationMangaDialog -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController -import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult -import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.lang.withIOContext -import eu.kanade.tachiyomi.util.system.getParcelableCompat -import eu.kanade.tachiyomi.util.system.toast -import me.saket.cascade.CascadeDropdownMenu +import cafe.adriel.voyager.navigator.Navigator +import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController +import eu.kanade.tachiyomi.util.system.getSerializableCompat class MigrationListController(bundle: Bundle? = null) : - FullComposeController(bundle) { + BasicFullComposeController(bundle) { constructor(config: MigrationProcedureConfig) : this( bundleOf( @@ -86,414 +16,11 @@ class MigrationListController(bundle: Bundle? = null) : ), ) - val config = args.getParcelableCompat(CONFIG_EXTRA) - - private var selectedMangaId: Long? = null - private var manualMigrations = 0 - - override fun createPresenter(): MigrationListPresenter { - return MigrationListPresenter(config!!) - } + val config = args.getSerializableCompat(CONFIG_EXTRA)!! @Composable override fun ComposeContent() { - val items by presenter.migratingItems.collectAsState() - val migrationDone by presenter.migrationDone.collectAsState() - val unfinishedCount by presenter.unfinishedCount.collectAsState() - Scaffold( - topBar = { scrollBehavior -> - val titleString = stringResource(R.string.migration) - val title by produceState(initialValue = titleString, items, unfinishedCount, titleString) { - withIOContext { - value = "$titleString ($unfinishedCount/${items.size})" - } - } - AppBar( - title = title, - actions = { - IconButton( - onClick = { openMigrationDialog(true) }, - enabled = migrationDone, - ) { - Icon( - imageVector = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll, - contentDescription = stringResource(R.string.copy), - ) - } - IconButton( - onClick = { openMigrationDialog(false) }, - enabled = migrationDone, - ) { - Icon( - imageVector = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll, - contentDescription = stringResource(R.string.migrate), - ) - } - }, - scrollBehavior = scrollBehavior, - ) - }, - ) { contentPadding -> - ScrollbarLazyColumn( - contentPadding = contentPadding + topSmallPaddingValues, - ) { - items(items, key = { it.manga.id }) { migrationItem -> - Row( - Modifier - .fillMaxWidth() - .animateItemPlacement() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - val result by migrationItem.searchResult.collectAsState() - MigrationItem( - modifier = Modifier - .padding(top = 8.dp) - .weight(1f), - manga = migrationItem.manga, - sourcesString = migrationItem.sourcesString, - chapterInfo = migrationItem.chapterInfo, - onClick = { - router.pushController( - MangaController( - migrationItem.manga.id, - true, - ), - ) - }, - ) - - Icon( - Icons.Outlined.ArrowForward, - contentDescription = stringResource(R.string.migrating_to), - modifier = Modifier.weight(0.2f), - ) - - MigrationItemResult( - modifier = Modifier - .padding(top = 8.dp) - .weight(1f), - migrationItem = migrationItem, - result = result, - ) - - MigrationActionIcon( - modifier = Modifier - .weight(0.2f), - result = result, - skipManga = { presenter.removeManga(migrationItem.manga.id) }, - searchManually = { - val manga = migrationItem.manga - selectedMangaId = manga.id - val sources = presenter.getMigrationSources() - val validSources = if (sources.size == 1) { - sources - } else { - sources.filter { it.id != manga.source } - } - val searchController = SearchController(manga, validSources) - searchController.targetController = this@MigrationListController - router.pushController(searchController) - }, - migrateNow = { - migrateManga(migrationItem.manga.id, false) - manualMigrations++ - }, - copyNow = { - migrateManga(migrationItem.manga.id, true) - manualMigrations++ - }, - ) - } - } - } - } - } - - @Composable - fun MigrationItem( - modifier: Modifier, - manga: Manga, - sourcesString: String, - chapterInfo: MigratingManga.ChapterInfo, - onClick: () -> Unit, - ) { - Column( - modifier - .widthIn(max = 150.dp) - .fillMaxWidth() - .clip(MaterialTheme.shapes.small) - .clickable(onClick = onClick) - .padding(4.dp), - ) { - val context = LocalContext.current - Box( - Modifier.fillMaxWidth() - .aspectRatio(MangaCover.Book.ratio), - ) { - MangaCover.Book( - modifier = Modifier - .fillMaxWidth(), - data = manga, - ) - Box( - modifier = Modifier - .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) - .background( - Brush.verticalGradient( - 0f to Color.Transparent, - 1f to Color(0xAA000000), - ), - ) - .fillMaxHeight(0.33f) - .fillMaxWidth() - .align(Alignment.BottomCenter), - ) - Text( - modifier = Modifier - .padding(8.dp) - .align(Alignment.BottomStart), - text = manga.title.ifBlank { stringResource(R.string.unknown) }, - fontSize = 12.sp, - lineHeight = 18.sp, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleSmall.copy( - color = Color.White, - shadow = Shadow( - color = Color.Black, - blurRadius = 4f, - ), - ), - ) - BadgeGroup(modifier = Modifier.padding(4.dp)) { - Badge(text = "${chapterInfo.chapterCount}") - } - } - Text( - text = sourcesString, - modifier = Modifier.padding(top = 4.dp, bottom = 1.dp, start = 8.dp), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = MaterialTheme.typography.titleSmall, - ) - - val formattedLatestChapter by produceState(initialValue = "") { - value = withIOContext { - chapterInfo.getFormattedLatestChapter(context) - } - } - Text( - text = formattedLatestChapter, - modifier = Modifier.padding(top = 1.dp, bottom = 4.dp, start = 8.dp), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - - @Composable - fun MigrationActionIcon( - modifier: Modifier, - result: SearchResult, - skipManga: () -> Unit, - searchManually: () -> Unit, - migrateNow: () -> Unit, - copyNow: () -> Unit, - ) { - var moreExpanded by remember { mutableStateOf(false) } - val closeMenu = { moreExpanded = false } - - Box(modifier) { - if (result is SearchResult.Searching) { - IconButton(onClick = skipManga) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = stringResource(R.string.action_stop), - ) - } - } else if (result is SearchResult.Result || result is SearchResult.NotFound) { - IconButton(onClick = { moreExpanded = !moreExpanded }) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = stringResource(R.string.abc_action_menu_overflow_description), - ) - } - CascadeDropdownMenu( - expanded = moreExpanded, - onDismissRequest = closeMenu, - offset = DpOffset(8.dp, (-56).dp), - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.action_search_manually)) }, - onClick = { - searchManually() - closeMenu() - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.action_skip_manga)) }, - onClick = { - skipManga() - closeMenu() - }, - ) - if (result is SearchResult.Result) { - DropdownMenuItem( - text = { Text(stringResource(R.string.action_migrate_now)) }, - onClick = { - migrateNow() - closeMenu() - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.action_copy_now)) }, - onClick = { - copyNow() - closeMenu() - }, - ) - } - } - } - } - } - - @Composable - fun MigrationItemResult(modifier: Modifier, migrationItem: MigratingManga, result: SearchResult) { - Box(modifier) { - when (result) { - SearchResult.Searching -> Box( - modifier = Modifier - .widthIn(max = 150.dp) - .fillMaxWidth() - .aspectRatio(MangaCover.Book.ratio), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - SearchResult.NotFound -> Image( - painter = rememberResourceBitmapPainter(id = R.drawable.cover_error), - contentDescription = null, - modifier = Modifier - .matchParentSize() - .clip(RoundedCornerShape(4.dp)), - contentScale = ContentScale.Crop, - ) - is SearchResult.Result -> { - val item by produceState?>( - initialValue = null, - migrationItem, - result, - ) { - value = withIOContext { - val manga = presenter.getManga(result) ?: return@withIOContext null - Triple( - manga, - presenter.getChapterInfo(result), - presenter.getSourceName(manga), - ) - } - } - if (item != null) { - val (manga, chapterInfo, source) = item!! - MigrationItem( - modifier = Modifier, - manga = manga, - sourcesString = source, - chapterInfo = chapterInfo, - onClick = { - router.pushController( - MangaController( - manga.id, - true, - ), - ) - }, - ) - } - } - } - } - } - - private fun noMigration() { - val res = resources - if (res != null) { - activity?.toast( - res.getQuantityString( - R.plurals.manga_migrated, - manualMigrations, - manualMigrations, - ), - ) - } - if (!presenter.hideNotFound) { - router.popCurrentController() - } - } - - fun useMangaForMigration(manga: Manga, source: Source) { - presenter.useMangaForMigration(manga, source, selectedMangaId ?: return) - } - - fun migrateMangas() { - presenter.migrateMangas() - } - - fun copyMangas() { - presenter.copyMangas() - } - - fun migrateManga(mangaId: Long, copy: Boolean) { - presenter.migrateManga(mangaId, copy) - } - - fun removeManga(mangaId: Long) { - presenter.removeManga(mangaId) - } - - fun sourceFinished() { - if (presenter.migratingItems.value.isEmpty()) noMigration() - } - - fun navigateOut(manga: Manga?) { - if (manga != null) { - val newStack = router.backstack.filter { - it.controller !is MangaController && - it.controller !is MigrationListController && - it.controller !is PreMigrationController - } + MangaController(manga.id).withFadeTransaction() - router.setBackstack(newStack, OneWayFadeChangeHandler()) - return - } - router.popCurrentController() - } - - override fun handleBack(): Boolean { - activity?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.stop_migrating) - .setPositiveButton(R.string.action_stop) { _, _ -> - router.popCurrentController() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - return true - } - - private fun openMigrationDialog(copy: Boolean) { - val totalManga = presenter.migratingItems.value.size - val mangaSkipped = presenter.mangasSkipped() - MigrationMangaDialog( - this, - copy, - totalManga, - mangaSkipped, - ).showDialog(router) + Navigator(screen = MigrationListScreen(config)) } companion object { 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 new file mode 100644 index 000000000..b66957f6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt @@ -0,0 +1,177 @@ +package eu.kanade.tachiyomi.ui.browse.migration.advanced.process + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +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.util.LocalRouter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler +import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen +import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.manga.MangaScreen +import eu.kanade.tachiyomi.util.lang.withUIContext +import eu.kanade.tachiyomi.util.system.toast + +class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen { + + @Composable + override fun Content() { + val screenModel = rememberScreenModel { MigrationListScreenModel(config) } + val items by screenModel.migratingItems.collectAsState() + val migrationDone by screenModel.migrationDone.collectAsState() + val unfinishedCount by screenModel.unfinishedCount.collectAsState() + val dialog by screenModel.dialog.collectAsState() + val navigator = LocalNavigator.currentOrThrow + val router = LocalRouter.currentOrThrow + val context = LocalContext.current + LaunchedEffect(items) { + if (items.isEmpty()) { + val manualMigrations = screenModel.manualMigrations.value + context.toast( + context.resources.getQuantityString( + R.plurals.manga_migrated, + manualMigrations, + manualMigrations, + ), + ) + if (!screenModel.hideNotFound) { + if (navigator.canPop) { + navigator.pop() + } else { + router.popCurrentController() + } + } + } + } + + LaunchedEffect(screenModel) { + screenModel.navigateOut.collect { + if (navigator.canPop) { + if (items.size == 1) { + val hasDetails = navigator.items.any { it is MangaScreen } + if (hasDetails) { + val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let { + screenModel.getManga(it.id) + } + withUIContext { + if (manga != null) { + val newStack = navigator.items.filter { + it !is MangaScreen && + it !is MigrationListScreen && + it !is PreMigrationScreen + } + MangaScreen(manga.id) + navigator replaceAll newStack.first() + navigator.push(newStack.drop(1)) + } else { + navigator.pop() + } + } + } + } else { + withUIContext { + navigator.pop() + } + } + } else { + if (items.size == 1) { + val hasDetails = router.backstack.any { it.controller is MangaController } + if (hasDetails) { + val manga = (items.firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.let { + screenModel.getManga(it.id) + } + withUIContext { + if (manga != null) { + val newStack = router.backstack.filter { + it.controller !is MangaController && + it.controller !is MigrationListController && + it.controller !is PreMigrationController + } + MangaController(manga.id).withFadeTransaction() + router.setBackstack(newStack, OneWayFadeChangeHandler()) + } else { + router.popCurrentController() + } + } + } + } else { + withUIContext { + router.popCurrentController() + } + } + } + } + } + MigrationListScreen( + items = items, + migrationDone = migrationDone, + unfinishedCount = unfinishedCount, + getManga = screenModel::getManga, + getChapterInfo = screenModel::getChapterInfo, + getSourceName = screenModel::getSourceName, + onMigrationItemClick = { + navigator.push(MangaScreen(it.id, true)) + }, + openMigrationDialog = screenModel::openMigrateDialog, + skipManga = { screenModel.removeManga(it) }, + searchManually = { migrationItem -> + val sources = screenModel.getMigrationSources() + val validSources = if (sources.size == 1) { + sources + } else { + sources.filter { it.id != migrationItem.manga.source } + } + val searchController = SearchController(migrationItem.manga, validSources) + searchController.useMangaForMigration = { manga, source -> + screenModel.useMangaForMigration(context, manga, source, migrationItem.manga.id) + } + router.pushController(searchController) + }, + migrateNow = { screenModel.migrateManga(it, false) }, + copyNow = { screenModel.migrateManga(it, true) }, + ) + + val onDismissRequest = { screenModel.dialog.value = null } + when (val dialog = dialog) { + is MigrationListScreenModel.Dialog.MigrateMangaDialog -> { + MigrationMangaDialog( + onDismissRequest = onDismissRequest, + copy = dialog.copy, + mangaSet = dialog.mangaSet, + mangaSkipped = dialog.mangaSkipped, + copyManga = screenModel::copyMangas, + migrateManga = screenModel::migrateMangas, + ) + } + MigrationListScreenModel.Dialog.MigrationExitDialog -> { + MigrationExitDialog( + onDismissRequest = onDismissRequest, + exitMigration = { + if (navigator.canPop) { + navigator.pop() + } else { + router.popCurrentController() + } + }, + ) + } + null -> Unit + } + + BackHandler(true) { + screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt similarity index 86% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt index 1396ee70a..a262ab196 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process -import android.os.Bundle +import android.content.Context import android.widget.Toast +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope import eu.kanade.domain.UnsortedPreferences import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.SetMangaCategories @@ -30,10 +32,8 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.online.all.EHentai -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult -import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat @@ -42,10 +42,11 @@ 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.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.sync.Semaphore @@ -55,7 +56,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.atomic.AtomicInteger -class MigrationListPresenter( +class MigrationListScreenModel( private val config: MigrationProcedureConfig, private val preferences: UnsortedPreferences = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), @@ -74,56 +75,56 @@ class MigrationListPresenter( private val getTracks: GetTracks = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(), private val deleteTrack: DeleteTrack = Injekt.get(), -) : BasePresenter() { +) : ScreenModel { private val smartSearchEngine = SmartSearchEngine(config.extraSearchParams) private val throttleManager = EHentaiThrottleManager() - var migrationsJob: Job? = null - private set - val migratingItems = MutableStateFlow>(emptyList()) val migrationDone = MutableStateFlow(false) val unfinishedCount = MutableStateFlow(0) + val manualMigrations = MutableStateFlow(0) + val hideNotFound = preferences.hideNotFoundMigration().get() - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + val navigateOut = MutableSharedFlow() - if (migrationsJob?.isActive != true) { - migrationsJob = presenterScope.launchIO { - runMigrations( - config.mangaIds - .map { - async { - val manga = getManga.await(it) ?: return@async null - MigratingManga( - manga = manga, - chapterInfo = getChapterInfo(it), - sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo( - if (manga.source == MERGED_SOURCE_ID) { - getMergedReferencesById.await(manga.id) - .map { sourceManager.getOrStub(it.mangaSourceId) } - } else { - null - }, - ), - parentContext = presenterScope.coroutineContext, - ) - } + val dialog = MutableStateFlow(null) + + init { + coroutineScope.launchIO { + runMigrations( + config.mangaIds + .map { + async { + val manga = getManga.await(it) ?: return@async null + MigratingManga( + manga = manga, + chapterInfo = getChapterInfo(it), + sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo( + if (manga.source == MERGED_SOURCE_ID) { + getMergedReferencesById.await(manga.id) + .map { sourceManager.getOrStub(it.mangaSourceId) } + } else { + null + }, + ), + parentContext = coroutineScope.coroutineContext, + ) } - .awaitAll() - .filterNotNull() - .also { - migratingItems.value = it - }, - ) - } + } + .awaitAll() + .filterNotNull() + .also { + migratingItems.value = it + }, + ) } } - suspend fun getManga(result: SearchResult.Result) = getManga.await(result.id) + suspend fun getManga(result: SearchResult.Result) = getManga(result.id) + suspend fun getManga(id: Long) = getManga.await(id) suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id) suspend fun getChapterInfo(id: Long) = getChapterByMangaId.await(id).let { chapters -> MigratingManga.ChapterInfo( @@ -146,7 +147,7 @@ class MigrationListPresenter( val sources = getMigrationSources() for (manga in mangas) { - if (migrationsJob?.isCancelled == true) { + if (!currentCoroutineContext().isActive) { break } // in case it was removed @@ -224,7 +225,7 @@ class MigrationListPresenter( source.getChapterList(localManga.toSManga()) } } catch (e: Exception) { - this@MigrationListPresenter.logcat(LogPriority.ERROR, e) + this@MigrationListScreenModel.logcat(LogPriority.ERROR, e) emptyList() } syncChaptersWithSource.await(chapters, localManga, source) @@ -274,14 +275,13 @@ class MigrationListPresenter( } } - private suspend fun sourceFinished() { + private fun sourceFinished() { unfinishedCount.value = migratingItems.value.count { it.searchResult.value != SearchResult.Searching } if (allMangasDone()) { migrationDone.value = true } - withUIContext { view?.sourceFinished() } } fun allMangasDone() = migratingItems.value.all { it.searchResult.value != SearchResult.Searching } && @@ -383,11 +383,11 @@ class MigrationListPresenter( updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate)) } - fun useMangaForMigration(manga: Manga, source: Source, selectedMangaId: Long) { + fun useMangaForMigration(context: Context, manga: Manga, source: Source, selectedMangaId: Long) { val migratingManga = migratingItems.value.find { it.manga.id == selectedMangaId } ?: return migratingManga.searchResult.value = SearchResult.Searching - presenterScope.launchIO { + coroutineScope.launchIO { val result = migratingManga.migrationScope.async { val localManga = networkToLocalManga.await(manga) try { @@ -413,14 +413,14 @@ class MigrationListPresenter( } else { migratingManga.searchResult.value = SearchResult.NotFound withUIContext { - view?.activity?.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG) + context.toast(R.string.no_chapters_found_for_migration, Toast.LENGTH_LONG) } } } } fun migrateMangas() { - presenterScope.launchIO { + coroutineScope.launchIO { migratingItems.value.forEach { manga -> val searchResult = manga.searchResult.value if (searchResult is SearchResult.Result) { @@ -438,7 +438,7 @@ class MigrationListPresenter( } fun copyMangas() { - presenterScope.launchIO { + coroutineScope.launchIO { migratingItems.value.forEach { manga -> val searchResult = manga.searchResult.value if (searchResult is SearchResult.Result) { @@ -455,26 +455,12 @@ class MigrationListPresenter( } private suspend fun navigateOut() { - val view = view ?: return - if (migratingItems.value.size == 1) { - val hasDetails = view.router.backstack.any { it.controller is MangaController } - if (hasDetails) { - val manga = (migratingItems.value.firstOrNull()?.searchResult?.value as? SearchResult.Result)?.let { - getManga.await(it.id) - } - withUIContext { - view.navigateOut(manga) - } - return - } - } - withUIContext { - view.navigateOut(null) - } + navigateOut.emit(Unit) } fun migrateManga(mangaId: Long, copy: Boolean) { - presenterScope.launchIO { + manualMigrations.value++ + coroutineScope.launchIO { val manga = migratingItems.value.find { it.manga.id == mangaId } ?: return@launchIO @@ -491,7 +477,7 @@ class MigrationListPresenter( } fun removeManga(mangaId: Long) { - presenterScope.launchIO { + coroutineScope.launchIO { val item = migratingItems.value.find { it.manga.id == mangaId } ?: return@launchIO if (migratingItems.value.size == 1) { @@ -517,11 +503,25 @@ class MigrationListPresenter( } } - override fun onDestroy() { - super.onDestroy() - migrationsJob?.cancel() + override fun onDispose() { + super.onDispose() migratingItems.value.forEach { it.migrationScope.cancel() } } + + fun openMigrateDialog( + copy: Boolean, + ) { + dialog.value = Dialog.MigrateMangaDialog( + copy, + migratingItems.value.size, + mangasSkipped(), + ) + } + + sealed class Dialog { + data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog() + object MigrationExitDialog : Dialog() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcedureConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcedureConfig.kt index a3d813bc5..c0469b885 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcedureConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcedureConfig.kt @@ -1,10 +1,8 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import java.io.Serializable -@Parcelize data class MigrationProcedureConfig( var mangaIds: List, val extraSearchParams: String?, -) : Parcelable +) : Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt index e42be502c..f18132e98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt @@ -5,9 +5,9 @@ import androidx.core.os.bundleOf import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.pushController -import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter import kotlinx.coroutines.runBlocking @@ -24,24 +24,23 @@ class SearchController( SOURCES to sources?.map { it.id }?.toLongArray(), ), ) { - constructor(targetController: MigrationListController?, mangaId: Long, sources: LongArray) : + constructor(mangaId: Long, sources: LongArray) : this( runBlocking { Injekt.get() .await(mangaId) }, sources.map { Injekt.get().getOrStub(it) }.filterIsInstance(), - ) { - this.targetController = targetController - } + ) @Suppress("unused") constructor(bundle: Bundle) : this( - null, bundle.getLong(OLD_MANGA), bundle.getLongArray(SOURCES) ?: LongArray(0), ) + var useMangaForMigration: ((Manga, Source) -> Unit)? = null + /** * Called when controller is initialized. */ @@ -58,10 +57,9 @@ class SearchController( } override fun onMangaClick(manga: Manga) { - val migrationListController = targetController as MigrationListController val sourceManager = Injekt.get() val source = sourceManager.get(manga.source) ?: return - migrationListController.useMangaForMigration(manga, source) + useMangaForMigration?.let { it(manga, source) } router.popCurrentController() } @@ -73,7 +71,7 @@ class SearchController( override fun onTitleClick(source: CatalogueSource) { presenter.sourcePreferences.lastUsedSource().set(source.id) - router.pushController(SourceSearchController(targetController as? MigrationListController ?: return, manga!!, source, presenter.query)) + router.pushController(SourceSearchController(manga!!, source, presenter.query).also { it.useMangaForMigration = useMangaForMigration }) } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt index 2cbc76112..9c0cf59a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt @@ -7,9 +7,9 @@ import androidx.core.os.bundleOf import eu.kanade.domain.manga.model.Manga import eu.kanade.presentation.browse.SourceSearchScreen import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.webview.WebViewActivity import uy.kohesive.injekt.Injekt @@ -19,15 +19,15 @@ class SourceSearchController( bundle: Bundle, ) : BrowseSourceController(bundle) { - constructor(targetController: MigrationListController, manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this( + constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this( bundleOf( SOURCE_ID_KEY to source.id, MANGA_KEY to manga, SEARCH_QUERY_KEY to searchQuery, ), - ) { - this.targetController = targetController - } + ) + + var useMangaForMigration: ((Manga, Source) -> Unit)? = null @Composable override fun ComposeContent() { @@ -37,11 +37,11 @@ class SourceSearchController( onFabClick = { filterSheet?.show() }, // SY --> onMangaClick = { manga -> - val migrationListController = targetController as? MigrationListController ?: return@SourceSearchScreen val sourceManager = Injekt.get() val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen - migrationListController.useMangaForMigration(manga, source) - router.popToTag(MigrationListController.TAG) + useMangaForMigration?.let { it(manga, source) } + router.popCurrentController() + router.popCurrentController() }, // SY <-- onWebViewClick = f@{