Range-selection for Migration (#466)

- Select a range of entries for migration
- Better bottom bar
- Show obsolete sources

---------

Co-authored-by: ImaginaryDesignation <108343184+ImaginaryDesignation@users.noreply.github.com>
This commit is contained in:
Cuong-Tran 2024-10-28 22:20:52 +07:00
parent 6b6e6a7fba
commit 467c39b899
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
10 changed files with 668 additions and 187 deletions

View file

@ -1,35 +1,124 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FindReplace
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material.icons.outlined.VerticalAlignBottom
import androidx.compose.material.icons.outlined.VerticalAlignTop
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.manga.components.BaseMangaListItem import eu.kanade.presentation.manga.components.BaseMangaListItem
import eu.kanade.presentation.manga.components.Button
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaItem
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun MigrateMangaScreen( fun MigrateMangaScreen(
navigateUp: () -> Unit, navigateUp: () -> Unit,
title: String?, title: String,
state: MigrateMangaScreenModel.State, state: MigrateMangaScreenModel.State,
onClickItem: (Manga) -> Unit, onClickItem: (MigrateMangaItem) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
// KMK -->
onMultiMigrateClicked: (() -> Unit),
onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit,
) { ) {
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val enableScrollToTop by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
val enableScrollToBottom by remember {
derivedStateOf {
listState.canScrollForward
}
}
// KMK <--
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
AppBar( // KMK -->
MigrateMangaAppBar(
title = title, title = title,
itemCnt = state.titles.size,
navigateUp = navigateUp, navigateUp = navigateUp,
selectedCount = state.selected.size,
onClickUnselectAll = { onSelectAll(false) },
onClickSelectAll = { onSelectAll(true) },
onClickInvertSelection = onInvertSelection,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
bottomBar = {
MigrateMangaBottomBar(
selected = state.selected,
onMultiMigrateClicked = onMultiMigrateClicked,
enableScrollToTop = enableScrollToTop,
enableScrollToBottom = enableScrollToBottom,
scrollToTop = {
scope.launch {
listState.scrollToItem(0)
}
},
scrollToBottom = {
scope.launch {
listState.scrollToItem(state.titles.size - 1)
}
},
// KMK <--
)
},
) { contentPadding -> ) { contentPadding ->
if (state.isEmpty) { if (state.isEmpty) {
EmptyScreen( EmptyScreen(
@ -44,6 +133,10 @@ fun MigrateMangaScreen(
state = state, state = state,
onClickItem = onClickItem, onClickItem = onClickItem,
onClickCover = onClickCover, onClickCover = onClickCover,
// KMK -->
onMangaSelected = onMangaSelected,
listState = listState,
// KMK <--
) )
} }
} }
@ -52,17 +145,36 @@ fun MigrateMangaScreen(
private fun MigrateMangaContent( private fun MigrateMangaContent(
contentPadding: PaddingValues, contentPadding: PaddingValues,
state: MigrateMangaScreenModel.State, state: MigrateMangaScreenModel.State,
onClickItem: (Manga) -> Unit, onClickItem: (MigrateMangaItem) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
// KMK -->
onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit,
listState: LazyListState,
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
state = listState,
) { ) {
items(state.titles) { manga -> // KMK <--
items(items = state.titles) {
MigrateMangaItem( MigrateMangaItem(
manga = manga, manga = it.manga,
onClickItem = onClickItem, onClickItem = {
onClickCover = onClickCover, // KMK -->
when {
state.selectionMode -> onMangaSelected(it, !it.selected, true, false)
// KMK <--
else -> onClickItem(it)
}
},
onClickCover = {
onClickCover(it.manga)
// KMK -->
}.takeIf { !state.selectionMode },
onLongClick = { onMangaSelected(it, !it.selected, true, true) },
selected = it.selected,
modifier = Modifier.animateItemFastScroll(),
// KMK <--
) )
} }
} }
@ -71,14 +183,159 @@ private fun MigrateMangaContent(
@Composable @Composable
private fun MigrateMangaItem( private fun MigrateMangaItem(
manga: Manga, manga: Manga,
onClickItem: (Manga) -> Unit, onClickItem: () -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (() -> Unit)?,
// KMK -->
onLongClick: () -> Unit,
selected: Boolean,
// KMK <--
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BaseMangaListItem( BaseMangaListItem(
modifier = modifier, modifier = modifier,
manga = manga, manga = manga,
onClickItem = { onClickItem(manga) }, onClickItem = onClickItem,
onClickCover = { onClickCover(manga) }, onClickCover = { onClickCover?.invoke() },
// KMK -->
onLongClick = onLongClick,
selected = selected,
// KMK <--
) )
} }
// KMK -->
@Composable
private fun MigrateMangaAppBar(
title: String,
itemCnt: Int,
navigateUp: () -> Unit,
selectedCount: Int,
onClickUnselectAll: () -> Unit,
onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
) {
AppBar(
title = title,
navigateUp = navigateUp,
actions = {
if (itemCnt > 0) {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onClickSelectAll,
),
),
)
}
},
actionModeCounter = selectedCount,
onCancelActionMode = onClickUnselectAll,
actionModeActions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onClickSelectAll,
),
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = onClickInvertSelection,
),
),
)
},
scrollBehavior = scrollBehavior,
)
}
@Composable
private fun MigrateMangaBottomBar(
modifier: Modifier = Modifier,
selected: List<MigrateMangaItem>,
onMultiMigrateClicked: (() -> Unit),
enableScrollToTop: Boolean,
enableScrollToBottom: Boolean,
scrollToTop: () -> Unit,
scrollToBottom: () -> Unit,
) {
val scope = rememberCoroutineScope()
val animatedElevation by animateDpAsState(
targetValue = if (selected.isNotEmpty()) 3.dp else 0.dp,
label = "elevation",
)
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large.copy(
bottomEnd = ZeroCornerSize,
bottomStart = ZeroCornerSize,
),
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = animatedElevation,
),
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 3).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(KMR.strings.action_scroll_to_top),
icon = Icons.Outlined.VerticalAlignTop,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = if (enableScrollToTop) {
scrollToTop
} else {
{}
},
enabled = enableScrollToTop,
)
Button(
title = stringResource(MR.strings.migrate),
icon = Icons.Outlined.FindReplace,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = if (selected.isNotEmpty()) {
onMultiMigrateClicked
} else {
{}
},
enabled = selected.isNotEmpty(),
)
Button(
title = stringResource(KMR.strings.action_scroll_to_bottom),
icon = Icons.Outlined.VerticalAlignBottom,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = if (enableScrollToBottom) {
scrollToBottom
} else {
{}
},
enabled = enableScrollToBottom,
)
}
}
}
// KMK <--

View file

@ -16,16 +16,15 @@ import androidx.compose.material.icons.outlined.Numbers
import androidx.compose.material.icons.outlined.SortByAlpha import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.installedExtension
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon import eu.kanade.presentation.browse.components.SourceIcon
import eu.kanade.presentation.components.AnimatedFloatingSearchBox import eu.kanade.presentation.components.AnimatedFloatingSearchBox
@ -57,9 +56,6 @@ fun MigrateSourceScreen(
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onToggleSortingDirection: () -> Unit, onToggleSortingDirection: () -> Unit,
onToggleSortingMode: () -> Unit, onToggleSortingMode: () -> Unit,
// SY -->
onClickAll: (Source) -> Unit,
// SY <--
// KMK --> // KMK -->
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
// KMK <-- // KMK <--
@ -87,9 +83,6 @@ fun MigrateSourceScreen(
onToggleSortingMode = onToggleSortingMode, onToggleSortingMode = onToggleSortingMode,
sortingDirection = state.sortingDirection, sortingDirection = state.sortingDirection,
onToggleSortingDirection = onToggleSortingDirection, onToggleSortingDirection = onToggleSortingDirection,
// SY -->
onClickAll = onClickAll,
// SY <--
// KMK --> // KMK -->
state = state, state = state,
onChangeSearchQuery = onChangeSearchQuery, onChangeSearchQuery = onChangeSearchQuery,
@ -108,9 +101,6 @@ private fun MigrateSourceList(
onToggleSortingMode: () -> Unit, onToggleSortingMode: () -> Unit,
sortingDirection: SetMigrateSorting.Direction, sortingDirection: SetMigrateSorting.Direction,
onToggleSortingDirection: () -> Unit, onToggleSortingDirection: () -> Unit,
// SY -->
onClickAll: (Source) -> Unit,
// SY <--
// KMK --> // KMK -->
state: MigrateSourceScreenModel.State, state: MigrateSourceScreenModel.State,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
@ -199,9 +189,6 @@ private fun MigrateSourceList(
count = count, count = count,
onClickItem = { onClickItem(source) }, onClickItem = { onClickItem(source) },
onLongClickItem = { onLongClickItem(source) }, onLongClickItem = { onLongClickItem(source) },
// SY -->
onClickAll = { onClickAll(source) },
// SY <--
) )
} }
} }
@ -214,9 +201,6 @@ private fun MigrateSourceItem(
count: Long, count: Long,
onClickItem: () -> Unit, onClickItem: () -> Unit,
onLongClickItem: () -> Unit, onLongClickItem: () -> Unit,
// SY -->
onClickAll: () -> Unit,
// SY <--
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BaseSourceItem( BaseSourceItem(
@ -230,16 +214,6 @@ private fun MigrateSourceItem(
BadgeGroup { BadgeGroup {
Badge(text = "$count") Badge(text = "$count")
} }
// SY -->
TextButton(onClick = onClickAll) {
Text(
text = stringResource(MR.strings.all),
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.primary,
),
)
}
// SY <--
}, },
content = { _, sourceLangString, /* KMK --> */ lang /* KMK <-- */ -> content = { _, sourceLangString, /* KMK --> */ lang /* KMK <-- */ ->
Column( Column(
@ -276,6 +250,17 @@ private fun MigrateSourceItem(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
) )
// KMK -->
} else if (source.installedExtension?.isObsolete == true) {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = stringResource(MR.strings.ext_obsolete),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
// KMK <--
} }
} }
} }

View file

@ -1,26 +1,14 @@
package eu.kanade.presentation.libraryUpdateError package eu.kanade.presentation.libraryUpdateError
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.animateColorAsState
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.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -31,12 +19,10 @@ import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material.icons.outlined.VerticalAlignBottom import androidx.compose.material.icons.outlined.VerticalAlignBottom
import androidx.compose.material.icons.outlined.VerticalAlignTop import androidx.compose.material.icons.outlined.VerticalAlignTop
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.ripple import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -45,16 +31,14 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
import eu.kanade.presentation.manga.components.Button
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -74,7 +58,6 @@ import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun LibraryUpdateErrorScreen( fun LibraryUpdateErrorScreen(
state: LibraryUpdateErrorScreenState, state: LibraryUpdateErrorScreenState,
modifier: Modifier = Modifier,
onClick: (LibraryUpdateErrorItem) -> Unit, onClick: (LibraryUpdateErrorItem) -> Unit,
onClickCover: (LibraryUpdateErrorItem) -> Unit, onClickCover: (LibraryUpdateErrorItem) -> Unit,
onMultiMigrateClicked: (() -> Unit), onMultiMigrateClicked: (() -> Unit),
@ -83,8 +66,15 @@ fun LibraryUpdateErrorScreen(
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit, onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
val listState = rememberLazyListState() BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val headerIndexes = remember { mutableStateOf<List<Int>>(emptyList()) }
LaunchedEffect(state) {
headerIndexes.value = state.getHeaderIndexes()
}
val enableScrollToTop by remember { val enableScrollToTop by remember {
derivedStateOf { derivedStateOf {
@ -98,10 +88,6 @@ fun LibraryUpdateErrorScreen(
} }
} }
val headerIndexes = remember { mutableStateOf<List<Int>>(emptyList()) }
LaunchedEffect(state) {
headerIndexes.value = state.getHeaderIndexes()
}
val enableScrollToPrevious by remember { val enableScrollToPrevious by remember {
derivedStateOf { derivedStateOf {
headerIndexes.value.any { it < listState.firstVisibleItemIndex } headerIndexes.value.any { it < listState.firstVisibleItemIndex }
@ -113,11 +99,9 @@ fun LibraryUpdateErrorScreen(
} }
} }
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
LibraryUpdateErrorsAppBar( LibraryUpdateErrorAppBar(
title = stringResource( title = stringResource(
KMR.strings.label_library_update_errors, KMR.strings.label_library_update_errors,
state.items.size, state.items.size,
@ -132,8 +116,7 @@ fun LibraryUpdateErrorScreen(
) )
}, },
bottomBar = { bottomBar = {
LibraryUpdateErrorsBottomBar( LibraryUpdateErrorBottomBar(
modifier = modifier,
selected = state.selected, selected = state.selected,
onMultiMigrateClicked = onMultiMigrateClicked, onMultiMigrateClicked = onMultiMigrateClicked,
enableScrollToTop = enableScrollToTop, enableScrollToTop = enableScrollToTop,
@ -198,7 +181,7 @@ fun LibraryUpdateErrorScreen(
} }
@Composable @Composable
private fun LibraryUpdateErrorsBottomBar( private fun LibraryUpdateErrorBottomBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selected: List<LibraryUpdateErrorItem>, selected: List<LibraryUpdateErrorItem>,
onMultiMigrateClicked: (() -> Unit), onMultiMigrateClicked: (() -> Unit),
@ -212,13 +195,19 @@ private fun LibraryUpdateErrorsBottomBar(
scrollToNext: () -> Unit, scrollToNext: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val animatedElevation by animateDpAsState(
targetValue = if (selected.isNotEmpty()) 3.dp else 0.dp,
label = "elevation",
)
Surface( Surface(
modifier = modifier, modifier = modifier,
shape = MaterialTheme.shapes.large.copy( shape = MaterialTheme.shapes.large.copy(
bottomEnd = ZeroCornerSize, bottomEnd = ZeroCornerSize,
bottomStart = ZeroCornerSize, bottomStart = ZeroCornerSize,
), ),
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = animatedElevation,
),
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false) } val confirm = remember { mutableStateListOf(false, false, false, false, false) }
@ -306,62 +295,7 @@ private fun LibraryUpdateErrorsBottomBar(
} }
@Composable @Composable
private fun RowScope.Button( private fun LibraryUpdateErrorAppBar(
title: String,
icon: ImageVector,
toConfirm: Boolean,
enabled: Boolean,
onLongClick: () -> Unit,
onClick: (() -> Unit),
content: (@Composable () -> Unit)? = null,
) {
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
val animatedColor by animateColorAsState(
if (enabled) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.38f,
)
},
)
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,
tint = animatedColor,
)
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
Text(
text = title,
overflow = TextOverflow.Visible,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
color = animatedColor,
)
}
content?.invoke()
}
}
@Composable
private fun LibraryUpdateErrorsAppBar(
title: String, title: String,
itemCnt: Int, itemCnt: Int,
navigateUp: () -> Unit, navigateUp: () -> Unit,

View file

@ -1,6 +1,6 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
@ -12,10 +12,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.selectedBackground
@Composable @Composable
fun BaseMangaListItem( fun BaseMangaListItem(
@ -23,13 +26,31 @@ fun BaseMangaListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClickItem: () -> Unit = {}, onClickItem: () -> Unit = {},
onClickCover: () -> Unit = onClickItem, onClickCover: () -> Unit = onClickItem,
// KMK -->
onLongClick: () -> Unit = onClickItem,
selected: Boolean,
// KMK <--
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) }, cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
content: @Composable RowScope.() -> Unit = { defaultContent(manga) }, content: @Composable RowScope.() -> Unit = { defaultContent(manga) },
) { ) {
// KMK -->
val haptic = LocalHapticFeedback.current
// KMK <--
Row( Row(
modifier = modifier modifier = modifier
.clickable(onClick = onClickItem) // KMK -->
.selectedBackground(selected)
.combinedClickable(
// KMK <--
onClick = onClickItem,
// KMK -->
onLongClick = {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
// KMK <--
)
.height(56.dp) .height(56.dp)
.padding(horizontal = MaterialTheme.padding.medium), .padding(horizontal = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
@ -182,18 +183,33 @@ fun MangaBottomActionMenu(
} }
@Composable @Composable
private fun RowScope.Button( internal fun RowScope.Button(
title: String, title: String,
icon: ImageVector, icon: ImageVector,
toConfirm: Boolean, toConfirm: Boolean,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
// KMK -->
enabled: Boolean = true,
// KMK <--
content: (@Composable () -> Unit)? = null, content: (@Composable () -> Unit)? = null,
) { ) {
val animatedWeight by animateFloatAsState( val animatedWeight by animateFloatAsState(
targetValue = if (toConfirm) 2f else 1f, targetValue = if (toConfirm) 2f else 1f,
label = "weight", label = "weight",
) )
// KMK -->
val animatedColor by animateColorAsState(
if (enabled) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.38f,
)
},
label = "color",
)
// KMK <--
Column( Column(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
@ -210,6 +226,9 @@ private fun RowScope.Button(
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = title, contentDescription = title,
// KMK -->
tint = animatedColor,
// KMK <--
) )
AnimatedVisibility( AnimatedVisibility(
visible = toConfirm, visible = toConfirm,
@ -221,6 +240,9 @@ private fun RowScope.Button(
overflow = TextOverflow.Visible, overflow = TextOverflow.Visible,
maxLines = 1, maxLines = 1,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
// KMK -->
color = animatedColor,
// KMK <--
) )
} }
content?.invoke() content?.invoke()

View file

@ -2,36 +2,69 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.activity.compose.BackHandler 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.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.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.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Deselect 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.SelectAll
import androidx.compose.material.icons.outlined.ToggleOn
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset 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.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection 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.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@ -41,13 +74,15 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.SourcesSearchBox import eu.kanade.presentation.components.SourcesSearchBox
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding 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.MigrationListScreen
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
@ -57,6 +92,7 @@ import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.io.Serializable import java.io.Serializable
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.seconds
sealed class MigrationType : Serializable { sealed class MigrationType : Serializable {
data class MangaList(val mangaIds: List<Long>) : MigrationType() data class MangaList(val mangaIds: List<Long>) : MigrationType()
@ -125,32 +161,19 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
title = stringResource(SYMR.strings.select_sources), title = stringResource(SYMR.strings.select_sources),
navigateUp = navigator::pop, navigateUp = navigator::pop,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(SYMR.strings.select_none),
icon = Icons.Outlined.Deselect,
onClick = { screenModel.massSelect(false) },
),
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = { screenModel.massSelect(true) },
),
AppBar.OverflowAction(
title = stringResource(SYMR.strings.match_enabled_sources),
onClick = { screenModel.matchSelection(true) },
),
AppBar.OverflowAction(
title = stringResource(SYMR.strings.match_pinned_sources),
onClick = { screenModel.matchSelection(false) },
),
),
)
},
) )
}, },
// KMK -->
bottomBar = {
PreMigrationScreenBottomBar(
modifier = Modifier,
onSelectAll = { screenModel.massSelect(true) },
onSelectNone = { screenModel.massSelect(false) },
onSelectPinned = { screenModel.matchSelection(false) },
onSelectEnabled = { screenModel.matchSelection(true) },
)
},
// KMK <--
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_migrate)) }, text = { Text(text = stringResource(MR.strings.action_migrate)) },
@ -234,6 +257,124 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
} }
} }
// 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 { companion object {
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) { fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) {
navigator.push( navigator.push(

View file

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -39,18 +40,39 @@ data class MigrateMangaScreen(
MigrateMangaScreen( MigrateMangaScreen(
navigateUp = navigator::pop, navigateUp = navigator::pop,
title = state.source!!.name, title = state.source?.name ?: "???",
state = state, state = state,
onClickItem = { onClickItem = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
listOf(it.id), listOf(it.manga.id),
) )
// SY <-- // SY <--
}, },
onClickCover = { navigator.push(MangaScreen(it.id)) }, onClickCover = { navigator.push(MangaScreen(it.id)) },
// KMK -->
onMultiMigrateClicked = {
if (state.selectionMode) {
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
state.selected.map { it.manga.id },
)
} else {
context.toast(KMR.strings.migrating_all_entries)
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
state.titles.map { it.manga.id },
)
}
},
onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection,
onMangaSelected = screenModel::toggleSelection,
// KMK <--
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.addOrRemove
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -32,6 +33,12 @@ class MigrateMangaScreenModel(
private val _events: Channel<MigrationMangaEvent> = Channel() private val _events: Channel<MigrationMangaEvent> = Channel()
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow() val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
// KMK -->
// First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
private val selectedMangaIds: HashSet<Long> = HashSet()
// KMK <--
init { init {
screenModelScope.launch { screenModelScope.launch {
mutableState.update { state -> mutableState.update { state ->
@ -46,9 +53,14 @@ class MigrateMangaScreenModel(
state.copy(titleList = persistentListOf()) state.copy(titleList = persistentListOf())
} }
} }
// KMK -->
.map { manga ->
toMigrationMangaScreenItems(manga)
}
// KMK <--
.map { manga -> .map { manga ->
manga manga
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }) .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.manga.title })
.toImmutableList() .toImmutableList()
} }
.collectLatest { list -> .collectLatest { list ->
@ -57,13 +69,117 @@ class MigrateMangaScreenModel(
} }
} }
// KMK -->
private fun toMigrationMangaScreenItems(mangas: List<Manga>): List<MigrateMangaItem> {
return mangas.map { manga ->
MigrateMangaItem(
manga = manga,
selected = manga.id in selectedMangaIds,
)
}
}
fun toggleSelection(
item: MigrateMangaItem,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
mutableState.update { state ->
val newItems = state.titles.toMutableList().apply {
val selectedIndex = indexOfFirst { it.manga.id == item.manga.id }
if (selectedIndex < 0) return@apply
val selectedItem = get(selectedIndex)
if (selectedItem.selected == selected) return@apply
val firstSelection = none { it.selected }
set(selectedIndex, selectedItem.copy(selected = selected))
selectedMangaIds.addOrRemove(item.manga.id, selected)
if (selected && userSelected && fromLongPress) {
if (firstSelection) {
selectedPositions[0] = selectedIndex
selectedPositions[1] = selectedIndex
} else {
// Try to select the items in-between when possible
val range: IntRange
if (selectedIndex < selectedPositions[0]) {
range = selectedIndex + 1 until selectedPositions[0]
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
range = (selectedPositions[1] + 1) until selectedIndex
selectedPositions[1] = selectedIndex
} else {
// Just select itself
range = IntRange.EMPTY
}
range.forEach {
val inBetweenItem = get(it)
if (!inBetweenItem.selected) {
selectedMangaIds.add(inBetweenItem.manga.id)
set(it, inBetweenItem.copy(selected = true))
}
}
}
} else if (userSelected && !fromLongPress) {
if (!selected) {
if (selectedIndex == selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.selected }
} else if (selectedIndex == selectedPositions[1]) {
selectedPositions[1] = indexOfLast { it.selected }
}
} else {
if (selectedIndex < selectedPositions[0]) {
selectedPositions[0] = selectedIndex
} else if (selectedIndex > selectedPositions[1]) {
selectedPositions[1] = selectedIndex
}
}
}
}
state.copy(titleList = newItems.toImmutableList())
}
}
fun toggleAllSelection(selected: Boolean) {
mutableState.update { state ->
val newItems = state.titles.map {
selectedMangaIds.addOrRemove(it.manga.id, selected)
it.copy(selected = selected)
}
state.copy(titleList = newItems.toImmutableList())
}
selectedPositions[0] = -1
selectedPositions[1] = -1
}
fun invertSelection() {
mutableState.update { state ->
val newItems = state.titles.map {
selectedMangaIds.addOrRemove(it.manga.id, !it.selected)
it.copy(selected = !it.selected)
}
state.copy(titleList = newItems.toImmutableList())
}
selectedPositions[0] = -1
selectedPositions[1] = -1
}
// KMK <--
@Immutable @Immutable
data class State( data class State(
val source: Source? = null, val source: Source? = null,
private val titleList: ImmutableList<Manga>? = null, private val titleList: ImmutableList<MigrateMangaItem>? = null,
) { ) {
// KMK -->
val selected = titles.filter { it.selected }
val selectionMode = selected.isNotEmpty()
// KMK <--
val titles: ImmutableList<Manga> val titles: ImmutableList<MigrateMangaItem>
get() = titleList ?: persistentListOf() get() = titleList ?: persistentListOf()
val isLoading: Boolean val isLoading: Boolean
@ -77,3 +193,11 @@ class MigrateMangaScreenModel(
sealed interface MigrationMangaEvent { sealed interface MigrationMangaEvent {
data object FailedFetchingFavorites : MigrationMangaEvent data object FailedFetchingFavorites : MigrationMangaEvent
} }
// KMK -->
@Immutable
data class MigrateMangaItem(
val manga: Manga,
val selected: Boolean,
)
// KMK <--

View file

@ -13,18 +13,10 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrateSourceScreen import eu.kanade.presentation.browse.MigrateSourceScreen
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent import eu.kanade.presentation.components.TabContent
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreen import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreen
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.DelicateCoroutinesApi
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun Screen.migrateSourceTab(): TabContent { fun Screen.migrateSourceTab(): TabContent {
@ -53,24 +45,6 @@ fun Screen.migrateSourceTab(): TabContent {
}, },
onToggleSortingDirection = screenModel::toggleSortingDirection, onToggleSortingDirection = screenModel::toggleSortingDirection,
onToggleSortingMode = screenModel::toggleSortingMode, onToggleSortingMode = screenModel::toggleSortingMode,
// SY -->
onClickAll = { source ->
// TODO: Jay wtf, need to clean this up sometime
@OptIn(DelicateCoroutinesApi::class)
launchIO {
val manga = Injekt.get<GetFavorites>().await()
val sourceMangas =
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
withUIContext {
PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator,
sourceMangas,
)
}
}
},
// SY <--
// KMK --> // KMK -->
onChangeSearchQuery = screenModel::search, onChangeSearchQuery = screenModel::search,
// KMK <-- // KMK <--

View file

@ -151,6 +151,7 @@
<!-- Migration --> <!-- Migration -->
<string name="current_">Current: %1$s</string> <string name="current_">Current: %1$s</string>
<string name="migrating_all_entries">Migrating all entries from source</string>
<!-- Misc --> <!-- Misc -->
<string name="label_to_be_updated">To be updated</string> <string name="label_to_be_updated">To be updated</string>