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,
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 <--
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue