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

(cherry picked from commit 2e180005a01f633ad7fafc5cfb3079f0bc858448)

Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
This commit is contained in:
AntsyLich 2025-05-28 21:04:44 +06:00 committed by Cuong-Tran
parent 147c24c42e
commit c328b37675
50 changed files with 619 additions and 898 deletions

View file

@ -6,6 +6,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(
@ -21,6 +22,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())
@ -111,10 +114,6 @@ class SourcePreferences(
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
fun defaultMangaOrder() = preferenceStore.getString("default_manga_order", "")
fun migrationSources() = preferenceStore.getString("migrate_sources", "")
fun smartMigration() = preferenceStore.getBoolean("smart_migrate", false)
fun useSourceWithMost() = preferenceStore.getBoolean("use_source_with_most", false)

View file

@ -77,6 +77,7 @@ fun GlobalSearchScreen(
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
hideSourceFilter = false,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,

View file

@ -67,6 +67,7 @@ fun MigrateSearchScreen(
navigateUp = navigateUp,
onChangeSearchQuery = onChangeSearchQuery,
onSearch = onSearch,
hideSourceFilter = true,
sourceFilter = state.sourceFilter,
onChangeSearchFilter = onChangeSearchFilter,
onlyShowHasResults = state.onlyShowHasResults,

View file

@ -42,6 +42,7 @@ fun GlobalSearchToolbar(
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
hideSourceFilter: Boolean,
sourceFilter: SourceFilter,
onChangeSearchFilter: (SourceFilter) -> Unit,
onlyShowHasResults: Boolean,
@ -89,6 +90,7 @@ fun GlobalSearchToolbar(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
// TODO: make this UX better; it only applies when triggering a new search
if (!hideSourceFilter) {
// KMK -->
if (hasPinnedSources) {
// KMK <--
@ -125,6 +127,7 @@ fun GlobalSearchToolbar(
)
VerticalDivider()
}
FilterChip(
selected = onlyShowHasResults,

View file

@ -320,7 +320,6 @@ class AboutScreen : Screen() {
is GetApplicationRelease.Result.OsTooOld -> {
context.toast(MR.strings.update_check_eol)
}
else -> {}
}
} catch (e: Exception) {
context.toast(e.message)

View file

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy
class MigrationSourceAdapter(
listener: FlexibleAdapter.OnItemClickListener,
) : FlexibleAdapter<MigrationSourceItem>(
null,
listener,
true,
) {
val sourceManager: SourceManager by injectLazy()
// SY _->
val sourcePreferences: SourcePreferences by injectLazy()
// SY <--
}

View file

@ -1,49 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.MigrationSourceItemBinding
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationSourceHolder(view: View, val adapter: MigrationSourceAdapter) :
FlexibleViewHolder(view, adapter) {
val binding = MigrationSourceItemBinding.bind(view)
init {
setDragHandleView(binding.reorder)
}
fun bind(source: HttpSource, sourceEnabled: Boolean) {
// Set capitalized title.
val sourceName =
// KMK -->
source.getNameForMangaInfo()
// KMK <--
binding.title.text = sourceName
// Update circle letter image.
itemView.post {
val icon = Injekt.get<ExtensionManager>().getAppIconForSource(source.id)
if (icon != null) {
binding.image.setImageDrawable(icon)
}
}
if (sourceEnabled) {
binding.title.alpha = 1.0f
binding.image.alpha = 1.0f
binding.title.paintFlags = binding.title.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
} else {
binding.title.alpha = DISABLED_ALPHA
binding.image.alpha = DISABLED_ALPHA
binding.title.paintFlags = binding.title.paintFlags or STRIKE_THRU_TEXT_FLAG
}
}
companion object {
private const val DISABLED_ALPHA = 0.3f
}
}

View file

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.os.Parcelable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.parcelize.Parcelize
import tachiyomi.domain.source.service.SourceManager
class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean) : AbstractFlexibleItem<MigrationSourceHolder>() {
override fun getLayoutRes() = R.layout.migration_source_item
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationSourceHolder {
return MigrationSourceHolder(view, adapter as MigrationSourceAdapter)
}
/**
* Binds the given view holder with this item.
*
* @param adapter The adapter of this item.
* @param holder The holder to bind.
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: MigrationSourceHolder,
position: Int,
payloads: List<Any?>?,
) {
holder.bind(source, sourceEnabled)
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is MigrationSourceItem) {
return source.id == other.source.id
}
return false
}
override fun hashCode(): Int {
return source.id.hashCode()
}
@Parcelize
data class MigrationSource(val sourceId: Long, val sourceEnabled: Boolean) : Parcelable
fun asParcelable(): MigrationSource {
return MigrationSource(source.id, sourceEnabled)
}
companion object {
fun fromParcelable(sourceManager: SourceManager, migrationSource: MigrationSource): MigrationSourceItem? {
val source = sourceManager.get(migrationSource.sourceId) as? HttpSource ?: return null
return MigrationSourceItem(
source,
migrationSource.sourceEnabled,
)
}
}
}

View file

@ -1,409 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.view.LayoutInflater
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.ZeroCornerSize
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.PushPin
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material.icons.outlined.ToggleOn
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.SOURCE_SEARCH_BOX_HEIGHT
import eu.kanade.presentation.components.SourcesSearchBox
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import java.io.Serializable
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.seconds
sealed class MigrationType : Serializable {
data class MangaList(val mangaIds: List<Long>) : MigrationType()
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
}
/**
* The screen showing a list of selectable sources used for migration, with migration setting dialog.
*/
class PreMigrationScreen(val migration: MigrationType) : Screen() {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { PreMigrationScreenModel() }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val navigator = LocalNavigator.currentOrThrow
var fabExpanded by remember { mutableStateOf(true) }
val items by screenModel.state.collectAsState()
val adapter by screenModel.adapter.collectAsState()
// KMK -->
var searchQuery by remember { mutableStateOf("") }
BackHandler(enabled = searchQuery.isNotBlank()) {
searchQuery = ""
}
// KMK <--
LaunchedEffect(items.isNotEmpty(), adapter != null/* KMK --> */, searchQuery/* KMK <-- */) {
if (adapter != null && items.isNotEmpty()) {
adapter?.updateDataSet(
items
// KMK -->
.filter { migrationSource ->
if (searchQuery.isBlank()) return@filter true
val source = migrationSource.source
searchQuery.split(",").any {
val input = it.trim()
if (input.isEmpty()) return@any false
source.name.contains(input, ignoreCase = true) ||
source.id == input.toLongOrNull()
}
},
// KMK <--
)
}
}
val nestedScrollConnection = remember {
// All this lines just for fab state :/
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
fabExpanded = available.y >= 0
return scrollBehavior.nestedScrollConnection.onPreScroll(available, source)
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available)
}
}
}
Scaffold(
topBar = {
AppBar(
title = stringResource(SYMR.strings.select_sources),
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
)
},
// KMK -->
bottomBar = {
PreMigrationScreenBottomBar(
modifier = Modifier,
onSelectAll = { screenModel.massSelect(true) },
onSelectNone = { screenModel.massSelect(false) },
onSelectPinned = { screenModel.matchSelection(false) },
onSelectEnabled = { screenModel.matchSelection(true) },
)
},
// KMK <--
floatingActionButton = {
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_migrate)) },
icon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowForward,
contentDescription = stringResource(MR.strings.action_migrate),
)
},
onClick = {
screenModel.onMigrationSheet(true)
},
expanded = fabExpanded,
)
},
) { contentPadding ->
// KMK -->
val density = LocalDensity.current
Box(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) {
var searchBoxHeight by remember { mutableIntStateOf(with(density) { SOURCE_SEARCH_BOX_HEIGHT.roundToPx() }) }
// KMK <--
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
// KMK -->
val top = searchBoxHeight
// val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() }
// KMK <--
val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() }
val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() }
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
screenModel.controllerBinding =
PreMigrationListBinding.inflate(LayoutInflater.from(context))
screenModel.adapter.value = MigrationSourceAdapter(screenModel.clickListener)
screenModel.controllerBinding.root.adapter = screenModel.adapter.value
screenModel.adapter.value?.isHandleDragEnabled = true
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true)
screenModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
right = right,
bottom = bottom,
)
},
)
}
// KMK -->
SourcesSearchBox(
searchQuery = searchQuery,
onChangeSearchQuery = { searchQuery = it ?: "" },
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = MaterialTheme.padding.small),
onGloballyPositioned = { layoutCoordinates ->
searchBoxHeight = layoutCoordinates.size.height
},
placeholderText = stringResource(KMR.strings.action_search_for_source),
)
// KMK <--
}
}
val migrationSheetOpen by screenModel.migrationSheetOpen.collectAsState()
if (migrationSheetOpen) {
MigrationBottomSheetDialog(
onDismissRequest = { screenModel.onMigrationSheet(false) },
onStartMigration = { extraParam ->
screenModel.onMigrationSheet(false)
screenModel.saveEnabledSources()
navigator replace MigrationListScreen(MigrationProcedureConfig(migration, extraParam))
},
)
}
}
// KMK -->
@Composable
fun PreMigrationScreenBottomBar(
modifier: Modifier,
onSelectAll: () -> Unit,
onSelectNone: () -> Unit,
onSelectPinned: () -> Unit,
onSelectEnabled: () -> Unit,
) {
val scope = rememberCoroutineScope()
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(
bottomEnd = ZeroCornerSize,
bottomStart = ZeroCornerSize,
),
color = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation = 0.dp),
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 4).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false
}
}
Row(
modifier = Modifier
.padding(
WindowInsets.navigationBars
.only(WindowInsetsSides.Bottom)
.asPaddingValues(),
)
.padding(horizontal = 8.dp, vertical = 12.dp),
) {
Button(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onSelectAll,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
)
Button(
title = stringResource(SYMR.strings.select_none),
icon = Icons.Outlined.Deselect,
onClick = onSelectNone,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
)
Button(
title = stringResource(SYMR.strings.match_enabled_sources),
icon = Icons.Outlined.ToggleOn,
onClick = onSelectEnabled,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
)
Button(
title = stringResource(SYMR.strings.match_pinned_sources),
icon = Icons.Outlined.PushPin,
onClick = onSelectPinned,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
)
}
}
}
@Composable
private fun RowScope.Button(
title: String,
icon: ImageVector,
toConfirm: Boolean,
onLongClick: () -> Unit,
onClick: (() -> Unit),
content: (@Composable () -> Unit)? = null,
) {
val animatedWeight by animateFloatAsState(
targetValue = if (toConfirm) 2f else 1f,
label = "weight",
)
Column(
modifier = Modifier
.size(48.dp)
.weight(animatedWeight)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onLongClick = onLongClick,
onClick = onClick,
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = title,
)
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
Text(
text = title,
overflow = TextOverflow.Visible,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelSmall,
)
}
content?.invoke()
}
}
// KMK <--
companion object {
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), null),
)
} else {
PreMigrationScreen(MigrationType.MangaList(mangaIds))
},
)
}
/* All usages have been replaced by original Mihon's migration dialog */
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, fromMangaId: Long, toManga: Long?) {
navigator.push(
if (skipPre) {
MigrationListScreen(
MigrationProcedureConfig(MigrationType.MangaSingle(fromMangaId, toManga), null),
)
} else {
PreMigrationScreen(MigrationType.MangaSingle(fromMangaId, toManga))
},
)
}
}
}

View file

@ -1,137 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PreMigrationScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
) : ScreenModel {
private val _state = MutableStateFlow(emptyList<MigrationSourceItem>())
val state = _state.asStateFlow()
private val _migrationSheetOpen = MutableStateFlow(false)
val migrationSheetOpen = _migrationSheetOpen.asStateFlow()
lateinit var controllerBinding: PreMigrationListBinding
var adapter: MutableStateFlow<MigrationSourceAdapter?> = MutableStateFlow(null)
val clickListener = FlexibleAdapter.OnItemClickListener { _, position ->
val adapter = adapter.value ?: return@OnItemClickListener false
adapter.getItem(position)?.let {
it.sourceEnabled = !it.sourceEnabled
}
adapter.notifyItemChanged(position)
false
}
init {
screenModelScope.launchIO {
val enabledSources = getEnabledSources()
_state.update { enabledSources }
}
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<MigrationSourceItem> {
val languages = sourcePreferences.enabledLanguages().get()
val sourcesSaved = sourcePreferences.migrationSources().get().split("/")
.mapNotNull { it.toLongOrNull() }
val disabledSources = sourcePreferences.disabledSources().get()
.mapNotNull { it.toLongOrNull() }
val sources = sourceManager.getVisibleCatalogueSources()
.asSequence()
.filterIsInstance<HttpSource>()
.filter { it.lang in languages }
.sortedBy { "(${it.lang}) ${it.name}" }
.map {
MigrationSourceItem(
it,
isEnabled(
sourcesSaved,
disabledSources,
it.id,
),
)
}
.toList()
return sources
.filter { it.sourceEnabled }
.sortedBy { sourcesSaved.indexOf(it.source.id) }
.plus(
sources.filterNot { it.sourceEnabled },
)
}
fun isEnabled(
sourcesSaved: List<Long>,
disabledSources: List<Long>,
id: Long,
): Boolean {
return if (sourcesSaved.isEmpty()) {
id !in disabledSources
} else {
id in sourcesSaved
}
}
fun massSelect(selectAll: Boolean) {
val adapter = adapter.value ?: return
adapter.currentItems.forEach {
it.sourceEnabled = selectAll
}
adapter.notifyDataSetChanged()
}
fun matchSelection(matchEnabled: Boolean) {
val adapter = adapter.value ?: return
val enabledSources = if (matchEnabled) {
sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() }
} else {
sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() }
}
val items = adapter.currentItems.toList()
items.forEach {
it.sourceEnabled = if (matchEnabled) {
it.source.id !in enabledSources
} else {
it.source.id in enabledSources
}
}
val sortedItems = items.sortedBy { it.source.name }.sortedBy { !it.sourceEnabled }
adapter.updateDataSet(sortedItems)
}
fun onMigrationSheet(isOpen: Boolean) {
_migrationSheetOpen.value = isOpen
}
fun saveEnabledSources() {
val listOfSources = adapter.value?.currentItems
?.filterIsInstance<MigrationSourceItem>()
?.filter {
it.sourceEnabled
}
?.joinToString("/") { it.source.id.toString() }
.orEmpty()
sourcePreferences.migrationSources().set(listOfSources)
}
}

View file

@ -15,13 +15,13 @@ import eu.kanade.presentation.browse.components.MigrationMangaDialog
import eu.kanade.presentation.browse.components.MigrationProgressDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationBottomSheetDialog
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast
import exh.util.overEq
import exh.util.underEq
import kotlinx.collections.immutable.persistentListOf
import mihon.feature.migration.MigrateMangaConfigScreen
import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.i18n.sy.SYMR
@ -76,7 +76,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
val newStack = navigator.items.filter {
it !is MangaScreen &&
it !is MigrationListScreen &&
it !is PreMigrationScreen
it !is MigrateMangaConfigScreen
} + MangaScreen(mangaId)
navigator replaceAll newStack.first()
navigator.push(newStack.drop(1))
@ -113,14 +113,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
openMigrationOptionsDialog = screenModel::openMigrationOptionsDialog,
// KMK <--
searchManually = { migrationItem ->
val sources = screenModel.getMigrationSources()
val validSources = if (sources.size == 1) {
sources
} else {
sources.filter { it.id != migrationItem.manga.source }
}
val searchScreen = MigrateSearchScreen(migrationItem.manga.id, validSources.map { it.id })
navigator push searchScreen
navigator push MigrateSearchScreen(migrationItem.manga.id)
},
migrateNow = { screenModel.migrateManga(it, true) },
copyNow = { screenModel.migrateManga(it, false) },

View file

@ -11,7 +11,6 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigratingManga.SearchResult
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel.Companion.migrateMangaInternal
import eu.kanade.tachiyomi.util.system.toast
@ -130,9 +129,8 @@ class MigrationListScreenModel(
}
fun getSourceName(manga: Manga) = sourceManager.getOrStub(manga.source).getNameForMangaInfo()
fun getMigrationSources() = preferences.migrationSources().get().split("/").mapNotNull {
val value = it.toLongOrNull() ?: return@mapNotNull null
sourceManager.get(value) as? CatalogueSource
fun getMigrationSources() = preferences.migrationSources().get().mapNotNull {
sourceManager.get(it) as? CatalogueSource
}
private suspend fun runMigrations(mangas: List<MigratingManga>) {

View file

@ -1,8 +1,12 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationType
import java.io.Serializable
sealed class MigrationType : Serializable {
data class MangaList(val mangaIds: List<Long>) : MigrationType()
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
}
data class MigrationProcedureConfig(
var migration: MigrationType,
val extraSearchParams: String?,

View file

@ -8,18 +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.i18n.kmk.KMR
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data class MigrateMangaScreen(
private val sourceId: Long,
@ -42,31 +38,14 @@ 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(listOf(it.id))) },
onClickCover = { navigator.push(MangaScreen(it.id)) },
// KMK -->
onMultiMigrateClicked = {
if (state.selectionMode) {
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
state.selected.map { it.manga.id },
)
navigator.push(MigrateMangaConfigScreen(state.selected.map { it.manga.id }))
} else {
context.toast(KMR.strings.migrating_all_entries)
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
state.titles.map { it.manga.id },
)
navigator.push(MigrateMangaConfigScreen(state.titles.map { it.manga.id }))
}
},
onSelectAll = screenModel::toggleAllSelection,

View file

@ -14,16 +14,13 @@ import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
/**
* Manual search [validSources] for manga to migrate to.
*/
class MigrateSearchScreen(private val mangaId: Long, private val validSources: List<Long>) : Screen() {
class MigrateSearchScreen(private val mangaId: Long) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel =
rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId, validSources = validSources) }
rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
val state by screenModel.state.collectAsState()
val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) }

View file

@ -1,9 +1,10 @@
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
@ -13,15 +14,20 @@ 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)!!
@ -40,17 +46,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 }
}
}

View file

@ -57,7 +57,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 },

View file

@ -29,7 +29,6 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.library.DeleteLibraryMangaDialog
import eu.kanade.presentation.library.LibrarySettingsDialog
@ -47,7 +46,6 @@ import eu.kanade.tachiyomi.data.connections.discord.DiscordScreen
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen
@ -68,6 +66,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import mihon.feature.migration.MigrateMangaConfigScreen
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.category.model.Category
@ -87,6 +86,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
data object LibraryTab : Tab {
@Suppress("unused")
private fun readResolve(): Any = LibraryTab
override val options: TabOptions
@ -210,11 +210,7 @@ data object LibraryTab : Tab {
.map { it.id }
screenModel.clearSelection()
if (selectedMangaIds.isNotEmpty()) {
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
selectedMangaIds,
)
navigator.push(MigrateMangaConfigScreen(selectedMangaIds))
} else {
context.toast(SYMR.strings.no_valid_entry)
}

View file

@ -6,13 +6,10 @@ import androidx.compose.runtime.getValue
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.libraryUpdateError.LibraryUpdateErrorScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import mihon.feature.migration.MigrateMangaConfigScreen
class LibraryUpdateErrorScreen : Screen() {
@ -25,19 +22,11 @@ class LibraryUpdateErrorScreen : Screen() {
LibraryUpdateErrorScreen(
state = state,
onClick = { item ->
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
listOf(item.error.mangaId),
)
navigator.push(MigrateMangaConfigScreen(listOf(item.error.mangaId)))
},
onClickCover = { item -> navigator.push(MangaScreen(item.error.mangaId)) },
onMultiMigrateClicked = {
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
state.selected.map { it.error.mangaId },
)
navigator.push(MigrateMangaConfigScreen(state.selected.map { it.error.mangaId }))
},
onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection,

View file

@ -45,7 +45,6 @@ import dev.icerock.moko.resources.StringResource
import eu.kanade.core.util.ifSourcesLoaded
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.browse.components.BulkFavoriteDialogs
import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
@ -70,7 +69,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreen
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
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
@ -105,6 +103,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.launchIO
import tachiyomi.core.common.util.lang.launchUI
@ -367,13 +366,7 @@ class MangaScreen(
successState.manga.favorite
},
onMigrateClicked = {
// SY -->
PreMigrationScreen.navigateToMigration(
Injekt.get<SourcePreferences>().skipPreMigration().get(),
navigator,
listOfNotNull(successState.manga.id),
)
// SY <--
navigator.push(MigrateMangaConfigScreen(listOf(successState.manga.id)))
}.takeIf { successState.manga.favorite },
// SY -->
previewsRowCount = successState.previewsRowCount,

View file

@ -0,0 +1,528 @@
package mihon.feature.migration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
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.PushPin
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.SmallExtendedFloatingActionButton
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
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.AnimatedFloatingSearchBox
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.SOURCE_SEARCH_BOX_HEIGHT
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.MigrationBottomSheetDialog
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationType
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
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.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR
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.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.icons.FlagEmoji
import tachiyomi.presentation.core.util.shouldExpandFAB
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Screen that allows the user to configure which sources should be used when migrating manga.
*
* It displays available sources, supports searching and bulk selection, and opens a migration
* configuration bottom sheet before starting the actual migration flow for the selected sources.
*
* @param mangaIds IDs of the manga that will be migrated using the sources configured on this screen.
*/
class MigrateMangaConfigScreen(private val mangaIds: List<Long>) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { ScreenModel() }
// KMK -->
var searchQuery by remember { mutableStateOf("") }
BackHandler(enabled = searchQuery.isNotBlank()) {
searchQuery = ""
}
// KMK <--
val state by screenModel.state.collectAsState()
val (selectedSources, availableSources) = state.sources
// KMK -->
.filter { sources ->
if (searchQuery.isBlank()) return@filter true
val source = sources.source
searchQuery.split(",").any {
val input = it.trim()
if (input.isEmpty()) return@any false
source.name.contains(input, ignoreCase = true) ||
source.id == input.toLongOrNull()
}
}
// KMK <--
.partition { it.isSelected }
val showLanguage by remember(state) {
derivedStateOf {
state.sources.distinctBy { it.source.lang }.size > 1
}
}
val lazyListState = rememberLazyListState()
Scaffold(
topBar = {
AppBar(
title = stringResource(SYMR.strings.select_sources),
navigateUp = navigator::pop,
scrollBehavior = it,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.migrationConfigScreen_selectPinnedLabel),
icon = Icons.Outlined.PushPin,
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.Pinned) },
),
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_selectAllLabel),
onClick = { screenModel.toggleSelection(ScreenModel.SelectionConfig.All) },
),
),
)
},
)
},
floatingActionButton = {
// KMK -->
AnimatedVisibility(
visible = selectedSources.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut(),
content = {
// KMK <--
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
onClick = {
// KMK -->
screenModel.onMigrationSheet(true)
// KMK <--
},
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)
}
// KMK -->
Box(
modifier = Modifier.padding(contentPadding),
) {
val density = LocalDensity.current
var searchBoxHeight by remember { mutableStateOf(SOURCE_SEARCH_BOX_HEIGHT) }
// KMK <--
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
// KMK -->
contentPadding = PaddingValues(top = searchBoxHeight),
// KMK <--
) {
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) },
)
}
}
}
// KMK -->
AnimatedFloatingSearchBox(
listState = lazyListState,
searchQuery = searchQuery,
onChangeSearchQuery = { searchQuery = it ?: "" },
placeholderText = stringResource(KMR.strings.action_search_for_source),
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(
horizontal = MaterialTheme.padding.medium,
vertical = MaterialTheme.padding.small,
)
.align(Alignment.TopCenter),
onGloballyPositioned = { layoutCoordinates ->
searchBoxHeight = with(density) { layoutCoordinates.size.height.toDp() + 2 * MaterialTheme.padding.small }
},
)
// KMK <--
}
}
// KMK -->
val migrationSheetOpen by screenModel.migrationSheetOpen.collectAsState()
if (migrationSheetOpen) {
MigrationBottomSheetDialog(
onDismissRequest = { screenModel.onMigrationSheet(false) },
onStartMigration = { extraParam ->
screenModel.onMigrationSheet(false)
navigator.replace(
// KMK -->
MigrationListScreen(
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), extraParam),
),
// KMK <--
)
},
)
}
// KMK <--
}
@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 = FlagEmoji.getEmojiLangFlag(source.lang) + " (${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()) {
// KMK -->
private val pinnedSources by lazy { sourcePreferences.pinnedSources().get().mapNotNull { it.toLongOrNull() } }
private val disabledSources by lazy { sourcePreferences.disabledSources().get().mapNotNull { it.toLongOrNull() } }
// KMK <--
init {
screenModelScope.launchIO {
initSources()
}
}
// KMK -->
private val _migrationSheetOpen = MutableStateFlow(false)
val migrationSheetOpen = _migrationSheetOpen.asStateFlow()
fun onMigrationSheet(isOpen: Boolean) {
_migrationSheetOpen.value = isOpen
}
// KMK <--
private val sourcesComparator = { includedSources: /* KMK --> */ Map<Long, Int> /* KMK <-- */ ->
compareBy<MigrationSource>(
// KMK -->
// { !it.isSelected },
{ includedSources[it.source.id] ?: Int.MAX_VALUE },
// KMK <--
{ 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 }
// KMK -->
.mapIndexed { index, id -> id to index }.toMap()
// KMK <--
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 includedSources = sourcePreferences.migrationSources().get()
// KMK -->
.mapIndexed { index, id -> id to index }.toMap()
// KMK <--
val sources = sourceManager
// KMK -->
.getVisibleCatalogueSources()
// KMK <--
.asSequence()
.filterIsInstance<HttpSource>()
.filter { it.lang in languages }
// KMK -->
.sortedWith(compareBy { includedSources[it.id] ?: Int.MAX_VALUE })
// KMK <--
.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 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
}
}

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:backgroundTint="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?android:attr/colorBackground"
app:cardElevation="0dp"
app:cardForegroundColor="@color/draggable_card_foreground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/image"
android:layout_width="60dp"
android:layout_height="60dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/anim_browse_enter" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="0dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="8dp"
android:text="Title"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/reorder"
app:layout_constraintStart_toEndOf="@+id/image"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/reorder"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_drag_handle_24dp"
app:tint="?android:attr/textColorHint" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/migration_source_item" />

View file

@ -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,
@ -35,7 +47,7 @@ inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
deserializer = {
try {
enumValueOf(it)
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
defaultValue
}
},

View file

@ -108,7 +108,6 @@
<string name="action_scroll_to_bottom">التمرير إلى الأسفل</string>
<string name="action_scroll_to_previous">التمرير إلى السابق</string>
<string name="action_scroll_to_next">التمرير إلى التالي</string>
<string name="migrating_all_entries">ترحيل جميع المدخلات من المصدر</string>
<string name="label_to_be_updated">سيتم تحديثها</string>
<string name="pref_manga_info">معلومات المانجا</string>
<string name="manual_download">تنزيل يدوي</string>

View file

@ -134,7 +134,6 @@
<string name="action_sort_feed">ফিড ছৰ্ট কৰক</string>
<string name="sort_feed_confirmation">আপুনি ফিডবোৰ বৰ্ণানুক্ৰমে ছৰ্ট কৰিব বিচাৰে নেকি?</string>
<string name="current_">বৰ্তমান: %1$s</string>
<string name="migrating_all_entries">উৎসৰ পৰা সকলো এণ্ট্ৰি মাইগ্ৰেট কৰি আছে</string>
<string name="label_to_be_updated">আপডেট কৰিবলগীয়া</string>
<string name="sponsor_me">মোক স্পনছৰ কৰক</string>
<string name="batch_add_description">সমৰ্থন: MangaDex, E-H, ExH, nH, 8Muses, Tsumino</string>

View file

@ -171,7 +171,6 @@
<string name="sort_feed_confirmation">Would you like to sort the feeds alphabetically?</string>
<!-- Migration -->
<string name="current_">Current: %1$s</string>
<string name="migrating_all_entries">Migrating all entries from source</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>

View file

@ -102,7 +102,6 @@
<string name="action_scroll_to_bottom">Přejít na konec</string>
<string name="action_scroll_to_previous">Přejít na předchozí</string>
<string name="action_scroll_to_next">Přejít na další</string>
<string name="migrating_all_entries">Migrovat všechny záznamy ze zdroje</string>
<string name="label_to_be_updated">Bude aktualizováno</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Ruční stahování</string>

View file

@ -102,7 +102,6 @@
<string name="action_scroll_to_bottom">Rul til bunden</string>
<string name="action_scroll_to_previous">Rul til forrige</string>
<string name="action_scroll_to_next">Rul til næste</string>
<string name="migrating_all_entries">Migrerer alle poster fra kilde</string>
<string name="label_to_be_updated">Skal opdateres</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Manuel download</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">Nach unten scrollen</string>
<string name="action_scroll_to_previous">Zum Vorherigen scrollen</string>
<string name="action_scroll_to_next">Zum Nächsten scrollen</string>
<string name="migrating_all_entries">Alle Einträge aus der Quelle migrieren</string>
<string name="label_to_be_updated">Zu aktualisieren</string>
<string name="pref_manga_info">Manga-Info</string>
<string name="manual_download">Manueller Download</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">Κύλιση προς τα κάτω</string>
<string name="action_scroll_to_previous">Κύλιση στο προηγούμενο</string>
<string name="action_scroll_to_next">Κύλιση στο επόμενο</string>
<string name="migrating_all_entries">Μεταφορά όλων των καταχωρήσεων από την πηγή</string>
<string name="label_to_be_updated">Να ενημερωθεί</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Χειροκίνητη λήψη</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">Desplazar al final</string>
<string name="action_scroll_to_previous">Desplazar al anterior</string>
<string name="action_scroll_to_next">Desplazar al siguiente</string>
<string name="migrating_all_entries">Migrando todas las entradas desde la fuente</string>
<string name="label_to_be_updated">Para actualizar</string>
<string name="pref_manga_info">Información del manga</string>
<string name="manual_download">Descarga manual</string>

View file

@ -101,7 +101,6 @@
<string name="action_scroll_to_bottom">Siirry alareunaan</string>
<string name="action_scroll_to_previous">Vieritä edelliseen</string>
<string name="action_scroll_to_next">Vieritä seuraavaan</string>
<string name="migrating_all_entries">Siirretään kaikki merkinnät lähteestä</string>
<string name="label_to_be_updated">Päivittääksesi</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Manuaalinen lataus</string>

View file

@ -84,7 +84,6 @@
<string name="action_scroll_to_bottom">Mag-scroll paibaba</string>
<string name="action_scroll_to_previous">Mag-scroll sa nakaraan</string>
<string name="action_scroll_to_next">Mag-scroll sa susunod</string>
<string name="migrating_all_entries">Nililipat ang lahat ng entry mula sa source</string>
<string name="label_to_be_updated">Para ma-update</string>
<string name="pref_manga_info">Impo ng Manga</string>
<string name="manual_download">Mano-manong I-downloa</string>

View file

@ -101,7 +101,6 @@
<string name="action_scroll_to_bottom">Défiler vers le bas</string>
<string name="action_scroll_to_previous">Défiler vers le précédent</string>
<string name="action_scroll_to_next">Défiler vers le suivant</string>
<string name="migrating_all_entries">Migrer toutes les entrées de la source</string>
<string name="label_to_be_updated">A mettre à jour</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Téléchargement manuel</string>

View file

@ -146,7 +146,6 @@
<string name="action_scroll_to_previous">Gulir ke sebelumnya</string>
<string name="action_sort_feed">Urutkan feed</string>
<string name="sort_feed_confirmation">Apakah Anda ingin mengurutkan feed berdasarkan abjad?</string>
<string name="migrating_all_entries">Memigrasikan semua entri dari sumber</string>
<string name="job_failed_schedule_update_check">Gagal menjadwalkan pembaruan perpustakaan otomatis: %s</string>
<string name="job_failed_schedule_backup_check">Gagal menjadwalkan pencadangan otomatis: %s</string>
<string name="label_to_be_updated">Untuk diperbarui</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">Scorri verso il basso</string>
<string name="action_scroll_to_previous">Scorri al precedente</string>
<string name="action_scroll_to_next">Scorri al successivo</string>
<string name="migrating_all_entries">Migrazione di tutte le voci dalla sorgente</string>
<string name="label_to_be_updated">Da aggiornare</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Download manuale</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">一番下までスクロール</string>
<string name="action_scroll_to_previous">前までスクロール</string>
<string name="action_scroll_to_next">次までスクロール</string>
<string name="migrating_all_entries">すべての項目をソースから移行中</string>
<string name="label_to_be_updated">更新予定</string>
<string name="pref_manga_info">マンガ情報</string>
<string name="manual_download">手動でダウンロード</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">Scroll naar beneden</string>
<string name="action_scroll_to_previous">Scroll naar vorige</string>
<string name="action_scroll_to_next">Scroll naar volgende</string>
<string name="migrating_all_entries">Alle invoergegevens vanuit bron migreren</string>
<string name="label_to_be_updated">Te updaten</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Handmatige download</string>

View file

@ -101,7 +101,6 @@
<string name="action_scroll_to_bottom">Przewiń na dół</string>
<string name="action_scroll_to_previous">Przewiń do poprzedniego</string>
<string name="action_scroll_to_next">Przewiń do następnego</string>
<string name="migrating_all_entries">Migracja wszystkich wpisów ze źródła</string>
<string name="label_to_be_updated">Do aktualizacji</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Ręczne pobieranie</string>

View file

@ -105,7 +105,6 @@
<string name="action_scroll_to_bottom">Rolar para o final</string>
<string name="action_scroll_to_previous">Rolar para o anterior</string>
<string name="action_scroll_to_next">Rolar para o próximo</string>
<string name="migrating_all_entries">Migrando todas as entradas da fonte</string>
<string name="label_to_be_updated">A ser atualizado</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Download Manual</string>

View file

@ -105,7 +105,6 @@
<string name="action_scroll_to_bottom">Rolar para o final</string>
<string name="action_scroll_to_previous">Rolar para o anterior</string>
<string name="action_scroll_to_next">Rolar para o próximo</string>
<string name="migrating_all_entries">Migrando todas as entradas da fonte</string>
<string name="label_to_be_updated">A ser atualizado</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Download manual</string>

View file

@ -102,7 +102,6 @@
<string name="action_scroll_to_bottom">Derulează până jos</string>
<string name="action_scroll_to_previous">Derulează până la anterioară</string>
<string name="action_scroll_to_next">Derulează mai departe</string>
<string name="migrating_all_entries">Migrarea tuturor intrărilor de la sursă</string>
<string name="label_to_be_updated">A se actualiza</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Descărcare manuală</string>

View file

@ -106,7 +106,6 @@
<string name="action_scroll_to_bottom">Прокрутка вниз</string>
<string name="action_scroll_to_previous">Прокрутка до предыдущей</string>
<string name="action_scroll_to_next">Прокрутка до следующего</string>
<string name="migrating_all_entries">Миграция всех записей из источника</string>
<string name="label_to_be_updated">Будет обновлено</string>
<string name="pref_manga_info">Информация о манге</string>
<string name="manual_download">Ручная загрузка</string>

View file

@ -103,7 +103,6 @@
<string name="action_scroll_to_bottom">Rulla till botten</string>
<string name="action_scroll_to_previous">Bläddra till föregående</string>
<string name="action_scroll_to_next">Bläddra till nästa</string>
<string name="migrating_all_entries">Migrerar alla poster från källan</string>
<string name="label_to_be_updated">Att uppdateras</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Manuell nedladdning</string>

View file

@ -105,7 +105,6 @@
<string name="action_scroll_to_bottom">Прокрутити до кінця</string>
<string name="action_scroll_to_previous">Прокрутити до попереднього</string>
<string name="action_scroll_to_next">Перейти до наступного</string>
<string name="migrating_all_entries">Міграція всіх записів з джерела</string>
<string name="label_to_be_updated">Оновлювати</string>
<string name="pref_manga_info">Manga Info</string>
<string name="manual_download">Ручне завантаження</string>

View file

@ -109,7 +109,6 @@
<string name="action_scroll_to_bottom">Cuộn xuống dưới cùng</string>
<string name="action_scroll_to_previous">Nhảy tới nhóm trước</string>
<string name="action_scroll_to_next">Nhảy tới nhóm sau</string>
<string name="migrating_all_entries">Di chuyển tất cả truyện của nguồn</string>
<string name="label_to_be_updated">Truyện sẽ được cập nhật</string>
<string name="pref_manga_info">Trang thông tin truyện</string>
<string name="manual_download">Tải về thủ công</string>

View file

@ -109,7 +109,6 @@
<string name="action_scroll_to_bottom">滚动到底部</string>
<string name="action_scroll_to_previous">滚动到上一个</string>
<string name="action_scroll_to_next">滚动到下一个</string>
<string name="migrating_all_entries">从源代码迁移所有条目</string>
<string name="label_to_be_updated">待更新</string>
<string name="pref_manga_info">漫画信息</string>
<string name="manual_download">手动下载</string>

View file

@ -109,7 +109,6 @@
<string name="action_scroll_to_bottom">移至底部</string>
<string name="action_scroll_to_previous">移至上一類</string>
<string name="action_scroll_to_next">移至下一類</string>
<string name="migrating_all_entries">正在從來源遷移所有項目</string>
<string name="label_to_be_updated">待更新</string>
<string name="pref_manga_info">漫畫資訊</string>
<string name="manual_download">手動下載</string>

View file

@ -1007,4 +1007,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>