Add migration config screen to select and prioritize target sources (#2144)

(cherry picked from commit 2e180005a01f633ad7fafc5cfb3079f0bc858448)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
This commit is contained in:
AntsyLich
2025-05-28 20:49:44 +05:45
committed by NGB-Was-Taken
parent e074df469e
commit 5156248a96
11 changed files with 492 additions and 85 deletions
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
import tachiyomi.core.common.preference.getLongArray
import tachiyomi.domain.library.model.LibraryDisplayMode
class SourcePreferences(
@@ -20,6 +21,8 @@ class SourcePreferences(
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
@@ -41,6 +41,7 @@ fun GlobalSearchScreen(
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
hideSourceFilter = false,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
@@ -32,6 +32,7 @@ fun MigrateSearchScreen(
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
hideSourceFilter = true,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,
@@ -40,6 +40,7 @@ fun GlobalSearchToolbar(
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
hideSourceFilter: Boolean,
sourceFilter: SourceFilter,
onChangeSearchFilter: (SourceFilter) -> Unit,
onlyShowHasResults: Boolean,
@@ -73,38 +74,40 @@ fun GlobalSearchToolbar(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
// TODO: make this UX better; it only applies when triggering a new search
FilterChip(
selected = sourceFilter == SourceFilter.PinnedOnly,
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.PushPin,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(MR.strings.pinned_sources))
},
)
FilterChip(
selected = sourceFilter == SourceFilter.All,
onClick = { onChangeSearchFilter(SourceFilter.All) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.DoneAll,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(MR.strings.all))
},
)
if (!hideSourceFilter) {
FilterChip(
selected = sourceFilter == SourceFilter.PinnedOnly,
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.PushPin,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(MR.strings.pinned_sources))
},
)
FilterChip(
selected = sourceFilter == SourceFilter.All,
onClick = { onChangeSearchFilter(SourceFilter.All) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.DoneAll,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(MR.strings.all))
},
)
VerticalDivider()
VerticalDivider()
}
FilterChip(
selected = onlyShowHasResults,
@@ -8,17 +8,14 @@ import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.MigrateMangaScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import mihon.feature.migration.MigrateMangaConfigScreen
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class MigrateMangaScreen(
private val sourceId: Long,
@@ -41,15 +38,7 @@ data class MigrateMangaScreen(
navigateUp = navigator::pop,
title = state.source!!.name,
state = state,
onClickItem = {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
listOf(it.id),
)
// SY <--
},
onClickItem = { navigator.push(MigrateMangaConfigScreen(it.id)) },
onClickCover = { navigator.push(MangaScreen(it.id)) },
)
@@ -1,27 +1,36 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateSearchScreenModel(
val mangaId: Long,
// SY -->
val validSources: List<Long>,
// SY <--
getManga: GetManga = Injekt.get(),
// SY -->
private val sourceManager: SourceManager = Injekt.get(),
// SY <--
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : SearchScreenModel() {
private val migrationSources by lazy { sourcePreferences.migrationSources().get() }
override val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
compareBy<CatalogueSource>(
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
{ migrationSources.indexOf(it.id) },
)
}
init {
screenModelScope.launch {
val manga = getManga.await(mangaId)!!
@@ -36,17 +45,6 @@ class MigrateSearchScreenModel(
}
override fun getEnabledSources(): List<CatalogueSource> {
// SY -->
return validSources.mapNotNull { sourceManager.get(it) }
.filterIsInstance<CatalogueSource>()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
.sortedWith(
compareBy(
{ it.id != state.value.fromSourceId },
{ "${it.id}" !in pinnedSources },
{ "${it.name.lowercase()} (${it.lang})" },
),
)
// SY <--
return migrationSources.mapNotNull { sourceManager.get(it) as? CatalogueSource }
}
}
@@ -56,7 +56,7 @@ abstract class SearchScreenModel(
protected var extensionFilter: String? = null
private val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
open val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
compareBy<CatalogueSource>(
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
{ "${it.id}" !in pinnedSources },
@@ -27,7 +27,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.manga.ChapterSettingsDialog
@@ -44,7 +43,8 @@ import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.isLocalOrStub
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedScreen
@@ -73,6 +73,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.feature.migration.MigrateMangaConfigScreen
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.withIOContext
@@ -206,9 +207,11 @@ class MangaScreen(
successState.manga.favorite
},
previewsRowCount = successState.previewsRowCount,
onMigrateClicked = {
navigator.push(MigrateMangaConfigScreen(successState.manga.id))
}.takeIf { successState.manga.favorite },
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
// SY -->
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
onEditInfoClicked = screenModel::showEditMangaInfoDialog,
onRecommendClicked = {
@@ -265,11 +268,18 @@ class MangaScreen(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = {
// SY -->
migrateManga(navigator, it, screenModel.manga!!.id)
// SY <--
},
onMigrate = { screenModel.showMigrateDialog(it) },
)
}
is MangaScreenModel.Dialog.Migrate -> {
MigrateDialog(
oldManga = dialog.oldManga,
newManga = dialog.newManga,
screenModel = MigrateDialogScreenModel(),
onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = onDismissRequest,
)
}
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
@@ -471,20 +481,6 @@ class MangaScreen(
// SY -->
/**
* Initiates source migration for the specific manga.
*/
private fun migrateManga(navigator: Navigator, manga: Manga, toMangaId: Long? = null) {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
manga.id,
toMangaId,
)
// SY <--
}
private fun openMetadataViewer(navigator: Navigator, manga: Manga) {
navigator.push(MetadataViewScreen(manga.id, manga.source))
}
@@ -0,0 +1,396 @@
package mihon.feature.migration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
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.Deselect
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.updateAndGet
import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.ReorderableLazyListState
import sh.calvin.reorderable.rememberReorderableLazyListState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.Pill
import tachiyomi.presentation.core.components.material.DISABLED_ALPHA
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.shouldExpandFAB
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateMangaConfigScreen(private val mangaId: Long) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ScreenModel() }
val state by screenModel.state.collectAsState()
val (selectedSources, availableSources) = state.sources.partition { it.isSelected }
val showLanguage by remember(state) {
derivedStateOf {
state.sources.distinctBy { it.source.lang }.size > 1
}
}
val lazyListState = rememberLazyListState()
Scaffold(
topBar = {
AppBar(
title = null,
navigateUp = navigator::pop,
scrollBehavior = it,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.migrationConfigScreen_selectAllLabel),
icon = Icons.Outlined.SelectAll,
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.All) },
),
AppBar.Action(
title = stringResource(MR.strings.migrationConfigScreen_selectNoneLabel),
icon = Icons.Outlined.Deselect,
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.None) },
),
AppBar.OverflowAction(
title = stringResource(MR.strings.migrationConfigScreen_selectEnabledLabel),
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.Enabled) },
),
AppBar.OverflowAction(
title = stringResource(MR.strings.migrationConfigScreen_selectPinnedLabel),
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.Pinned) },
),
),
)
},
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
onClick = { navigator.replace(MigrateSearchScreen(mangaId)) },
expanded = lazyListState.shouldExpandFAB(),
)
},
) { contentPadding ->
val reorderableState = rememberReorderableLazyListState(lazyListState, contentPadding) { from, to ->
val fromIndex = selectedSources.indexOfFirst { it.id == from.key }
val toIndex = selectedSources.indexOfFirst { it.id == to.key }
if (fromIndex == -1 || toIndex == -1) return@rememberReorderableLazyListState
screenModel.orderSource(fromIndex, toIndex)
}
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = contentPadding,
) {
listOf(selectedSources, availableSources).fastForEachIndexed { listIndex, sources ->
val selectedSourceList = listIndex == 0
if (sources.isNotEmpty()) {
val headerPrefix = if (selectedSourceList) "selected" else "available"
item("$headerPrefix-header") {
Text(
text = stringResource(
resource = if (selectedSourceList) {
MR.strings.migrationConfigScreen_selectedHeader
} else {
MR.strings.migrationConfigScreen_availableHeader
},
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.padding(MaterialTheme.padding.medium)
.animateItem(),
)
}
}
itemsIndexed(
items = sources,
key = { _, item -> item.id },
) { index, item ->
SourceItemContainer(
firstItem = index == 0,
lastItem = index == (sources.size - 1),
source = item.source,
showLanguage = showLanguage,
isSelected = item.isSelected,
dragEnabled = selectedSourceList && sources.size > 1,
state = reorderableState,
key = { if (selectedSourceList) it.id else "available-${it.id}" },
onClick = { screenModel.toggleSelection(item.id) },
)
}
}
}
}
}
@Composable
private fun LazyItemScope.SourceItemContainer(
firstItem: Boolean,
lastItem: Boolean,
source: Source,
showLanguage: Boolean,
isSelected: Boolean,
dragEnabled: Boolean,
state: ReorderableLazyListState,
key: (Source) -> Any,
onClick: () -> Unit,
) {
val shape = remember(firstItem, lastItem) {
val top = if (firstItem) 12.dp else 0.dp
val bottom = if (lastItem) 12.dp else 0.dp
RoundedCornerShape(top, top, bottom, bottom)
}
ReorderableItem(
state = state,
key = key(source),
enabled = dragEnabled,
) { _ ->
ElevatedCard(
shape = shape,
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.medium)
.animateItem(),
) {
SourceItem(
source = source,
showLanguage = showLanguage,
isSelected = isSelected,
dragEnabled = dragEnabled,
scope = this@ReorderableItem,
onClick = onClick,
)
}
}
if (!lastItem) {
HorizontalDivider(modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium))
}
}
@Composable
private fun SourceItem(
source: Source,
showLanguage: Boolean,
isSelected: Boolean,
dragEnabled: Boolean,
scope: ReorderableCollectionItemScope,
onClick: () -> Unit,
) {
ListItem(
headlineContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically,
) {
SourceIcon(source = source)
Text(
text = source.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
)
if (showLanguage) {
Pill(
text = LocaleHelper.getLocalizedDisplayName(source.lang),
style = MaterialTheme.typography.bodySmall,
)
}
}
},
trailingContent = if (dragEnabled) {
{
Icon(
imageVector = Icons.Outlined.DragHandle,
contentDescription = null,
modifier = with(scope) {
Modifier.draggableHandle()
},
)
}
} else {
null
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
),
modifier = Modifier
.clickable(onClick = onClick)
.alpha(if (isSelected) 1f else DISABLED_ALPHA),
)
}
private class ScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : StateScreenModel<ScreenModel.State>(State()) {
init {
screenModelScope.launchIO {
initSources()
}
}
private val sourcesComparator = { includedSources: List<Long> ->
compareBy<MigrationSource>(
{ !it.isSelected },
{ includedSources.indexOf(it.source.id) },
{ with(it.source) { "$name (${LocaleHelper.getLocalizedDisplayName(lang)})" } },
)
}
private fun updateSources(save: Boolean = true, action: (List<MigrationSource>) -> List<MigrationSource>) {
val state = mutableState.updateAndGet { state ->
val updatedSources = action(state.sources)
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
}
if (!save) return
state.sources
.filter { source -> source.isSelected }
.map { source -> source.source.id }
.let { sources -> sourcePreferences.migrationSources().set(sources) }
}
private fun initSources() {
val languages = sourcePreferences.enabledLanguages().get()
val pinnedSources = sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
val includedSources = sourcePreferences.migrationSources().get()
val disabledSources = sourcePreferences.disabledSources().get()
.mapNotNull { it.toLongOrNull() }
val sources = sourceManager.getCatalogueSources()
.asSequence()
.filterIsInstance<HttpSource>()
.filter { it.lang in languages }
.map {
val source = Source(
id = it.id,
lang = it.lang,
name = it.name,
supportsLatest = false,
isStub = false,
)
MigrationSource(
source = source,
isSelected = when {
includedSources.isNotEmpty() -> source.id in includedSources
pinnedSources.isNotEmpty() -> source.id in pinnedSources
else -> source.id !in disabledSources
},
)
}
.toList()
updateSources(save = false) { sources }
}
fun toggleSelection(id: Long) {
updateSources { sources ->
sources.map { source ->
source.copy(isSelected = if (source.source.id == id) !source.isSelected else source.isSelected)
}
}
}
fun toggleSelection(config: SelectionConfig) {
val pinnedSources = sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
val disabledSources = sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() }
val isSelected: (Long) -> Boolean = {
when (config) {
SelectionConfig.All -> true
SelectionConfig.None -> false
SelectionConfig.Pinned -> it in pinnedSources
SelectionConfig.Enabled -> it !in disabledSources
}
}
updateSources { sources ->
sources.map { source ->
source.copy(isSelected = isSelected(source.source.id))
}
}
}
fun orderSource(from: Int, to: Int) {
updateSources {
it.toMutableList()
.apply {
add(to, removeAt(from))
}
.toList()
}
}
data class State(
val sources: List<MigrationSource> = emptyList(),
)
enum class SelectionConfig {
All,
None,
Pinned,
Enabled,
}
}
data class MigrationSource(
val source: Source,
val isSelected: Boolean,
) {
val id = source.id
val visualName = source.visualName
}
}
@@ -24,6 +24,18 @@ interface PreferenceStore {
fun getAll(): Map<String, *>
}
fun PreferenceStore.getLongArray(
key: String,
defaultValue: List<Long>,
): Preference<List<Long>> {
return getObject(
key = key,
defaultValue = defaultValue,
serializer = { it.joinToString(",") },
deserializer = { it.split(",").mapNotNull { l -> l.toLongOrNull() } },
)
}
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
key: String,
defaultValue: T,
@@ -995,4 +995,12 @@
<!-- Notes screen -->
<string name="notes_placeholder">Enjoyed the part where…</string>
<string name="migrationConfigScreen.selectedHeader">Selected</string>
<string name="migrationConfigScreen.availableHeader">Available</string>
<string name="migrationConfigScreen.selectAllLabel">Select all</string>
<string name="migrationConfigScreen.selectNoneLabel">Select none</string>
<string name="migrationConfigScreen.selectEnabledLabel">Select enabled sources</string>
<string name="migrationConfigScreen.selectPinnedLabel">Select pinned sources</string>
<string name="migrationConfigScreen.continueButtonText">Continue</string>
</resources>