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

View file

@ -52,7 +52,7 @@ fun ChapterSettingsDialog(
onUnreadFilterChanged: (TriState) -> Unit, onUnreadFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean, scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit), onScanlatorFilterClicked: () -> Unit,
onSortModeChanged: (Long) -> Unit, onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit, onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingManga: Boolean) -> Unit, onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
@ -138,7 +138,7 @@ private fun ColumnScope.FilterPage(
bookmarkedFilter: TriState, bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit, onBookmarkedFilterChanged: (TriState) -> Unit,
scanlatorFilterActive: Boolean, scanlatorFilterActive: Boolean,
onScanlatorFilterClicked: (() -> Unit), onScanlatorFilterClicked: () -> Unit,
) { ) {
TriStateItem( TriStateItem(
label = stringResource(MR.strings.label_downloaded), 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 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(
@ -28,7 +27,6 @@ fun BaseMangaListItem(
onClickCover: () -> Unit = onClickItem, onClickCover: () -> Unit = onClickItem,
// KMK --> // KMK -->
onLongClick: () -> Unit = onClickItem, onLongClick: () -> Unit = onClickItem,
selected: Boolean,
// KMK <-- // KMK <--
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) }, cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
@ -40,7 +38,6 @@ fun BaseMangaListItem(
Row( Row(
modifier = modifier modifier = modifier
// KMK --> // KMK -->
.selectedBackground(selected)
.combinedClickable( .combinedClickable(
// KMK <-- // KMK <--
onClick = onClickItem, onClick = onClickItem,

View file

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

View file

@ -1,21 +1,70 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga 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.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.derivedStateOf
import androidx.compose.runtime.getValue 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.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.MigrateMangaScreen import eu.kanade.presentation.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.Screen
import eu.kanade.presentation.util.animateItemFastScroll
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import mihon.feature.migration.config.MigrationConfigScreen import mihon.feature.migration.config.MigrationConfigScreen
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.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.selectedBackground
import kotlin.time.Duration.Companion.seconds
data class MigrateMangaScreen( data class MigrateMangaScreen(
private val sourceId: Long, private val sourceId: Long,
@ -34,25 +83,90 @@ data class MigrateMangaScreen(
return return
} }
MigrateMangaScreen( BackHandler(enabled = state.selectionMode) {
navigateUp = navigator::pop, screenModel.clearSelection()
title = state.source?.name ?: "???", }
state = state,
onClickItem = { navigator.push(MigrationConfigScreen(it.id)) }, val lazyListState = rememberLazyListState()
onClickCover = { navigator.push(MangaScreen(it.id)) },
// KMK --> // KMK -->
onMultiMigrateClicked = { val scope = rememberCoroutineScope()
if (state.selectionMode) { val enableScrollToTop by remember {
navigator.push(MigrationConfigScreen(state.selected.map { it.manga.id })) derivedStateOf {
} else { lazyListState.firstVisibleItemIndex > 0
navigator.push(MigrationConfigScreen(state.titles.map { it.manga.id })) }
} }
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 <-- // 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) { LaunchedEffect(Unit) {
screenModel.events.collectLatest { event -> 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 -> mutableState.update { state ->
val newItems = state.titles.map { val newItems = state.titles.map {
selectedMangaIds.addOrRemove(it.manga.id, selected) selectedMangaIds.addOrRemove(it.manga.id, selected)
@ -169,14 +169,19 @@ class MigrateMangaScreenModel(
} }
// KMK <-- // KMK <--
fun clearSelection() {
// KMK -->
toggleAllSelection(false)
// KMK <--
}
@Immutable @Immutable
data class State( data class State(
val source: Source? = null, val source: Source? = null,
private val titleList: ImmutableList<MigrateMangaItem>? = null, private val titleList: ImmutableList<MigrateMangaItem>? = null,
) { ) {
// KMK --> // KMK -->
val selected = titles.filter { it.selected } val selection = titles.filter { it.selected }
val selectionMode = selected.isNotEmpty()
// KMK <-- // KMK <--
val titles: ImmutableList<MigrateMangaItem> val titles: ImmutableList<MigrateMangaItem>
@ -187,6 +192,8 @@ class MigrateMangaScreenModel(
val isEmpty: Boolean val isEmpty: Boolean
get() = titles.isEmpty() get() = titles.isEmpty()
val selectionMode = selection.isNotEmpty()
} }
} }