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:
parent
9cbd46947b
commit
a86ec40c79
7 changed files with 334 additions and 371 deletions
|
|
@ -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 <--
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue