diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index baae91d68..a84767aea 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -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()) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index da4db5e98..8d949235a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -40,6 +40,7 @@ fun GlobalSearchScreen( navigateUp = navigateUp, onChangeSearchQuery = onChangeSearchQuery, onSearch = onSearch, + hideSourceFilter = false, sourceFilter = state.sourceFilter, onChangeSearchFilter = onChangeSearchFilter, onlyShowHasResults = state.onlyShowHasResults, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 31abb596c..48b86bba5 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -32,6 +32,7 @@ fun MigrateSearchScreen( navigateUp = navigateUp, onChangeSearchQuery = onChangeSearchQuery, onSearch = onSearch, + hideSourceFilter = true, sourceFilter = state.sourceFilter, onChangeSearchFilter = onChangeSearchFilter, onlyShowHasResults = state.onlyShowHasResults, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index fa61696ae..dd67afbbf 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt index 6f26c559b..67fa5cedc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt @@ -10,10 +10,10 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.MigrateMangaScreen import eu.kanade.presentation.util.Screen -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen 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 @@ -38,7 +38,7 @@ data class MigrateMangaScreen( navigateUp = navigator::pop, title = state.source!!.name, state = state, - onClickItem = { navigator.push(MigrateSearchScreen(it.id)) }, + onClickItem = { navigator.push(MigrateMangaConfigScreen(it.id)) }, onClickCover = { navigator.push(MangaScreen(it.id)) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index d22756c78..b01212415 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -1,20 +1,33 @@ 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.tachiyomi.source.CatalogueSource +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 uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class MigrateSearchScreenModel( val mangaId: Long, getManga: GetManga = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val sourcePreferences: SourcePreferences = Injekt.get(), ) : SearchScreenModel() { + private val migrationSources by lazy { sourcePreferences.migrationSources().get() } + + override val sortComparator = { map: Map -> + compareBy( + { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, + { migrationSources.indexOf(it.id) }, + ) + } + init { screenModelScope.launch { val manga = getManga.await(mangaId)!! @@ -29,14 +42,6 @@ class MigrateSearchScreenModel( } override fun getEnabledSources(): List { - return super.getEnabledSources() - .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})" }, - ), - ) + return migrationSources.mapNotNull { sourceManager.get(it) as? CatalogueSource } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index a1c1cdfdf..9d90411a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -55,7 +55,7 @@ abstract class SearchScreenModel( protected var extensionFilter: String? = null - private val sortComparator = { map: Map -> + open val sortComparator = { map: Map -> compareBy( { (map[it] as? SearchItemResult.Success)?.isEmpty ?: true }, { "${it.id}" !in pinnedSources }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 94dd299fe..3c5acb998 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -45,7 +45,6 @@ import eu.kanade.tachiyomi.source.isLocalOrStub import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel -import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen @@ -60,6 +59,7 @@ import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.launch import logcat.LogPriority +import mihon.feature.migration.MigrateMangaConfigScreen import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -163,7 +163,7 @@ class MangaScreen( successState.manga.favorite }, onMigrateClicked = { - navigator.push(MigrateSearchScreen(successState.manga.id)) + navigator.push(MigrateMangaConfigScreen(successState.manga.id)) }.takeIf { successState.manga.favorite }, onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) }, onMultiBookmarkClicked = screenModel::bookmarkChapters, diff --git a/app/src/main/java/mihon/feature/migration/MigrateMangaConfigScreen.kt b/app/src/main/java/mihon/feature/migration/MigrateMangaConfigScreen.kt new file mode 100644 index 000000000..2ca412f78 --- /dev/null +++ b/app/src/main/java/mihon/feature/migration/MigrateMangaConfigScreen.kt @@ -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(State()) { + + init { + screenModelScope.launchIO { + initSources() + } + } + + private val sourcesComparator = { includedSources: List -> + compareBy( + { !it.isSelected }, + { includedSources.indexOf(it.source.id) }, + { with(it.source) { "$name (${LocaleHelper.getLocalizedDisplayName(lang)})" } }, + ) + } + + private fun updateSources(save: Boolean = true, action: (List) -> List) { + 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() + .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 = 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 + } +} diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt b/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt index 0cd3a21bf..6407224a6 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/preference/PreferenceStore.kt @@ -24,6 +24,18 @@ interface PreferenceStore { fun getAll(): Map } +fun PreferenceStore.getLongArray( + key: String, + defaultValue: List, +): Preference> { + return getObject( + key = key, + defaultValue = defaultValue, + serializer = { it.joinToString(",") }, + deserializer = { it.split(",").mapNotNull { l -> l.toLongOrNull() } }, + ) +} + inline fun > PreferenceStore.getEnum( key: String, defaultValue: T, diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 7d1a4681c..41f6fad3e 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -995,4 +995,12 @@ Enjoyed the part where… + + Selected + Available + Select all + Select none + Select enabled sources + Select pinned sources + Continue