From a86ec40c79a92db01c04d8e497ee4c21f0d16715 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:00:00 +0700 Subject: [PATCH] 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> --- .../presentation/browse/MigrateMangaScreen.kt | 339 ------------------ .../LibraryUpdateErrorScreen.kt | 6 +- .../manga/ChapterSettingsDialog.kt | 4 +- .../manga/components/BaseMangaListItem.kt | 3 - .../manga/components/MangaBottomActionMenu.kt | 6 +- .../migration/manga/MigrateMangaScreen.kt | 334 ++++++++++++++++- .../manga/MigrateMangaScreenModel.kt | 13 +- 7 files changed, 334 insertions(+), 371 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt deleted file mode 100644 index 21aa59ade..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateMangaScreen.kt +++ /dev/null @@ -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, - 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 <-- diff --git a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt index 752c4d71e..fdbdcfa20 100644 --- a/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/libraryUpdateError/LibraryUpdateErrorScreen.kt @@ -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, - 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) diff --git a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt index 9af282179..29def7b75 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt @@ -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), diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt index 16281868a..e56216ba0 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/BaseMangaListItem.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index a52ff2f14..75cb82c55 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -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[i] = i == toConfirmIndex } + confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() resetJob = scope.launch { delay(1.seconds) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt index c986535e2..23989faae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreen.kt @@ -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, + 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 <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt index 896581d9e..768eaf21c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaScreenModel.kt @@ -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? = null, ) { // KMK --> - val selected = titles.filter { it.selected } - val selectionMode = selected.isNotEmpty() + val selection = titles.filter { it.selected } // KMK <-- val titles: ImmutableList @@ -187,6 +192,8 @@ class MigrateMangaScreenModel( val isEmpty: Boolean get() = titles.isEmpty() + + val selectionMode = selection.isNotEmpty() } }