diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index d805f3b43..5a5dd79ae 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -4,7 +4,11 @@ import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.Public +import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -12,7 +16,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import dev.icerock.moko.resources.StringResource -import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton import eu.kanade.presentation.browse.components.GlobalSearchCardRow import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem @@ -31,6 +34,7 @@ import tachiyomi.domain.source.model.FeedSavedSearch import tachiyomi.domain.source.model.SavedSearch import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR +import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.topSmallPaddingValues @@ -156,10 +160,21 @@ fun SourceFeedScreen( } }, floatingActionButton = { - BrowseSourceFloatingActionButton( - isVisible = hasFilters, - onFabClick = onFabClick, + // KMK --> + SmallExtendedFloatingActionButton( + text = { + Text( + text = if (hasFilters) { + stringResource(MR.strings.action_filter) + } else { + stringResource(SYMR.strings.saved_searches) + }, + ) + }, + icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") }, + onClick = onFabClick, ) + // KMK <-- }, ) { paddingValues -> Crossfade(targetState = isLoading, label = "source_feed") { state -> diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt deleted file mode 100644 index d5e797974..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt +++ /dev/null @@ -1,34 +0,0 @@ -package eu.kanade.presentation.browse.components - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.FilterList -import androidx.compose.material3.Icon -import androidx.compose.material3.SmallExtendedFloatingActionButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import tachiyomi.presentation.core.i18n.stringResource - -@Composable -fun BrowseSourceFloatingActionButton( - isVisible: Boolean, - onFabClick: () -> Unit, - modifier: Modifier = Modifier, -) { - SmallExtendedFloatingActionButton( - modifier = modifier, - text = { - Text( - text = if (isVisible) { - stringResource(MR.strings.action_filter) - } else { - stringResource(SYMR.strings.saved_searches) - }, - ) - }, - icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") }, - onClick = onFabClick, - ) -} 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 deleted file mode 100644 index 36a6ba14f..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationActionIcon.kt +++ /dev/null @@ -1,95 +0,0 @@ -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.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -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.unit.DpOffset -import androidx.compose.ui.unit.dp -import mihon.feature.migration.list.models.MigratingManga -import tachiyomi.i18n.MR -import tachiyomi.i18n.sy.SYMR -import tachiyomi.presentation.core.i18n.stringResource - -@Composable -fun MigrationActionIcon( - modifier: Modifier, - result: MigratingManga.SearchResult, - skipManga: () -> Unit, - // KMK --> - cancelManga: () -> Unit, - // KMK <-- - searchManually: () -> Unit, - migrateNow: () -> Unit, - copyNow: () -> Unit, -) { - var moreExpanded by remember { mutableStateOf(false) } - val closeMenu = { moreExpanded = false } - - Box(modifier) { - if (result is MigratingManga.SearchResult.Searching) { - // KMK --> - IconButton(onClick = cancelManga) { - // KMK <-- - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = stringResource(SYMR.strings.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(MR.strings.action_menu_overflow_description), - ) - } - DropdownMenu( - expanded = moreExpanded, - onDismissRequest = closeMenu, - offset = DpOffset(8.dp, (-56).dp), - ) { - DropdownMenuItem( - text = { Text(stringResource(SYMR.strings.action_search_manually)) }, - onClick = { - searchManually() - closeMenu() - }, - ) - DropdownMenuItem( - text = { Text(stringResource(SYMR.strings.action_skip_entry)) }, - onClick = { - skipManga() - closeMenu() - }, - ) - if (result is MigratingManga.SearchResult.Result) { - DropdownMenuItem( - text = { Text(stringResource(SYMR.strings.action_migrate_now)) }, - onClick = { - migrateNow() - closeMenu() - }, - ) - DropdownMenuItem( - text = { Text(stringResource(SYMR.strings.action_copy_now)) }, - onClick = { - copyNow() - closeMenu() - }, - ) - } - } - } - } -} 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 deleted file mode 100644 index 0fc75bfbe..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItem.kt +++ /dev/null @@ -1,118 +0,0 @@ -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.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import eu.kanade.presentation.manga.components.MangaCover -import mihon.feature.migration.list.models.MigratingManga -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.domain.manga.model.Manga -import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.Badge -import tachiyomi.presentation.core.components.BadgeGroup -import tachiyomi.presentation.core.i18n.stringResource - -@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(MR.strings.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 deleted file mode 100644 index 7f3617ee4..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/components/MigrationItemResult.kt +++ /dev/null @@ -1,105 +0,0 @@ -package eu.kanade.presentation.browse.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.CircularProgressIndicator -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.layout.ContentScale -import androidx.compose.ui.unit.dp -import eu.kanade.presentation.manga.components.MangaCover -import eu.kanade.presentation.util.rememberResourceBitmapPainter -import eu.kanade.tachiyomi.R -import mihon.feature.migration.list.models.MigratingManga -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.domain.manga.model.Manga -import tachiyomi.i18n.sy.SYMR -import tachiyomi.presentation.core.i18n.stringResource - -@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.height(IntrinsicSize.Min)) { - when (result) { - MigratingManga.SearchResult.Searching -> Box( - modifier = Modifier - .widthIn(max = 150.dp) - .fillMaxSize() - .aspectRatio(MangaCover.Book.ratio), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - MigratingManga.SearchResult.NotFound -> Column( - Modifier - .widthIn(max = 150.dp) - .fillMaxSize() - .padding(top = 4.dp), - ) { - Image( - painter = rememberResourceBitmapPainter(id = R.drawable.cover_error), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(MangaCover.Book.ratio) - .clip(MaterialTheme.shapes.extraSmall), - contentScale = ContentScale.Crop, - ) - Text( - text = stringResource(SYMR.strings.no_alternatives_found), - modifier = Modifier.padding(top = 4.dp, bottom = 1.dp, start = 8.dp), - style = MaterialTheme.typography.titleSmall, - ) - } - 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.fillMaxSize(), - manga = manga, - sourcesString = source, - chapterInfo = chapterInfo, - onClick = { - onMigrationItemClick(manga) - }, - ) - } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index 681686f59..13de72765 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -42,18 +42,18 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() { onChangeSearchFilter = screenModel::setSourceFilter, onToggleResults = screenModel::toggleFilterResults, onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) }, - onClickItem = { manga -> + onClickItem = { // KMK --> if (bulkFavoriteState.selectionMode) { - bulkFavoriteScreenModel.toggleSelection(manga) + bulkFavoriteScreenModel.toggleSelection(it) } else { // KMK <-- // SY --> navigator.items .filterIsInstance() .last() - .newSelectedItem = mangaId to manga.id - navigator.popUntil { it is MigrationListScreen } + .matchOverride = mangaId to it.id + navigator.popUntil { screen -> screen is MigrationListScreen } // SY <-- } }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt index e60a0a502..22e207564 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt @@ -1,12 +1,20 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import androidx.activity.compose.BackHandler +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material3.Icon +import androidx.compose.material3.SmallExtendedFloatingActionButton import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler @@ -15,7 +23,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.presentation.browse.BrowseSourceContent -import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton import eu.kanade.presentation.browse.components.BulkFavoriteDialogs import eu.kanade.presentation.browse.components.bulkSelectionButton import eu.kanade.presentation.components.AppBarActions @@ -34,6 +41,7 @@ import mihon.feature.migration.list.MigrationListScreen import mihon.presentation.core.util.collectAsLazyPagingItems import tachiyomi.core.common.Constants import tachiyomi.domain.manga.model.Manga +import tachiyomi.i18n.MR import tachiyomi.i18n.sy.SYMR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource @@ -121,12 +129,15 @@ data class MigrateSourceSearchScreen( } }, floatingActionButton = { - // SY --> - BrowseSourceFloatingActionButton( - isVisible = state.filters.isNotEmpty(), - onFabClick = screenModel::openFilterSheet, + SmallExtendedFloatingActionButton( + text = { Text(text = stringResource(MR.strings.action_filter)) }, + icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) }, + onClick = screenModel::openFilterSheet, + modifier = Modifier.animateFloatingActionButton( + visible = state.filters.isNotEmpty(), + alignment = Alignment.BottomEnd, + ), ) - // SY <-- }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { paddingValues -> @@ -135,7 +146,7 @@ data class MigrateSourceSearchScreen( navigator.items .filterIsInstance() .last() - .newSelectedItem = currentManga.id to it.id + .matchOverride = currentManga.id to it.id navigator.popUntil { it is MigrationListScreen } // SY <-- } diff --git a/app/src/main/java/exh/smartsearch/SmartLibrarySearchEngine.kt b/app/src/main/java/exh/smartsearch/SmartLibrarySearchEngine.kt index 75e2c4bf3..1da279bf1 100644 --- a/app/src/main/java/exh/smartsearch/SmartLibrarySearchEngine.kt +++ b/app/src/main/java/exh/smartsearch/SmartLibrarySearchEngine.kt @@ -10,7 +10,7 @@ class SmartLibrarySearchEngine( override fun getTitle(result: LibraryManga) = result.manga.ogTitle suspend fun smartSearch(library: List, title: String): LibraryManga? = - smartSearch( + deepSearch( { query -> library.filter { it.manga.ogTitle.contains(query, true) } }, diff --git a/app/src/main/java/exh/ui/smartsearch/SmartSearchScreenModel.kt b/app/src/main/java/exh/ui/smartsearch/SmartSearchScreenModel.kt index 399d85ad6..fbd62af37 100644 --- a/app/src/main/java/exh/ui/smartsearch/SmartSearchScreenModel.kt +++ b/app/src/main/java/exh/ui/smartsearch/SmartSearchScreenModel.kt @@ -26,7 +26,7 @@ class SmartSearchScreenModel( init { screenModelScope.launchIO { val result = try { - val resultManga = smartSearchEngine.smartSearch(source, config.origTitle) + val resultManga = smartSearchEngine.deepSearch(source, config.origTitle) if (resultManga != null) { val localManga = networkToLocalManga(resultManga) SearchResults.Found(localManga) diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index 31ce0657d..2cba2ba7d 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -62,8 +62,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.update import mihon.feature.migration.list.MigrationListScreen -import mihon.feature.migration.list.MigrationProcedureConfig -import mihon.feature.migration.list.MigrationType import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableLazyListState @@ -100,13 +98,14 @@ class MigrationConfigScreen(private val mangaIds: List) : Screen() { val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { ScreenModel() } + val state by screenModel.state.collectAsState() + // KMK --> var searchQuery by remember { mutableStateOf("") } BackHandler(enabled = searchQuery.isNotBlank()) { searchQuery = "" } // KMK <-- - val state by screenModel.state.collectAsState() var migrationSheetOpen by rememberSaveable { mutableStateOf(false) } @@ -119,9 +118,7 @@ class MigrationConfigScreen(private val mangaIds: List) : Screen() { navigator.replace( // KMK --> // MigrateSearchScreen(mangaId) - MigrationListScreen( - MigrationProcedureConfig(MigrationType.MangaList(mangaIds), extraSearchQuery), - ), + MigrationListScreen(mangaIds, extraSearchQuery), // KMK <-- ) } diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt index 0118cc93d..ffe2b10f9 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt @@ -29,48 +29,33 @@ import tachiyomi.i18n.sy.SYMR /** * Screen showing a list of pair of current-target manga entries being migrated. */ -class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() { +class MigrationListScreen(private val mangaIds: List, private val extraSearchQuery: String?) : Screen() { - var newSelectedItem: Pair? = null + var matchOverride: Pair? = null @Composable override fun Content() { - val screenModel = rememberScreenModel { MigrationListScreenModel(config) } + val navigator = LocalNavigator.currentOrThrow + val screenModel = rememberScreenModel { MigrationListScreenModel(mangaIds, extraSearchQuery) } + val context = LocalContext.current val items by screenModel.migratingItems.collectAsState() - val migrationDone by screenModel.migrationDone.collectAsState() + val migrationComplete by screenModel.migrationDone.collectAsState() val finishedCount by screenModel.finishedCount.collectAsState() val dialog by screenModel.dialog.collectAsState() val migrateProgress by screenModel.migratingProgress.collectAsState() - val navigator = LocalNavigator.currentOrThrow - val context = LocalContext.current - LaunchedEffect(items) { - if (items?.isEmpty() == true) { - val manualMigrations = screenModel.manualMigrations.value - context.toast( - context.pluralStringResource( - SYMR.plurals.entry_migrated, - manualMigrations, - manualMigrations, - ), - ) - if (!screenModel.hideNotFound) { - navigator.pop() - } - } - } - LaunchedEffect(newSelectedItem) { - if (newSelectedItem != null) { - val (oldId, newId) = newSelectedItem!! + LaunchedEffect(matchOverride) { + if (matchOverride != null) { + val (oldId, newId) = matchOverride!! screenModel.useMangaForMigration(context, newId, oldId) - newSelectedItem = null + matchOverride = null } } LaunchedEffect(screenModel) { screenModel.navigateOut.collect { if (items.orEmpty().size == 1 && navigator.items.any { it is MangaScreen }) { - val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.id + val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Success)?.id withUIContext { if (mangaId != null) { val newStack = navigator.items.filter { @@ -95,28 +80,44 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen } } } + + LaunchedEffect(items) { + if (items?.isEmpty() == true) { + val manualMigrations = screenModel.manualMigrations.value + context.toast( + context.pluralStringResource( + SYMR.plurals.entry_migrated, + manualMigrations, + manualMigrations, + ), + ) + if (!screenModel.hideUnmatched) { + navigator.pop() + } + } + } MigrationListScreenContent( items = items ?: persistentListOf(), - migrationDone = migrationDone, + migrationComplete = migrationComplete, finishedCount = finishedCount, getManga = screenModel::getManga, getChapterInfo = screenModel::getChapterInfo, getSourceName = screenModel::getSourceName, - onMigrationItemClick = { + onItemClick = { navigator.push(MangaScreen(it.id, true)) }, - openMigrationDialog = screenModel::openMigrateDialog, - skipManga = { screenModel.removeManga(it) }, - // KMK --> - cancelManga = { screenModel.cancelManga(it) }, - navigateUp = { navigator.pop() }, - openMigrationOptionsDialog = screenModel::openMigrationOptionsDialog, - // KMK <-- - searchManually = { migrationItem -> + onSearchManually = { migrationItem -> navigator push MigrateSearchScreen(migrationItem.manga.id) }, - migrateNow = { screenModel.migrateManga(it, true) }, - copyNow = { screenModel.migrateManga(it, false) }, + onSkip = { screenModel.removeManga(it) }, + onMigrate = { screenModel.migrateNow(it, true) }, + onCopy = { screenModel.migrateNow(it, false) }, + openMigrationDialog = screenModel::showMigrateDialog, + // KMK --> + onCancel = { screenModel.cancelManga(it) }, + navigateUp = { navigator.pop() }, + openOptionsDialog = screenModel::openOptionsDialog, + // KMK <-- ) val onDismissRequest = { screenModel.dialog.value = null } @@ -124,24 +125,24 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen @Suppress("NAME_SHADOWING") val dialog = dialog ) { - is MigrationListScreenModel.Dialog.MigrateMangaDialog -> { + is MigrationListScreenModel.Dialog.Migrate -> { MigrationMangaDialog( onDismissRequest = onDismissRequest, copy = dialog.copy, - mangaSet = dialog.mangaSet, - mangaSkipped = dialog.mangaSkipped, + totalCount = dialog.totalCount, + skippedCount = dialog.skippedCount, copyManga = screenModel::copyMangas, - migrateManga = screenModel::migrateMangas, + onMigrate = screenModel::migrateMangas, ) } - MigrationListScreenModel.Dialog.MigrationExitDialog -> { + MigrationListScreenModel.Dialog.Exit -> { MigrationExitDialog( onDismissRequest = onDismissRequest, exitMigration = navigator::pop, ) } // KMK --> - MigrationListScreenModel.Dialog.MigrationOptionsDialog -> { + MigrationListScreenModel.Dialog.Options -> { MigrationConfigScreenSheet( preferences = screenModel.preferences, onDismissRequest = onDismissRequest, @@ -164,7 +165,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen } BackHandler(true) { - screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog + screenModel.dialog.value = MigrationListScreenModel.Dialog.Exit } } } diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenContent.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenContent.kt index e5f97b3a5..fea93c92c 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenContent.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenContent.kt @@ -1,33 +1,63 @@ package mihon.feature.migration.list +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.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.automirrored.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.material.icons.outlined.Settings +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +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.text.style.TextOverflow +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import eu.kanade.presentation.browse.components.MigrationActionIcon -import eu.kanade.presentation.browse.components.MigrationItem -import eu.kanade.presentation.browse.components.MigrationItemResult +import androidx.compose.ui.unit.sp import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.manga.components.MangaCover +import eu.kanade.presentation.util.animateItemFastScroll +import eu.kanade.presentation.util.rememberResourceBitmapPainter +import eu.kanade.tachiyomi.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import mihon.feature.migration.list.models.MigratingManga @@ -35,7 +65,9 @@ import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.i18n.sy.SYMR -import tachiyomi.presentation.core.components.ScrollbarLazyColumn +import tachiyomi.presentation.core.components.Badge +import tachiyomi.presentation.core.components.BadgeGroup +import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.topSmallPaddingValues import tachiyomi.presentation.core.i18n.stringResource @@ -44,22 +76,22 @@ import tachiyomi.presentation.core.util.plus @Composable fun MigrationListScreenContent( items: ImmutableList, - migrationDone: Boolean, + migrationComplete: Boolean, finishedCount: Int, - getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?, - getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo, + getManga: suspend (MigratingManga.SearchResult.Success) -> Manga?, + getChapterInfo: suspend (MigratingManga.SearchResult.Success) -> MigratingManga.ChapterInfo, getSourceName: (Manga) -> String, - onMigrationItemClick: (Manga) -> Unit, + onItemClick: (Manga) -> Unit, + onSearchManually: (MigratingManga) -> Unit, + onSkip: (Long) -> Unit, + onMigrate: (Long) -> Unit, + onCopy: (Long) -> Unit, openMigrationDialog: (Boolean) -> Unit, - skipManga: (Long) -> Unit, // KMK --> - cancelManga: (Long) -> Unit, + onCancel: (Long) -> Unit, navigateUp: () -> Unit, - openMigrationOptionsDialog: () -> Unit, + openOptionsDialog: () -> Unit, // KMK <-- - searchManually: (MigratingManga) -> Unit, - migrateNow: (Long) -> Unit, - copyNow: (Long) -> Unit, ) { Scaffold( topBar = { scrollBehavior -> @@ -81,20 +113,20 @@ fun MigrationListScreenContent( AppBar.Action( title = stringResource(MR.strings.action_settings), icon = Icons.Outlined.Settings, - onClick = openMigrationOptionsDialog, + onClick = openOptionsDialog, ), // KMK <-- AppBar.Action( title = stringResource(MR.strings.copy), icon = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll, onClick = { openMigrationDialog(true) }, - enabled = migrationDone, + enabled = migrationComplete, ), AppBar.Action( title = stringResource(MR.strings.migrate), icon = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll, onClick = { openMigrationDialog(false) }, - enabled = migrationDone, + enabled = migrationComplete, ), ), ) @@ -103,30 +135,27 @@ fun MigrationListScreenContent( ) }, ) { contentPadding -> - ScrollbarLazyColumn( - contentPadding = contentPadding + topSmallPaddingValues, - ) { - items(items, key = { "migration-list-${it.manga.id}" }) { migrationItem -> + FastScrollLazyColumn(contentPadding = contentPadding + topSmallPaddingValues) { + items(items, key = { "migration-list-${it.manga.id}" }) { item -> Row( Modifier .fillMaxWidth() - .animateItem() + .animateItemFastScroll() .padding(horizontal = 16.dp) .height(IntrinsicSize.Min), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - val result by migrationItem.searchResult.collectAsState() - MigrationItem( + MigrationListItem( modifier = Modifier .padding(top = 8.dp) .weight(1f) .align(Alignment.Top) .fillMaxHeight(), - manga = migrationItem.manga, - sourcesString = migrationItem.sourcesString, - chapterInfo = migrationItem.chapterInfo, - onClick = { onMigrationItemClick(migrationItem.manga) }, + manga = item.manga, + source = item.source, + chapterInfo = item.chapterInfo, + onClick = { onItemClick(item.manga) }, ) Icon( @@ -135,34 +164,190 @@ fun MigrationListScreenContent( modifier = Modifier.weight(0.2f), ) - MigrationItemResult( + val result by item.searchResult.collectAsState() + MigrationListItemResult( modifier = Modifier .padding(top = 8.dp) .weight(1f) .align(Alignment.Top) .fillMaxHeight(), - migrationItem = migrationItem, + migrationItem = item, result = result, getManga = getManga, getChapterInfo = getChapterInfo, getSourceName = getSourceName, - onMigrationItemClick = onMigrationItemClick, + onItemClick = onItemClick, ) - MigrationActionIcon( + MigrationListItemAction( modifier = Modifier .weight(0.2f), result = result, - skipManga = { skipManga(migrationItem.manga.id) }, + onSearchManually = { onSearchManually(item) }, + onSkip = { onSkip(item.manga.id) }, + onMigrate = { onMigrate(item.manga.id) }, + onCopy = { onCopy(item.manga.id) }, // KMK --> - cancelManga = { cancelManga(migrationItem.manga.id) }, + onCancel = { onCancel(item.manga.id) }, // KMK <-- - searchManually = { searchManually(migrationItem) }, - migrateNow = { - migrateNow(migrationItem.manga.id) - }, - copyNow = { - copyNow(migrationItem.manga.id) + ) + } + } + } + } +} + +@Composable +fun MigrationListItem( + modifier: Modifier, + manga: Manga, + source: 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.Companion.fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio), + ) { + MangaCover.Book( + modifier = Modifier.Companion + .fillMaxWidth(), + data = manga, + ) + Box( + modifier = Modifier.Companion + .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) + .background( + Brush.Companion.verticalGradient( + 0f to Color.Companion.Transparent, + 1f to Color(0xAA000000), + ), + ) + .fillMaxHeight(0.33f) + .fillMaxWidth() + .align(Alignment.Companion.BottomCenter), + ) + Text( + modifier = Modifier.Companion + .padding(8.dp) + .align(Alignment.Companion.BottomStart), + text = manga.title.ifBlank { stringResource(MR.strings.unknown) }, + fontSize = 12.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Companion.Ellipsis, + style = MaterialTheme.typography.titleSmall.copy( + color = Color.Companion.White, + shadow = Shadow( + color = Color.Companion.Black, + blurRadius = 4f, + ), + ), + ) + BadgeGroup(modifier = Modifier.Companion.padding(4.dp)) { + Badge(text = "${chapterInfo.chapterCount}") + } + } + Text( + text = source, + modifier = Modifier.Companion.padding(top = 4.dp, bottom = 1.dp, start = 8.dp), + overflow = TextOverflow.Companion.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.titleSmall, + ) + + val formattedLatestChapter by produceState(initialValue = "") { + value = withIOContext { + chapterInfo.getFormattedLatestChapter(context) + } + } + Text( + text = formattedLatestChapter, + modifier = Modifier.Companion.padding(top = 1.dp, bottom = 4.dp, start = 8.dp), + overflow = TextOverflow.Companion.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +@Composable +fun MigrationListItemResult( + modifier: Modifier, + migrationItem: MigratingManga, + result: MigratingManga.SearchResult, + getManga: suspend (MigratingManga.SearchResult.Success) -> Manga?, + getChapterInfo: suspend (MigratingManga.SearchResult.Success) -> MigratingManga.ChapterInfo, + getSourceName: (Manga) -> String, + onItemClick: (Manga) -> Unit, +) { + Box(modifier.height(IntrinsicSize.Min)) { + when (result) { + MigratingManga.SearchResult.Searching -> Box( + modifier = Modifier.Companion + .widthIn(max = 150.dp) + .fillMaxSize() + .aspectRatio(MangaCover.Book.ratio), + contentAlignment = Alignment.Companion.Center, + ) { + CircularProgressIndicator() + } + + MigratingManga.SearchResult.NotFound -> Column( + Modifier.Companion + .widthIn(max = 150.dp) + .fillMaxSize() + .padding(top = 4.dp), + ) { + Image( + painter = rememberResourceBitmapPainter(id = R.drawable.cover_error), + contentDescription = null, + modifier = Modifier.Companion + .fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio) + .clip(MaterialTheme.shapes.extraSmall), + contentScale = ContentScale.Companion.Crop, + ) + Text( + text = stringResource(SYMR.strings.no_alternatives_found), + modifier = Modifier.Companion.padding(top = 4.dp, bottom = 1.dp, start = 8.dp), + style = MaterialTheme.typography.titleSmall, + ) + } + + is MigratingManga.SearchResult.Success -> { + 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!! + MigrationListItem( + modifier = Modifier.Companion.fillMaxSize(), + manga = manga, + source = source, + chapterInfo = chapterInfo, + onClick = { + onItemClick(manga) }, ) } @@ -170,3 +355,77 @@ fun MigrationListScreenContent( } } } + +@Composable +fun MigrationListItemAction( + modifier: Modifier, + result: MigratingManga.SearchResult, + onSearchManually: () -> Unit, + onSkip: () -> Unit, + onMigrate: () -> Unit, + onCopy: () -> Unit, + // KMK --> + onCancel: () -> Unit, + // KMK <-- +) { + var moreExpanded by remember { mutableStateOf(false) } + val closeMenu = { moreExpanded = false } + Box(modifier) { + when (result) { + MigratingManga.SearchResult.Searching -> { + // KMK --> + IconButton(onClick = onCancel) { + // KMK <-- + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(SYMR.strings.action_stop), + ) + } + } + MigratingManga.SearchResult.NotFound, is MigratingManga.SearchResult.Success -> { + IconButton(onClick = { moreExpanded = !moreExpanded }) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(MR.strings.action_menu_overflow_description), + ) + } + DropdownMenu( + expanded = moreExpanded, + onDismissRequest = closeMenu, + offset = DpOffset(8.dp, (-56).dp), + ) { + DropdownMenuItem( + text = { Text(stringResource(SYMR.strings.action_search_manually)) }, + onClick = { + onSearchManually() + closeMenu() + }, + ) + DropdownMenuItem( + text = { Text(stringResource(SYMR.strings.action_skip_entry)) }, + onClick = { + onSkip() + closeMenu() + }, + ) + if (result is MigratingManga.SearchResult.Success) { + DropdownMenuItem( + text = { Text(stringResource(SYMR.strings.action_migrate_now)) }, + onClick = { + onMigrate() + closeMenu() + }, + ) + DropdownMenuItem( + text = { Text(stringResource(SYMR.strings.action_copy_now)) }, + onClick = { + onCopy() + closeMenu() + }, + ) + } + } + } + } + } +} diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt index 3dccd9cce..79041c488 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt @@ -49,7 +49,8 @@ import uy.kohesive.injekt.api.get import java.util.concurrent.atomic.AtomicInteger class MigrationListScreenModel( - private val config: MigrationProcedureConfig, + private var mangaIds: List, + extraSearchQuery: String?, val preferences: SourcePreferences = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), private val getManga: GetManga = Injekt.get(), @@ -57,12 +58,16 @@ class MigrationListScreenModel( private val updateManga: UpdateManga = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), - private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), private val migrateManga: MigrateMangaUseCase = Injekt.get(), + // SY --> + private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(), + // SY <-- ) : ScreenModel { - private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams) + private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery) + // SY --> private val throttleManager = ThrottleManager() + // SY <-- val migratingItems = MutableStateFlow?>(null) val migrationDone = MutableStateFlow(false) @@ -70,10 +75,10 @@ class MigrationListScreenModel( val manualMigrations = MutableStateFlow(0) - var hideNotFound = preferences.migrationHideUnmatched().get() - private var showOnlyUpdates = preferences.migrationHideWithoutUpdates().get() - private var useSourceWithMost = preferences.migrationPrioritizeByChapters().get() - private var useSmartSearch = preferences.migrationDeepSearchMode().get() + var hideUnmatched = preferences.migrationHideUnmatched().get() + private var hideWithoutUpdates = preferences.migrationHideWithoutUpdates().get() + private var prioritizeByChapters = preferences.migrationPrioritizeByChapters().get() + private var deepSearchMode = preferences.migrationDeepSearchMode().get() val navigateOut = MutableSharedFlow() @@ -85,12 +90,6 @@ class MigrationListScreenModel( init { screenModelScope.launchIO { - val mangaIds = when (val migration = config.migration) { - is MigrationType.MangaList -> { - migration.mangaIds - } - is MigrationType.MangaSingle -> listOf(migration.fromMangaId) - } runMigrations( mangaIds .map { @@ -99,7 +98,7 @@ class MigrationListScreenModel( MigratingManga( manga = manga, chapterInfo = getChapterInfo(it), - sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo( + source = sourceManager.getOrStub(manga.source).getNameForMangaInfo( if (manga.source == MERGED_SOURCE_ID) { getMergedReferencesById.await(manga.id) .map { sourceManager.getOrStub(it.mangaSourceId) } @@ -120,9 +119,9 @@ class MigrationListScreenModel( } } - suspend fun getManga(result: SearchResult.Result) = getManga(result.id) + suspend fun getManga(result: SearchResult.Success) = getManga(result.id) suspend fun getManga(id: Long) = getManga.await(id) - suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id) + suspend fun getChapterInfo(result: SearchResult.Success) = getChapterInfo(result.id) private suspend fun getChapterInfo(id: Long) = getChaptersByMangaId.await(id).let { chapters -> MigratingManga.ChapterInfo( latestChapter = chapters.maxOfOrNull { it.chapterNumber }, @@ -145,11 +144,8 @@ class MigrationListScreenModel( break } // in case it was removed - when (val migration = config.migration) { - is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) { - continue - } - else -> Unit + if (manga.manga.id !in mangaIds) { + continue } if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) { @@ -165,107 +161,27 @@ class MigrationListScreenModel( } else { sources.filter { it.id != mangaSource.id } } - when (val migration = config.migration) { - is MigrationType.MangaSingle -> if (migration.toManga != null) { - val localManga = getManga.await(migration.toManga) - if (localManga != null) { - val source = sourceManager.get(localManga.source) as? CatalogueSource - if (source != null) { - try { - val chapters = if (source is EHentai) { - source.getChapterList(localManga.toSManga(), throttleManager::throttle) - } else { - source.getChapterList(localManga.toSManga()) - } - syncChaptersWithSource.await(chapters, localManga, source) - } catch (_: Exception) { - } - manga.progress.value = validSources.size to validSources.size - return@async localManga - } - } - } - else -> Unit - } - if (useSourceWithMost) { + if (prioritizeByChapters) { val sourceSemaphore = Semaphore(3) val processedSources = AtomicInteger() validSources.map { source -> async async2@{ sourceSemaphore.withPermit { - try { - val searchResult = if (useSmartSearch) { - smartSearchEngine.smartSearch(source, mangaObj.ogTitle) - } else { - smartSearchEngine.normalSearch(source, mangaObj.ogTitle) - } - - if (searchResult != null && - !(searchResult.url == mangaObj.url && source.id == mangaObj.source) - ) { - val localManga = networkToLocalManga(searchResult) - - val chapters = if (source is EHentai) { - source.getChapterList(localManga.toSManga(), throttleManager::throttle) - } else { - source.getChapterList(localManga.toSManga()) - } - - try { - syncChaptersWithSource.await(chapters, localManga, source) - } catch (_: Exception) { - return@async2 null - } - manga.progress.value = - validSources.size to processedSources.incrementAndGet() - localManga to chapters.size - } else { - null - } - } catch (e: CancellationException) { - // Ignore cancellations - throw e - } catch (_: Exception) { - null + val result = searchSource(manga.manga, source, deepSearchMode) + if (result != null) { + manga.progress.value = + validSources.size to processedSources.incrementAndGet() } + result } } }.mapNotNull { it.await() }.maxByOrNull { it.second }?.first } else { validSources.forEachIndexed { index, source -> - val searchResult = try { - val searchResult = if (useSmartSearch) { - smartSearchEngine.smartSearch(source, mangaObj.ogTitle) - } else { - smartSearchEngine.normalSearch(source, mangaObj.ogTitle) - } - - if (searchResult != null) { - val localManga = networkToLocalManga(searchResult) - val chapters = try { - if (source is EHentai) { - source.getChapterList(localManga.toSManga(), throttleManager::throttle) - } else { - source.getChapterList(localManga.toSManga()) - } - } catch (e: Exception) { - this@MigrationListScreenModel.logcat(LogPriority.ERROR, e) - emptyList() - } - syncChaptersWithSource.await(chapters, localManga, source) - localManga - } else { - null - } - } catch (e: CancellationException) { - // Ignore cancellations - throw e - } catch (_: Exception) { - null - } + val result = searchSource(manga.manga, source, deepSearchMode) manga.progress.value = validSources.size to (index + 1) - if (searchResult != null) return@async searchResult + if (result != null) return@async result.first } null @@ -294,37 +210,82 @@ class MigrationListScreenModel( manga.searchResult.value = if (result == null) { SearchResult.NotFound } else { - SearchResult.Result(result.id) + SearchResult.Success(result.id) } - if (result == null && hideNotFound) { + if (result == null && hideUnmatched) { removeManga(manga) } if (result != null && - showOnlyUpdates && + hideWithoutUpdates && (getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0) ) { removeManga(manga) } - sourceFinished() + updateMigrationProgress() } } } - private suspend fun sourceFinished() { - finishedCount.value = migratingItems.value.orEmpty().count { - it.searchResult.value != SearchResult.Searching - } - if (allMangasDone()) { - migrationDone.value = true - } - if (migratingItems.value?.isEmpty() == true) { - navigateOut() + private suspend fun searchSource( + manga: Manga, + source: CatalogueSource, + deepSearchMode: Boolean, + ): Pair? { + return try { + val searchResult = if (deepSearchMode) { + smartSearchEngine.deepSearch(source, manga.ogTitle) + } else { + smartSearchEngine.regularSearch(source, manga.ogTitle) + } + + if (searchResult != null && + !(searchResult.url == manga.url && source.id == manga.source) + ) { + val localManga = networkToLocalManga(searchResult) + + val chapters = try { + if (source is EHentai) { + source.getChapterList(localManga.toSManga(), throttleManager::throttle) + } else { + source.getChapterList(localManga.toSManga()) + } + } catch (e: Exception) { + this@MigrationListScreenModel.logcat(LogPriority.ERROR, e) + return null + } + + try { + syncChaptersWithSource.await(chapters, localManga, source) + } catch (_: Exception) { + return null + } + localManga to chapters.size + } else { + null + } + } catch (e: CancellationException) { + // Ignore cancellations + throw e + } catch (_: Exception) { + null } } - private fun allMangasDone() = migratingItems.value.orEmpty().all { it.searchResult.value != SearchResult.Searching } && - migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Result } + private suspend fun updateMigrationProgress() { + finishedCount.value = migratingItems.value.orEmpty().count { + it.searchResult.value != SearchResult.Searching + } + if (migrationComplete()) { + migrationDone.value = true + } + if (migratingItems.value?.isEmpty() == true) { + navigateBack() + } + } + + private fun migrationComplete() = migratingItems.value.orEmpty().all { it.searchResult.value != SearchResult.Searching } && + migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Success } private fun mangasSkipped() = migratingItems.value.orEmpty().count { it.searchResult.value == SearchResult.NotFound } @@ -357,7 +318,7 @@ class MigrationListScreenModel( Timber.tag("MigrationListScreenModel").e(e, "Error updating manga from source") } - migratingManga.searchResult.value = SearchResult.Result(result.id) + migratingManga.searchResult.value = SearchResult.Success(result.id) } else { migratingManga.searchResult.value = SearchResult.NotFound withUIContext { @@ -366,7 +327,7 @@ class MigrationListScreenModel( } // KMK --> - sourceFinished() + updateMigrationProgress() // KMK <-- } } @@ -385,18 +346,18 @@ class MigrationListScreenModel( migratingProgress.value = 0f val items = migratingItems.value.orEmpty() try { - items.forEachIndexed { index, current -> + items.forEachIndexed { index, manga -> try { ensureActive() - val target = current.searchResult.value.let { - if (it is SearchResult.Result) { + val target = manga.searchResult.value.let { + if (it is SearchResult.Success) { getManga.await(it.id) } else { null } } if (target != null) { - migrateManga(current.manga, target, replace) + migrateManga(manga.manga, target, replace) } } catch (e: Exception) { if (e is CancellationException) throw e @@ -405,7 +366,7 @@ class MigrationListScreenModel( migratingProgress.value = index.toFloat() / items.size } - navigateOut() + navigateBack() } finally { migratingProgress.value = Float.MAX_VALUE migrateJob = null @@ -418,20 +379,20 @@ class MigrationListScreenModel( migrateJob = null } - private suspend fun navigateOut() { + private suspend fun navigateBack() { navigateOut.emit(Unit) } - fun migrateManga(mangaId: Long, replace: Boolean) { + fun migrateNow(mangaId: Long, replace: Boolean) { manualMigrations.value++ screenModelScope.launchIO { - val current = migratingItems.value.orEmpty().find { it.manga.id == mangaId } + val manga = migratingItems.value.orEmpty().find { it.manga.id == mangaId } ?: return@launchIO - val target = getManga.await((current.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO) + val target = getManga.await((manga.searchResult.value as? SearchResult.Success)?.id ?: return@launchIO) ?: return@launchIO - migrateManga(current.manga, target, replace) + migrateManga(manga.manga, target, replace) removeManga(mangaId) } } @@ -445,7 +406,7 @@ class MigrationListScreenModel( item.searchingJob?.cancel() item.searchingJob = null item.searchResult.value = SearchResult.NotFound - sourceFinished() + updateMigrationProgress() } } // KMK <-- @@ -456,23 +417,18 @@ class MigrationListScreenModel( ?: return@launchIO removeManga(item) item.migrationScope.cancel() - sourceFinished() + updateMigrationProgress() } } private fun removeManga(item: MigratingManga) { - when (val migration = config.migration) { - is MigrationType.MangaList -> { - val ids = migration.mangaIds.toMutableList() - val index = ids.indexOf(item.manga.id) - if (index > -1) { - ids.removeAt(index) - config.migration = MigrationType.MangaList(ids) - val index2 = migratingItems.value.orEmpty().indexOf(item) - if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList() - } - } - is MigrationType.MangaSingle -> Unit + val ids = mangaIds.toMutableList() + val index = ids.indexOf(item.manga.id) + if (index > -1) { + ids.removeAt(index) + mangaIds = ids + val index2 = migratingItems.value.orEmpty().indexOf(item) + if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList() } } @@ -483,10 +439,8 @@ class MigrationListScreenModel( } } - fun openMigrateDialog( - copy: Boolean, - ) { - dialog.value = Dialog.MigrateMangaDialog( + fun showMigrateDialog(copy: Boolean) { + dialog.value = Dialog.Migrate( copy, migratingItems.value.orEmpty().size, mangasSkipped(), @@ -494,24 +448,24 @@ class MigrationListScreenModel( } // KMK --> - fun openMigrationOptionsDialog() { - dialog.value = Dialog.MigrationOptionsDialog + fun openOptionsDialog() { + dialog.value = Dialog.Options } fun updateOptions() { - hideNotFound = preferences.migrationHideUnmatched().get() - showOnlyUpdates = preferences.migrationHideWithoutUpdates().get() - useSourceWithMost = preferences.migrationPrioritizeByChapters().get() - useSmartSearch = preferences.migrationDeepSearchMode().get() + hideUnmatched = preferences.migrationHideUnmatched().get() + hideWithoutUpdates = preferences.migrationHideWithoutUpdates().get() + prioritizeByChapters = preferences.migrationPrioritizeByChapters().get() + deepSearchMode = preferences.migrationDeepSearchMode().get() } // KMK <-- sealed class Dialog { - data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog() - data object MigrationExitDialog : Dialog() + data class Migrate(val copy: Boolean, val totalCount: Int, val skippedCount: Int) : Dialog() + data object Exit : Dialog() // KMK --> - data object MigrationOptionsDialog : Dialog() + data object Options : Dialog() // KMK <-- } } diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationProcedureConfig.kt b/app/src/main/java/mihon/feature/migration/list/MigrationProcedureConfig.kt deleted file mode 100644 index 0872b30b6..000000000 --- a/app/src/main/java/mihon/feature/migration/list/MigrationProcedureConfig.kt +++ /dev/null @@ -1,13 +0,0 @@ -package mihon.feature.migration.list - -import java.io.Serializable - -sealed class MigrationType : Serializable { - data class MangaList(val mangaIds: List) : MigrationType() - data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType() -} - -data class MigrationProcedureConfig( - var migration: MigrationType, - val extraSearchParams: String?, -) : Serializable diff --git a/app/src/main/java/mihon/feature/migration/list/components/MigrationExitDialog.kt b/app/src/main/java/mihon/feature/migration/list/components/MigrationExitDialog.kt index d588a403f..cc34ce311 100644 --- a/app/src/main/java/mihon/feature/migration/list/components/MigrationExitDialog.kt +++ b/app/src/main/java/mihon/feature/migration/list/components/MigrationExitDialog.kt @@ -15,6 +15,9 @@ fun MigrationExitDialog( ) { AlertDialog( onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(SYMR.strings.stop_migrating)) + }, confirmButton = { TextButton(onClick = exitMigration) { Text(text = stringResource(SYMR.strings.action_stop)) @@ -25,8 +28,5 @@ fun MigrationExitDialog( Text(text = stringResource(MR.strings.action_cancel)) } }, - title = { - Text(text = stringResource(SYMR.strings.stop_migrating)) - }, ) } diff --git a/app/src/main/java/mihon/feature/migration/list/components/MigrationMangaDialog.kt b/app/src/main/java/mihon/feature/migration/list/components/MigrationMangaDialog.kt index d346d98d8..4601cacff 100644 --- a/app/src/main/java/mihon/feature/migration/list/components/MigrationMangaDialog.kt +++ b/app/src/main/java/mihon/feature/migration/list/components/MigrationMangaDialog.kt @@ -13,20 +13,30 @@ import tachiyomi.presentation.core.i18n.stringResource fun MigrationMangaDialog( onDismissRequest: () -> Unit, copy: Boolean, - mangaSet: Int, - mangaSkipped: Int, + totalCount: Int, + skippedCount: Int, copyManga: () -> Unit, - migrateManga: () -> Unit, + onMigrate: () -> Unit, ) { AlertDialog( onDismissRequest = onDismissRequest, + text = { + Text( + text = pluralStringResource( + if (copy) SYMR.plurals.copy_entry else SYMR.plurals.migrate_entry, + count = totalCount, + totalCount, + (if (skippedCount > 0) " " + stringResource(SYMR.strings.skipping_, skippedCount) else ""), + ), + ) + }, confirmButton = { TextButton( onClick = { if (copy) { copyManga() } else { - migrateManga() + onMigrate() } }, ) { @@ -38,15 +48,5 @@ fun MigrationMangaDialog( Text(text = stringResource(MR.strings.action_cancel)) } }, - text = { - Text( - text = pluralStringResource( - if (copy) SYMR.plurals.copy_entry else SYMR.plurals.migrate_entry, - count = mangaSet, - mangaSet, - (if (mangaSkipped > 0) " " + stringResource(SYMR.strings.skipping_, mangaSkipped) else ""), - ), - ) - }, ) } diff --git a/app/src/main/java/mihon/feature/migration/list/models/MigratingManga.kt b/app/src/main/java/mihon/feature/migration/list/models/MigratingManga.kt index 9d38ac7e7..5bfd6cc5a 100644 --- a/app/src/main/java/mihon/feature/migration/list/models/MigratingManga.kt +++ b/app/src/main/java/mihon/feature/migration/list/models/MigratingManga.kt @@ -16,7 +16,7 @@ import kotlin.coroutines.CoroutineContext class MigratingManga( val manga: Manga, val chapterInfo: ChapterInfo, - val sourcesString: String, + val source: String, parentContext: CoroutineContext, ) { val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default) @@ -33,7 +33,7 @@ class MigratingManga( sealed class SearchResult { data object Searching : SearchResult() data object NotFound : SearchResult() - data class Result(val id: Long) : SearchResult() + data class Success(val id: Long) : SearchResult() } data class ChapterInfo( diff --git a/app/src/main/java/mihon/feature/migration/list/search/BaseSmartSearchEngine.kt b/app/src/main/java/mihon/feature/migration/list/search/BaseSmartSearchEngine.kt index 9f5fc4dc8..cedfd01d2 100644 --- a/app/src/main/java/mihon/feature/migration/list/search/BaseSmartSearchEngine.kt +++ b/app/src/main/java/mihon/feature/migration/list/search/BaseSmartSearchEngine.kt @@ -17,10 +17,10 @@ abstract class BaseSmartSearchEngine( protected abstract fun getTitle(result: T): String - protected suspend fun smartSearch(searchAction: SearchAction, title: String): T? { + protected suspend fun deepSearch(searchAction: SearchAction, title: String): T? { val cleanedTitle = cleanSmartSearchTitle(title) - val queries = getSmartSearchQueries(cleanedTitle) + val queries = getDeepSearchQueries(cleanedTitle) val eligibleManga = supervisorScope { queries.map { query -> @@ -42,10 +42,10 @@ abstract class BaseSmartSearchEngine( }.flatMap { it.await() } } - return eligibleManga.maxByOrNull { it.dist }?.manga + return eligibleManga.maxByOrNull { it.distance }?.entry } - protected suspend fun normalSearch(searchAction: SearchAction, title: String): T? { + protected suspend fun regularSearch(searchAction: SearchAction, title: String): T? { val eligibleManga = supervisorScope { val searchQuery = if (extraSearchParams != null) { "$title ${extraSearchParams.trim()}" @@ -66,7 +66,7 @@ abstract class BaseSmartSearchEngine( } } - return eligibleManga.maxByOrNull { it.dist }?.manga + return eligibleManga.maxByOrNull { it.distance }?.entry } private fun cleanSmartSearchTitle(title: String): String { @@ -142,7 +142,7 @@ abstract class BaseSmartSearchEngine( return result.toString() } - private fun getSmartSearchQueries(cleanedTitle: String): List { + private fun getDeepSearchQueries(cleanedTitle: String): List { val splitCleanedTitle = cleanedTitle.split(" ") val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length } @@ -155,7 +155,6 @@ abstract class BaseSmartSearchEngine( // Search largest word // Search first two words // Search first word - val searchQueries = listOf( listOf(cleanedTitle), splitSortedByLargest.take(2), @@ -179,4 +178,4 @@ abstract class BaseSmartSearchEngine( } } -data class SearchEntry(val manga: T, val dist: Double) +data class SearchEntry(val entry: T, val distance: Double) diff --git a/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt b/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt index f1410eb7f..e0207bbd2 100644 --- a/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt +++ b/app/src/main/java/mihon/feature/migration/list/search/SmartSourceSearchEngine.kt @@ -11,13 +11,13 @@ class SmartSourceSearchEngine( override fun getTitle(result: SManga) = result.originalTitle - suspend fun smartSearch(source: CatalogueSource, title: String): Manga? = - smartSearch(makeSearchAction(source), title).let { + suspend fun regularSearch(source: CatalogueSource, title: String): Manga? = + regularSearch(makeSearchAction(source), title).let { it?.toDomainManga(source.id) } - suspend fun normalSearch(source: CatalogueSource, title: String): Manga? = - normalSearch(makeSearchAction(source), title).let { + suspend fun deepSearch(source: CatalogueSource, title: String): Manga? = + deepSearch(makeSearchAction(source), title).let { it?.toDomainManga(source.id) }