feat(migration): Allow skipping smart search for single entry migration (#1496)

* feat(migration): Allow skipping smart search for single entry migration

Introduces a "Smart search" setting when migrating a single manga, allowing users to choose between an automated best-match search or manual selection directly.

* ensure manual migration is only triggered once

* Add UI toggle

* refactor pref
This commit is contained in:
Cuong-Tran 2026-02-20 08:49:51 +07:00 committed by GitHub
parent 36c1c642ca
commit fe7b10f941
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 70 additions and 15 deletions

View file

@ -87,6 +87,8 @@ class SourcePreferences(
fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false) fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false)
// KMK --> // KMK -->
fun migrationSmartSearchSingleEntry() = preferenceStore.getBoolean("migration_smart_search_single_entry", false)
fun globalSearchPinnedState() = preferenceStore.getEnum( fun globalSearchPinnedState() = preferenceStore.getEnum(
Preference.appStateKey("global_search_pinned_toggle_state"), Preference.appStateKey("global_search_pinned_toggle_state"),
SourceFilter.PinnedOnly, SourceFilter.PinnedOnly,

View file

@ -121,7 +121,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
return return
} }
val screen = // KMK --> if (mangaId == null) { val screen = // KMK --> if (mangaId == null) {
MigrationListScreen(mangaIds, extraSearchQuery) MigrationListScreen(mangaIds, extraSearchQuery, screenModel.sourcePreferences.migrationSmartSearchSingleEntry().get())
// KMK --> // KMK -->
// } else { // } else {
// MigrateSearchScreen(mangaId) // MigrateSearchScreen(mangaId)
@ -297,6 +297,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
migrationSheetOpen = false migrationSheetOpen = false
continueMigration(openSheet = false, extraSearchQuery = extraSearchQuery) continueMigration(openSheet = false, extraSearchQuery = extraSearchQuery)
}, },
// KMK -->
isSingleEntry = mangaIds.size == 1,
// KMK <--
) )
} }
} }

View file

@ -39,6 +39,7 @@ import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.Button import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -53,6 +54,7 @@ fun MigrationConfigScreenSheet(
onStartMigration: (extraSearchQuery: String?) -> Unit, onStartMigration: (extraSearchQuery: String?) -> Unit,
// KMK --> // KMK -->
fullSettings: Boolean = true, fullSettings: Boolean = true,
isSingleEntry: Boolean = false,
// KMK <-- // KMK <--
) { ) {
var extraSearchQuery by rememberSaveable { mutableStateOf("") } var extraSearchQuery by rememberSaveable { mutableStateOf("") }
@ -160,17 +162,30 @@ fun MigrationConfigScreenSheet(
if (fullSettings) { if (fullSettings) {
// KMK <-- // KMK <--
MigrationSheetDividerItem() MigrationSheetDividerItem()
MigrationSheetWarningItem(stringResource(MR.strings.migrationConfigScreen_enhancedOptionsWarning)) // KMK -->
MigrationSheetSwitchItem( val migrationSmartSearchSingleEntryPref = preferences.migrationSmartSearchSingleEntry()
title = stringResource(MR.strings.migrationConfigScreen_deepSearchModeTitle), val isSmartSearchSingleEntry by migrationSmartSearchSingleEntryPref.collectAsState()
subtitle = stringResource(MR.strings.migrationConfigScreen_deepSearchModeSubtitle), if (isSingleEntry) {
preference = preferences.migrationDeepSearchMode(), MigrationSheetSwitchItem(
) title = stringResource(KMR.strings.migrationConfigScreen_smartSearchSingleEntryTitle),
MigrationSheetSwitchItem( subtitle = stringResource(KMR.strings.migrationConfigScreen_smartSearchSingleEntrySubtitle),
title = stringResource(MR.strings.migrationConfigScreen_prioritizeByChaptersTitle), preference = migrationSmartSearchSingleEntryPref,
subtitle = stringResource(MR.strings.migrationConfigScreen_prioritizeByChaptersSubtitle), )
preference = preferences.migrationPrioritizeByChapters(), }
) if (!isSingleEntry || isSmartSearchSingleEntry) {
// KMK <--
MigrationSheetWarningItem(stringResource(MR.strings.migrationConfigScreen_enhancedOptionsWarning))
MigrationSheetSwitchItem(
title = stringResource(MR.strings.migrationConfigScreen_deepSearchModeTitle),
subtitle = stringResource(MR.strings.migrationConfigScreen_deepSearchModeSubtitle),
preference = preferences.migrationDeepSearchMode(),
)
MigrationSheetSwitchItem(
title = stringResource(MR.strings.migrationConfigScreen_prioritizeByChaptersTitle),
subtitle = stringResource(MR.strings.migrationConfigScreen_prioritizeByChaptersSubtitle),
preference = preferences.migrationPrioritizeByChapters(),
)
}
} }
} }
HorizontalDivider() HorizontalDivider()

View file

@ -6,6 +6,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -25,7 +28,13 @@ import tachiyomi.i18n.MR
/** /**
* Screen showing a list of pair of current-target manga entries being migrated. * Screen showing a list of pair of current-target manga entries being migrated.
*/ */
class MigrationListScreen(private val mangaIds: Collection<Long>, private val extraSearchQuery: String?) : Screen() { class MigrationListScreen(
private val mangaIds: Collection<Long>,
private val extraSearchQuery: String?,
// KMK -->
private val isSmartSearchSingleEntry: Boolean = false,
// KMK <--
) : Screen() {
private var matchOverride: Pair<Long, Long>? = null private var matchOverride: Pair<Long, Long>? = null
@ -36,10 +45,24 @@ class MigrationListScreen(private val mangaIds: Collection<Long>, private val ex
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MigrationListScreenModel(mangaIds, extraSearchQuery) } // KMK -->
val singleEntryNoSmartSearch = mangaIds.size == 1 && !isSmartSearchSingleEntry
// KMK <--
val screenModel = rememberScreenModel { MigrationListScreenModel(mangaIds, extraSearchQuery, /* KMK --> */ singleEntryNoSmartSearch /* KMK <-- */) }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
val context = LocalContext.current val context = LocalContext.current
// KMK -->
var hasPushedManual by rememberSaveable(mangaIds) { mutableStateOf(false) }
LaunchedEffect(mangaIds) {
if (singleEntryNoSmartSearch && !hasPushedManual) {
@Suppress("AssignedValueIsNeverRead")
hasPushedManual = true
navigator.push(MigrateSearchScreen(mangaIds.single()))
}
}
// KMK <--
LaunchedEffect(matchOverride) { LaunchedEffect(matchOverride) {
val (current, target) = matchOverride ?: return@LaunchedEffect val (current, target) = matchOverride ?: return@LaunchedEffect
screenModel.useMangaForMigration( screenModel.useMangaForMigration(

View file

@ -48,6 +48,9 @@ import uy.kohesive.injekt.api.get
class MigrationListScreenModel( class MigrationListScreenModel(
mangaIds: Collection<Long>, mangaIds: Collection<Long>,
extraSearchQuery: String?, extraSearchQuery: String?,
// KMK -->
runManually: Boolean = false,
// KMK <--
val preferences: SourcePreferences = Injekt.get(), val preferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
@ -100,12 +103,19 @@ class MigrationListScreenModel(
// KMK <-- // KMK <--
), ),
parentContext = screenModelScope.coroutineContext, parentContext = screenModelScope.coroutineContext,
) // KMK -->
).apply {
if (runManually) searchResult.value = SearchResult.NotFound
// KMK <--
}
} }
} }
.awaitAll() .awaitAll()
.filterNotNull() .filterNotNull()
mutableState.update { it.copy(items = manga.toImmutableList()) } mutableState.update { it.copy(items = manga.toImmutableList()) }
// KMK -->
if (runManually) return@launchIO
// KMK <--
runMigrations(manga) runMigrations(manga)
} }
} }

View file

@ -183,6 +183,8 @@
<string name="sort_feed_confirmation">Would you like to sort the feeds alphabetically?</string> <string name="sort_feed_confirmation">Would you like to sort the feeds alphabetically?</string>
<!-- Migration --> <!-- Migration -->
<string name="current_">Current: %1$s</string> <string name="current_">Current: %1$s</string>
<string name="migrationConfigScreen_smartSearchSingleEntryTitle">Use smart search</string>
<string name="migrationConfigScreen_smartSearchSingleEntrySubtitle">If enabled, automatically picks the best match result. Otherwise, shows all search results for manual selection.</string>
<!-- Misc --> <!-- Misc -->
<string name="job_failed_schedule_update_check">Failed to schedule automatic library update: %s</string> <string name="job_failed_schedule_update_check">Failed to schedule automatic library update: %s</string>
<string name="job_failed_schedule_backup_check">Failed to schedule automatic backup: %s</string> <string name="job_failed_schedule_backup_check">Failed to schedule automatic backup: %s</string>