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)
// KMK -->
fun migrationSmartSearchSingleEntry() = preferenceStore.getBoolean("migration_smart_search_single_entry", false)
fun globalSearchPinnedState() = preferenceStore.getEnum(
Preference.appStateKey("global_search_pinned_toggle_state"),
SourceFilter.PinnedOnly,

View file

@ -121,7 +121,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
return
}
val screen = // KMK --> if (mangaId == null) {
MigrationListScreen(mangaIds, extraSearchQuery)
MigrationListScreen(mangaIds, extraSearchQuery, screenModel.sourcePreferences.migrationSmartSearchSingleEntry().get())
// KMK -->
// } else {
// MigrateSearchScreen(mangaId)
@ -297,6 +297,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
migrationSheetOpen = false
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.toggle
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@ -53,6 +54,7 @@ fun MigrationConfigScreenSheet(
onStartMigration: (extraSearchQuery: String?) -> Unit,
// KMK -->
fullSettings: Boolean = true,
isSingleEntry: Boolean = false,
// KMK <--
) {
var extraSearchQuery by rememberSaveable { mutableStateOf("") }
@ -160,17 +162,30 @@ fun MigrationConfigScreenSheet(
if (fullSettings) {
// KMK <--
MigrationSheetDividerItem()
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(),
)
// KMK -->
val migrationSmartSearchSingleEntryPref = preferences.migrationSmartSearchSingleEntry()
val isSmartSearchSingleEntry by migrationSmartSearchSingleEntryPref.collectAsState()
if (isSingleEntry) {
MigrationSheetSwitchItem(
title = stringResource(KMR.strings.migrationConfigScreen_smartSearchSingleEntryTitle),
subtitle = stringResource(KMR.strings.migrationConfigScreen_smartSearchSingleEntrySubtitle),
preference = migrationSmartSearchSingleEntryPref,
)
}
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()

View file

@ -6,6 +6,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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 cafe.adriel.voyager.core.model.rememberScreenModel
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.
*/
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
@ -36,10 +45,24 @@ class MigrationListScreen(private val mangaIds: Collection<Long>, private val ex
@Composable
override fun Content() {
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 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) {
val (current, target) = matchOverride ?: return@LaunchedEffect
screenModel.useMangaForMigration(

View file

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

View file

@ -183,6 +183,8 @@
<string name="sort_feed_confirmation">Would you like to sort the feeds alphabetically?</string>
<!-- Migration -->
<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 -->
<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>