refactor: clean up browse/migrate (#1414)

* Support mass migration in 'Browse -> Migrate' (mihonapp/mihon#2338)

(cherry picked from commit 22f851173b1eca242645f328a46e6038c035d5ec)

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
Cuong-Tran 2025-12-22 12:00:00 +07:00
parent 9cbd46947b
commit a86ec40c79
7 changed files with 334 additions and 371 deletions

View file

@ -1,339 +0,0 @@
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.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.lazy.LazyListState
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.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.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.AppBarActions
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 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.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import kotlin.time.Duration.Companion.seconds
@Composable
fun MigrateMangaScreen(
navigateUp: () -> Unit,
title: String,
state: MigrateMangaScreenModel.State,
onClickItem: (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(
topBar = { scrollBehavior ->
// KMK -->
MigrateMangaAppBar(
title = title,
itemCnt = state.titles.size,
navigateUp = navigateUp,
selectedCount = state.selected.size,
onClickUnselectAll = { onSelectAll(false) },
onClickSelectAll = { onSelectAll(true) },
onClickInvertSelection = onInvertSelection,
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 ->
if (state.isEmpty) {
EmptyScreen(
stringRes = MR.strings.empty_screen,
modifier = Modifier.padding(contentPadding),
)
return@Scaffold
}
MigrateMangaContent(
contentPadding = contentPadding,
state = state,
onClickItem = onClickItem,
onClickCover = onClickCover,
// KMK -->
onMangaSelected = onMangaSelected,
listState = listState,
// KMK <--
)
}
}
@Composable
private fun MigrateMangaContent(
contentPadding: PaddingValues,
state: MigrateMangaScreenModel.State,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
// KMK -->
onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit,
listState: LazyListState,
) {
FastScrollLazyColumn(
contentPadding = contentPadding,
state = listState,
) {
// KMK <--
items(items = state.titles) { item ->
MigrateMangaItem(
manga = item.manga,
onClickItem = {
// KMK -->
when {
state.selectionMode -> onMangaSelected(item, !item.selected, true, false)
// KMK <--
else -> onClickItem(it)
}
},
onClickCover = onClickCover,
// KMK -->
onLongClick = { onMangaSelected(item, !item.selected, true, true) },
selected = item.selected,
modifier = Modifier.animateItemFastScroll(),
// KMK <--
)
}
}
}
@Composable
private fun MigrateMangaItem(
manga: Manga,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
// KMK -->
onLongClick: () -> Unit,
selected: Boolean,
// KMK <--
modifier: Modifier = Modifier,
) {
BaseMangaListItem(
modifier = modifier,
manga = manga,
onClickItem = { onClickItem(manga) },
onClickCover = { onClickCover(manga) },
// 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

@ -61,7 +61,7 @@ fun LibraryUpdateErrorScreen(
state: LibraryUpdateErrorScreenState,
onClick: (LibraryUpdateErrorItem) -> Unit,
onClickCover: (LibraryUpdateErrorItem) -> Unit,
onMultiMigrateClicked: (() -> Unit),
onMultiMigrateClicked: () -> Unit,
onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
onErrorsDelete: () -> Unit,
@ -189,7 +189,7 @@ fun LibraryUpdateErrorScreen(
private fun LibraryUpdateErrorBottomBar(
modifier: Modifier = Modifier,
selected: List<LibraryUpdateErrorItem>,
onMultiMigrateClicked: (() -> Unit),
onMultiMigrateClicked: () -> Unit,
enableScrollToTop: Boolean,
enableScrollToBottom: Boolean,
scrollToTop: () -> Unit,
@ -219,7 +219,7 @@ private fun LibraryUpdateErrorBottomBar(
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 5).forEach { i -> confirm[i] = i == toConfirmIndex }
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)

View file

@ -52,7 +52,7 @@ fun ChapterSettingsDialog(
onUnreadFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
onScanlatorFilterClicked: () -> Unit,
onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
@ -138,7 +138,7 @@ private fun ColumnScope.FilterPage(
bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit),
onScanlatorFilterClicked: () -> Unit,
) {
TriStateItem(
label = stringResource(MR.strings.label_downloaded),

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.selectedBackground
@Composable
fun BaseMangaListItem(
@ -28,7 +27,6 @@ fun BaseMangaListItem(
onClickCover: () -> Unit = onClickItem,
// KMK -->
onLongClick: () -> Unit = onClickItem,
selected: Boolean,
// KMK <--
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
actions: @Composable RowScope.() -> Unit = {},
@ -40,7 +38,6 @@ fun BaseMangaListItem(
Row(
modifier = modifier
// KMK -->
.selectedBackground(selected)
.combinedClickable(
// KMK <--
onClick = onClickItem,

View file

@ -100,7 +100,7 @@ fun MangaBottomActionMenu(
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
@ -288,11 +288,11 @@ fun LibraryBottomActionMenu(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false, false /* SY --> */, false /* SY <-- */) }
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..<confirm.size).forEach { i -> confirm[i] = i == toConfirmIndex }
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)

View file

@ -1,21 +1,70 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateDpAsState
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.lazy.LazyListState
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrateMangaScreen
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.Button
import eu.kanade.presentation.util.Screen
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import mihon.feature.migration.config.MigrationConfigScreen
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.selectedBackground
import kotlin.time.Duration.Companion.seconds
data class MigrateMangaScreen(
private val sourceId: Long,
@ -34,25 +83,90 @@ data class MigrateMangaScreen(
return
}
MigrateMangaScreen(
navigateUp = navigator::pop,
title = state.source?.name ?: "???",
state = state,
onClickItem = { navigator.push(MigrationConfigScreen(it.id)) },
onClickCover = { navigator.push(MangaScreen(it.id)) },
// KMK -->
onMultiMigrateClicked = {
if (state.selectionMode) {
navigator.push(MigrationConfigScreen(state.selected.map { it.manga.id }))
} else {
navigator.push(MigrationConfigScreen(state.titles.map { it.manga.id }))
}
BackHandler(enabled = state.selectionMode) {
screenModel.clearSelection()
}
val lazyListState = rememberLazyListState()
// KMK -->
val scope = rememberCoroutineScope()
val enableScrollToTop by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex > 0
}
}
val enableScrollToBottom by remember {
derivedStateOf {
lazyListState.canScrollForward
}
}
// KMK <--
Scaffold(
topBar = { scrollBehavior ->
// KMK -->
MigrateMangaAppBar(
// KMK <--
title = state.source!!.name,
navigateUp = {
// KMK -->
navigator.pop()
},
itemCnt = state.titles.size,
selectedCount = state.selection.size,
onClickUnselectAll = screenModel::clearSelection,
onClickSelectAll = screenModel::toggleAllSelection,
onClickInvertSelection = screenModel::invertSelection,
// KMK <--
scrollBehavior = scrollBehavior,
)
},
// KMK -->
bottomBar = {
MigrateMangaBottomBar(
selected = state.selection,
onMultiMigrateClicked = {
val selection = state.selection
.map { it.manga.id }
navigator.push(MigrationConfigScreen(selection))
},
enableScrollToTop = enableScrollToTop,
enableScrollToBottom = enableScrollToBottom,
scrollToTop = {
scope.launch {
lazyListState.scrollToItem(0)
}
},
scrollToBottom = {
scope.launch {
lazyListState.scrollToItem(state.titles.size - 1)
}
},
)
},
onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection,
onMangaSelected = screenModel::toggleSelection,
// KMK <--
)
) { contentPadding ->
if (state.isEmpty) {
EmptyScreen(
stringRes = MR.strings.empty_screen,
modifier = Modifier.padding(contentPadding),
)
return@Scaffold
}
MigrateMangaContent(
lazyListState = lazyListState,
contentPadding = contentPadding,
state = state,
// KMK -->
onMangaSelected = screenModel::toggleSelection,
onClickItem = { navigator.push(MigrationConfigScreen(it.id)) },
// KMK <--
onClickCover = { navigator.push(MangaScreen(it.id)) },
)
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { event ->
@ -64,4 +178,188 @@ data class MigrateMangaScreen(
}
}
}
@Composable
private fun MigrateMangaContent(
lazyListState: LazyListState,
contentPadding: PaddingValues,
state: MigrateMangaScreenModel.State,
// KMK -->
onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit,
// KMK <--
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
) {
FastScrollLazyColumn(
state = lazyListState,
contentPadding = contentPadding,
) {
items(items = state.titles) { manga ->
MigrateMangaItem(
manga = manga.manga,
isSelected = manga.selected,
onClickItem = {
// KMK -->
when {
state.selectionMode -> onMangaSelected(manga, !manga.selected, true, false)
// KMK <--
else -> onClickItem(it)
}
},
onClickCover = onClickCover,
// KMK -->
onLongClick = { onMangaSelected(manga, !manga.selected, true, true) },
modifier = Modifier.animateItemFastScroll(),
// KMK <--
)
}
}
}
@Composable
private fun MigrateMangaItem(
manga: Manga,
isSelected: Boolean,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
// KMK -->
onLongClick: () -> Unit,
// KMK <--
modifier: Modifier = Modifier,
) {
BaseMangaListItem(
modifier = modifier.selectedBackground(isSelected),
manga = manga,
onClickItem = { onClickItem(manga) },
onClickCover = { onClickCover(manga) },
// KMK -->
onLongClick = onLongClick,
// KMK <--
)
}
// KMK -->
@Composable
private fun MigrateMangaAppBar(
title: String,
navigateUp: () -> Unit,
itemCnt: Int,
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)
confirm.indices.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 = scrollToTop,
enabled = enableScrollToTop,
)
Button(
title = stringResource(MR.strings.migrate),
icon = Icons.Outlined.FindReplace,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onMultiMigrateClicked,
enabled = selected.isNotEmpty(),
)
Button(
title = stringResource(KMR.strings.action_scroll_to_bottom),
icon = Icons.Outlined.VerticalAlignBottom,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = scrollToBottom,
enabled = enableScrollToBottom,
)
}
}
}
// KMK <--
}

View file

@ -143,7 +143,7 @@ class MigrateMangaScreenModel(
}
}
fun toggleAllSelection(selected: Boolean) {
fun toggleAllSelection(selected: Boolean = true) {
mutableState.update { state ->
val newItems = state.titles.map {
selectedMangaIds.addOrRemove(it.manga.id, selected)
@ -169,14 +169,19 @@ class MigrateMangaScreenModel(
}
// KMK <--
fun clearSelection() {
// KMK -->
toggleAllSelection(false)
// KMK <--
}
@Immutable
data class State(
val source: Source? = null,
private val titleList: ImmutableList<MigrateMangaItem>? = null,
) {
// KMK -->
val selected = titles.filter { it.selected }
val selectionMode = selected.isNotEmpty()
val selection = titles.filter { it.selected }
// KMK <--
val titles: ImmutableList<MigrateMangaItem>
@ -187,6 +192,8 @@ class MigrateMangaScreenModel(
val isEmpty: Boolean
get() = titles.isEmpty()
val selectionMode = selection.isNotEmpty()
}
}