mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
Add migration config screen to select and prioritize target sources (#2144)
This commit is contained in:
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
import tachiyomi.core.common.preference.getLongArray
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
|
||||||
class SourcePreferences(
|
class SourcePreferences(
|
||||||
@@ -20,6 +21,8 @@ class SourcePreferences(
|
|||||||
|
|
||||||
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
|
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
|
||||||
|
|
||||||
|
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||||
|
|
||||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||||
|
@@ -40,6 +40,7 @@ fun GlobalSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
hideSourceFilter = false,
|
||||||
sourceFilter = state.sourceFilter,
|
sourceFilter = state.sourceFilter,
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
@@ -32,6 +32,7 @@ fun MigrateSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
hideSourceFilter = true,
|
||||||
sourceFilter = state.sourceFilter,
|
sourceFilter = state.sourceFilter,
|
||||||
onChangeSearchFilter = onChangeSearchFilter,
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
onlyShowHasResults = state.onlyShowHasResults,
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
@@ -40,6 +40,7 @@ fun GlobalSearchToolbar(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
hideSourceFilter: Boolean,
|
||||||
sourceFilter: SourceFilter,
|
sourceFilter: SourceFilter,
|
||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
onlyShowHasResults: Boolean,
|
onlyShowHasResults: Boolean,
|
||||||
@@ -73,38 +74,40 @@ fun GlobalSearchToolbar(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
// TODO: make this UX better; it only applies when triggering a new search
|
// TODO: make this UX better; it only applies when triggering a new search
|
||||||
FilterChip(
|
if (!hideSourceFilter) {
|
||||||
selected = sourceFilter == SourceFilter.PinnedOnly,
|
FilterChip(
|
||||||
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
selected = sourceFilter == SourceFilter.PinnedOnly,
|
||||||
leadingIcon = {
|
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||||
Icon(
|
leadingIcon = {
|
||||||
imageVector = Icons.Outlined.PushPin,
|
Icon(
|
||||||
contentDescription = null,
|
imageVector = Icons.Outlined.PushPin,
|
||||||
modifier = Modifier
|
contentDescription = null,
|
||||||
.size(FilterChipDefaults.IconSize),
|
modifier = Modifier
|
||||||
)
|
.size(FilterChipDefaults.IconSize),
|
||||||
},
|
)
|
||||||
label = {
|
},
|
||||||
Text(text = stringResource(MR.strings.pinned_sources))
|
label = {
|
||||||
},
|
Text(text = stringResource(MR.strings.pinned_sources))
|
||||||
)
|
},
|
||||||
FilterChip(
|
)
|
||||||
selected = sourceFilter == SourceFilter.All,
|
FilterChip(
|
||||||
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
selected = sourceFilter == SourceFilter.All,
|
||||||
leadingIcon = {
|
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
||||||
Icon(
|
leadingIcon = {
|
||||||
imageVector = Icons.Outlined.DoneAll,
|
Icon(
|
||||||
contentDescription = null,
|
imageVector = Icons.Outlined.DoneAll,
|
||||||
modifier = Modifier
|
contentDescription = null,
|
||||||
.size(FilterChipDefaults.IconSize),
|
modifier = Modifier
|
||||||
)
|
.size(FilterChipDefaults.IconSize),
|
||||||
},
|
)
|
||||||
label = {
|
},
|
||||||
Text(text = stringResource(MR.strings.all))
|
label = {
|
||||||
},
|
Text(text = stringResource(MR.strings.all))
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
VerticalDivider()
|
VerticalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = onlyShowHasResults,
|
selected = onlyShowHasResults,
|
||||||
|
@@ -10,10 +10,10 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.browse.MigrateMangaScreen
|
import eu.kanade.presentation.browse.MigrateMangaScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
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.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import mihon.feature.migration.MigrateMangaConfigScreen
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ data class MigrateMangaScreen(
|
|||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
title = state.source!!.name,
|
title = state.source!!.name,
|
||||||
state = state,
|
state = state,
|
||||||
onClickItem = { navigator.push(MigrateSearchScreen(it.id)) },
|
onClickItem = { navigator.push(MigrateMangaConfigScreen(it.id)) },
|
||||||
onClickCover = { navigator.push(MangaScreen(it.id)) },
|
onClickCover = { navigator.push(MangaScreen(it.id)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,20 +1,33 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.SearchScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class MigrateSearchScreenModel(
|
class MigrateSearchScreenModel(
|
||||||
val mangaId: Long,
|
val mangaId: Long,
|
||||||
getManga: GetManga = Injekt.get(),
|
getManga: GetManga = Injekt.get(),
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||||
) : SearchScreenModel() {
|
) : 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 {
|
init {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
val manga = getManga.await(mangaId)!!
|
val manga = getManga.await(mangaId)!!
|
||||||
@@ -29,14 +42,6 @@ class MigrateSearchScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getEnabledSources(): List<CatalogueSource> {
|
override fun getEnabledSources(): List<CatalogueSource> {
|
||||||
return super.getEnabledSources()
|
return migrationSources.mapNotNull { sourceManager.get(it) as? 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})" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -55,7 +55,7 @@ abstract class SearchScreenModel(
|
|||||||
|
|
||||||
protected var extensionFilter: String? = null
|
protected var extensionFilter: String? = null
|
||||||
|
|
||||||
private val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
|
open val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
|
||||||
compareBy<CatalogueSource>(
|
compareBy<CatalogueSource>(
|
||||||
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
|
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
|
||||||
{ "${it.id}" !in pinnedSources },
|
{ "${it.id}" !in pinnedSources },
|
||||||
|
@@ -45,7 +45,6 @@ import eu.kanade.tachiyomi.source.isLocalOrStub
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
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.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.browse.BrowseSourceScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
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 eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import mihon.feature.migration.MigrateMangaConfigScreen
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
@@ -163,7 +163,7 @@ class MangaScreen(
|
|||||||
successState.manga.favorite
|
successState.manga.favorite
|
||||||
},
|
},
|
||||||
onMigrateClicked = {
|
onMigrateClicked = {
|
||||||
navigator.push(MigrateSearchScreen(successState.manga.id))
|
navigator.push(MigrateMangaConfigScreen(successState.manga.id))
|
||||||
}.takeIf { successState.manga.favorite },
|
}.takeIf { successState.manga.favorite },
|
||||||
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
|
||||||
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
||||||
|
@@ -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 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(
|
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||||
key: String,
|
key: String,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
|
@@ -995,4 +995,12 @@
|
|||||||
|
|
||||||
<!-- Notes screen -->
|
<!-- Notes screen -->
|
||||||
<string name="notes_placeholder">Enjoyed the part where…</string>
|
<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>
|
</resources>
|
||||||
|
Reference in New Issue
Block a user