Range-selection for Migration (#466)
- Select a range of entries for migration - Better bottom bar - Show obsolete sources --------- Co-authored-by: ImaginaryDesignation <108343184+ImaginaryDesignation@users.noreply.github.com>
This commit is contained in:
parent
6b6e6a7fba
commit
467c39b899
10 changed files with 668 additions and 187 deletions
|
|
@ -1,35 +1,124 @@
|
|||
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?,
|
||||
title: String,
|
||||
state: MigrateMangaScreenModel.State,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickItem: (MigrateMangaItem) -> 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 ->
|
||||
AppBar(
|
||||
// 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(
|
||||
|
|
@ -44,6 +133,10 @@ fun MigrateMangaScreen(
|
|||
state = state,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
// KMK -->
|
||||
onMangaSelected = onMangaSelected,
|
||||
listState = listState,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -52,17 +145,36 @@ fun MigrateMangaScreen(
|
|||
private fun MigrateMangaContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: MigrateMangaScreenModel.State,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickItem: (MigrateMangaItem) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
// KMK -->
|
||||
onMangaSelected: (MigrateMangaItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
listState: LazyListState,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
state = listState,
|
||||
) {
|
||||
items(state.titles) { manga ->
|
||||
// KMK <--
|
||||
items(items = state.titles) {
|
||||
MigrateMangaItem(
|
||||
manga = manga,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
manga = it.manga,
|
||||
onClickItem = {
|
||||
// KMK -->
|
||||
when {
|
||||
state.selectionMode -> onMangaSelected(it, !it.selected, true, false)
|
||||
// KMK <--
|
||||
else -> onClickItem(it)
|
||||
}
|
||||
},
|
||||
onClickCover = {
|
||||
onClickCover(it.manga)
|
||||
// KMK -->
|
||||
}.takeIf { !state.selectionMode },
|
||||
onLongClick = { onMangaSelected(it, !it.selected, true, true) },
|
||||
selected = it.selected,
|
||||
modifier = Modifier.animateItemFastScroll(),
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -71,14 +183,159 @@ private fun MigrateMangaContent(
|
|||
@Composable
|
||||
private fun MigrateMangaItem(
|
||||
manga: Manga,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
onClickItem: () -> Unit,
|
||||
onClickCover: (() -> Unit)?,
|
||||
// KMK -->
|
||||
onLongClick: () -> Unit,
|
||||
selected: Boolean,
|
||||
// KMK <--
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseMangaListItem(
|
||||
modifier = modifier,
|
||||
manga = manga,
|
||||
onClickItem = { onClickItem(manga) },
|
||||
onClickCover = { onClickCover(manga) },
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = { onClickCover?.invoke() },
|
||||
// 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 <--
|
||||
|
|
|
|||
|
|
@ -16,16 +16,15 @@ import androidx.compose.material.icons.outlined.Numbers
|
|||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.model.installedExtension
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
import eu.kanade.presentation.components.AnimatedFloatingSearchBox
|
||||
|
|
@ -57,9 +56,6 @@ fun MigrateSourceScreen(
|
|||
onClickItem: (Source) -> Unit,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
onToggleSortingMode: () -> Unit,
|
||||
// SY -->
|
||||
onClickAll: (Source) -> Unit,
|
||||
// SY <--
|
||||
// KMK -->
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
// KMK <--
|
||||
|
|
@ -87,9 +83,6 @@ fun MigrateSourceScreen(
|
|||
onToggleSortingMode = onToggleSortingMode,
|
||||
sortingDirection = state.sortingDirection,
|
||||
onToggleSortingDirection = onToggleSortingDirection,
|
||||
// SY -->
|
||||
onClickAll = onClickAll,
|
||||
// SY <--
|
||||
// KMK -->
|
||||
state = state,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
|
|
@ -108,9 +101,6 @@ private fun MigrateSourceList(
|
|||
onToggleSortingMode: () -> Unit,
|
||||
sortingDirection: SetMigrateSorting.Direction,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
// SY -->
|
||||
onClickAll: (Source) -> Unit,
|
||||
// SY <--
|
||||
// KMK -->
|
||||
state: MigrateSourceScreenModel.State,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
|
|
@ -199,9 +189,6 @@ private fun MigrateSourceList(
|
|||
count = count,
|
||||
onClickItem = { onClickItem(source) },
|
||||
onLongClickItem = { onLongClickItem(source) },
|
||||
// SY -->
|
||||
onClickAll = { onClickAll(source) },
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -214,9 +201,6 @@ private fun MigrateSourceItem(
|
|||
count: Long,
|
||||
onClickItem: () -> Unit,
|
||||
onLongClickItem: () -> Unit,
|
||||
// SY -->
|
||||
onClickAll: () -> Unit,
|
||||
// SY <--
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseSourceItem(
|
||||
|
|
@ -230,16 +214,6 @@ private fun MigrateSourceItem(
|
|||
BadgeGroup {
|
||||
Badge(text = "$count")
|
||||
}
|
||||
// SY -->
|
||||
TextButton(onClick = onClickAll) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.all),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
},
|
||||
content = { _, sourceLangString, /* KMK --> */ lang /* KMK <-- */ ->
|
||||
Column(
|
||||
|
|
@ -276,6 +250,17 @@ private fun MigrateSourceItem(
|
|||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
// KMK -->
|
||||
} else if (source.installedExtension?.isObsolete == true) {
|
||||
Text(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
text = stringResource(MR.strings.ext_obsolete),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
// KMK <--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,14 @@
|
|||
package eu.kanade.presentation.libraryUpdateError
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -31,12 +19,10 @@ 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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
|
|
@ -45,16 +31,14 @@ import androidx.compose.runtime.mutableStateListOf
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
|
||||
import eu.kanade.presentation.manga.components.Button
|
||||
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
|
||||
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorScreenState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -74,7 +58,6 @@ import kotlin.time.Duration.Companion.seconds
|
|||
@Composable
|
||||
fun LibraryUpdateErrorScreen(
|
||||
state: LibraryUpdateErrorScreenState,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (LibraryUpdateErrorItem) -> Unit,
|
||||
onClickCover: (LibraryUpdateErrorItem) -> Unit,
|
||||
onMultiMigrateClicked: (() -> Unit),
|
||||
|
|
@ -83,8 +66,15 @@ fun LibraryUpdateErrorScreen(
|
|||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val headerIndexes = remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
LaunchedEffect(state) {
|
||||
headerIndexes.value = state.getHeaderIndexes()
|
||||
}
|
||||
|
||||
val enableScrollToTop by remember {
|
||||
derivedStateOf {
|
||||
|
|
@ -98,10 +88,6 @@ fun LibraryUpdateErrorScreen(
|
|||
}
|
||||
}
|
||||
|
||||
val headerIndexes = remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
LaunchedEffect(state) {
|
||||
headerIndexes.value = state.getHeaderIndexes()
|
||||
}
|
||||
val enableScrollToPrevious by remember {
|
||||
derivedStateOf {
|
||||
headerIndexes.value.any { it < listState.firstVisibleItemIndex }
|
||||
|
|
@ -113,11 +99,9 @@ fun LibraryUpdateErrorScreen(
|
|||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
LibraryUpdateErrorsAppBar(
|
||||
LibraryUpdateErrorAppBar(
|
||||
title = stringResource(
|
||||
KMR.strings.label_library_update_errors,
|
||||
state.items.size,
|
||||
|
|
@ -132,8 +116,7 @@ fun LibraryUpdateErrorScreen(
|
|||
)
|
||||
},
|
||||
bottomBar = {
|
||||
LibraryUpdateErrorsBottomBar(
|
||||
modifier = modifier,
|
||||
LibraryUpdateErrorBottomBar(
|
||||
selected = state.selected,
|
||||
onMultiMigrateClicked = onMultiMigrateClicked,
|
||||
enableScrollToTop = enableScrollToTop,
|
||||
|
|
@ -198,7 +181,7 @@ fun LibraryUpdateErrorScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryUpdateErrorsBottomBar(
|
||||
private fun LibraryUpdateErrorBottomBar(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: List<LibraryUpdateErrorItem>,
|
||||
onMultiMigrateClicked: (() -> Unit),
|
||||
|
|
@ -212,13 +195,19 @@ private fun LibraryUpdateErrorsBottomBar(
|
|||
scrollToNext: () -> 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.surfaceContainerHigh,
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
elevation = animatedElevation,
|
||||
),
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
|
||||
|
|
@ -306,62 +295,7 @@ private fun LibraryUpdateErrorsBottomBar(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Button(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
toConfirm: Boolean,
|
||||
enabled: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: (() -> Unit),
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
||||
val animatedColor by animateColorAsState(
|
||||
if (enabled) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.38f,
|
||||
)
|
||||
},
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.weight(animatedWeight)
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
tint = animatedColor,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = toConfirm,
|
||||
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = animatedColor,
|
||||
)
|
||||
}
|
||||
content?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryUpdateErrorsAppBar(
|
||||
private fun LibraryUpdateErrorAppBar(
|
||||
title: String,
|
||||
itemCnt: Int,
|
||||
navigateUp: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
@ -12,10 +12,13 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
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(
|
||||
|
|
@ -23,13 +26,31 @@ fun BaseMangaListItem(
|
|||
modifier: Modifier = Modifier,
|
||||
onClickItem: () -> Unit = {},
|
||||
onClickCover: () -> Unit = onClickItem,
|
||||
// KMK -->
|
||||
onLongClick: () -> Unit = onClickItem,
|
||||
selected: Boolean,
|
||||
// KMK <--
|
||||
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable RowScope.() -> Unit = { defaultContent(manga) },
|
||||
) {
|
||||
// KMK -->
|
||||
val haptic = LocalHapticFeedback.current
|
||||
// KMK <--
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClickItem)
|
||||
// KMK -->
|
||||
.selectedBackground(selected)
|
||||
.combinedClickable(
|
||||
// KMK <--
|
||||
onClick = onClickItem,
|
||||
// KMK -->
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
// KMK <--
|
||||
)
|
||||
.height(56.dp)
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
|
|
@ -182,18 +183,33 @@ fun MangaBottomActionMenu(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Button(
|
||||
internal fun RowScope.Button(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
toConfirm: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
// KMK -->
|
||||
enabled: Boolean = true,
|
||||
// KMK <--
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(
|
||||
targetValue = if (toConfirm) 2f else 1f,
|
||||
label = "weight",
|
||||
)
|
||||
// KMK -->
|
||||
val animatedColor by animateColorAsState(
|
||||
if (enabled) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.38f,
|
||||
)
|
||||
},
|
||||
label = "color",
|
||||
)
|
||||
// KMK <--
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
|
|
@ -210,6 +226,9 @@ private fun RowScope.Button(
|
|||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
// KMK -->
|
||||
tint = animatedColor,
|
||||
// KMK <--
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = toConfirm,
|
||||
|
|
@ -221,6 +240,9 @@ private fun RowScope.Button(
|
|||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
// KMK -->
|
||||
color = animatedColor,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
content?.invoke()
|
||||
|
|
|
|||
|
|
@ -2,36 +2,69 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.design
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material.icons.outlined.Deselect
|
||||
import androidx.compose.material.icons.outlined.PushPin
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material.icons.outlined.ToggleOn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.updatePadding
|
||||
|
|
@ -41,13 +74,15 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.SourcesSearchBox
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.databinding.PreMigrationListBinding
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationProcedureConfig
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
|
|
@ -57,6 +92,7 @@ import tachiyomi.presentation.core.components.material.padding
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.io.Serializable
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
sealed class MigrationType : Serializable {
|
||||
data class MangaList(val mangaIds: List<Long>) : MigrationType()
|
||||
|
|
@ -125,32 +161,19 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
|
|||
title = stringResource(SYMR.strings.select_sources),
|
||||
navigateUp = navigator::pop,
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(SYMR.strings.select_none),
|
||||
icon = Icons.Outlined.Deselect,
|
||||
onClick = { screenModel.massSelect(false) },
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = { screenModel.massSelect(true) },
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(SYMR.strings.match_enabled_sources),
|
||||
onClick = { screenModel.matchSelection(true) },
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(SYMR.strings.match_pinned_sources),
|
||||
onClick = { screenModel.matchSelection(false) },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
// KMK -->
|
||||
bottomBar = {
|
||||
PreMigrationScreenBottomBar(
|
||||
modifier = Modifier,
|
||||
onSelectAll = { screenModel.massSelect(true) },
|
||||
onSelectNone = { screenModel.massSelect(false) },
|
||||
onSelectPinned = { screenModel.matchSelection(false) },
|
||||
onSelectEnabled = { screenModel.matchSelection(true) },
|
||||
)
|
||||
},
|
||||
// KMK <--
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_migrate)) },
|
||||
|
|
@ -234,6 +257,124 @@ class PreMigrationScreen(val migration: MigrationType) : Screen() {
|
|||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
@Composable
|
||||
fun PreMigrationScreenBottomBar(
|
||||
modifier: Modifier,
|
||||
onSelectAll: () -> Unit,
|
||||
onSelectNone: () -> Unit,
|
||||
onSelectPinned: () -> Unit,
|
||||
onSelectEnabled: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.large.copy(
|
||||
bottomEnd = ZeroCornerSize,
|
||||
bottomStart = ZeroCornerSize,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation = 0.dp),
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val confirm = remember { mutableStateListOf(false, false, false, false) }
|
||||
var resetJob: Job? = remember { null }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
(0 until 4).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(MR.strings.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = onSelectAll,
|
||||
toConfirm = confirm[0],
|
||||
onLongClick = { onLongClickItem(0) },
|
||||
)
|
||||
Button(
|
||||
title = stringResource(SYMR.strings.select_none),
|
||||
icon = Icons.Outlined.Deselect,
|
||||
onClick = onSelectNone,
|
||||
toConfirm = confirm[1],
|
||||
onLongClick = { onLongClickItem(1) },
|
||||
)
|
||||
Button(
|
||||
title = stringResource(SYMR.strings.match_enabled_sources),
|
||||
icon = Icons.Outlined.ToggleOn,
|
||||
onClick = onSelectEnabled,
|
||||
toConfirm = confirm[2],
|
||||
onLongClick = { onLongClickItem(2) },
|
||||
)
|
||||
Button(
|
||||
title = stringResource(SYMR.strings.match_pinned_sources),
|
||||
icon = Icons.Outlined.PushPin,
|
||||
onClick = onSelectPinned,
|
||||
toConfirm = confirm[3],
|
||||
onLongClick = { onLongClickItem(3) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Button(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
toConfirm: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: (() -> Unit),
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(
|
||||
targetValue = if (toConfirm) 2f else 1f,
|
||||
label = "weight",
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.weight(animatedWeight)
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = toConfirm,
|
||||
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Visible,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
content?.invoke()
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
companion object {
|
||||
fun navigateToMigration(skipPre: Boolean, navigator: Navigator, mangaIds: List<Long>) {
|
||||
navigator.push(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
|
@ -39,18 +40,39 @@ data class MigrateMangaScreen(
|
|||
|
||||
MigrateMangaScreen(
|
||||
navigateUp = navigator::pop,
|
||||
title = state.source!!.name,
|
||||
title = state.source?.name ?: "???",
|
||||
state = state,
|
||||
onClickItem = {
|
||||
// SY -->
|
||||
PreMigrationScreen.navigateToMigration(
|
||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||
navigator,
|
||||
listOf(it.id),
|
||||
listOf(it.manga.id),
|
||||
)
|
||||
// SY <--
|
||||
},
|
||||
onClickCover = { navigator.push(MangaScreen(it.id)) },
|
||||
// KMK -->
|
||||
onMultiMigrateClicked = {
|
||||
if (state.selectionMode) {
|
||||
PreMigrationScreen.navigateToMigration(
|
||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||
navigator,
|
||||
state.selected.map { it.manga.id },
|
||||
)
|
||||
} else {
|
||||
context.toast(KMR.strings.migrating_all_entries)
|
||||
PreMigrationScreen.navigateToMigration(
|
||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||
navigator,
|
||||
state.titles.map { it.manga.id },
|
||||
)
|
||||
}
|
||||
},
|
||||
onSelectAll = screenModel::toggleAllSelection,
|
||||
onInvertSelection = screenModel::invertSelection,
|
||||
onMangaSelected = screenModel::toggleSelection,
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
|
|||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -32,6 +33,12 @@ class MigrateMangaScreenModel(
|
|||
private val _events: Channel<MigrationMangaEvent> = Channel()
|
||||
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
|
||||
|
||||
// KMK -->
|
||||
// First and last selected index in list
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
private val selectedMangaIds: HashSet<Long> = HashSet()
|
||||
// KMK <--
|
||||
|
||||
init {
|
||||
screenModelScope.launch {
|
||||
mutableState.update { state ->
|
||||
|
|
@ -46,9 +53,14 @@ class MigrateMangaScreenModel(
|
|||
state.copy(titleList = persistentListOf())
|
||||
}
|
||||
}
|
||||
// KMK -->
|
||||
.map { manga ->
|
||||
toMigrationMangaScreenItems(manga)
|
||||
}
|
||||
// KMK <--
|
||||
.map { manga ->
|
||||
manga
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title })
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.manga.title })
|
||||
.toImmutableList()
|
||||
}
|
||||
.collectLatest { list ->
|
||||
|
|
@ -57,13 +69,117 @@ class MigrateMangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
private fun toMigrationMangaScreenItems(mangas: List<Manga>): List<MigrateMangaItem> {
|
||||
return mangas.map { manga ->
|
||||
MigrateMangaItem(
|
||||
manga = manga,
|
||||
selected = manga.id in selectedMangaIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelection(
|
||||
item: MigrateMangaItem,
|
||||
selected: Boolean,
|
||||
userSelected: Boolean = false,
|
||||
fromLongPress: Boolean = false,
|
||||
) {
|
||||
mutableState.update { state ->
|
||||
val newItems = state.titles.toMutableList().apply {
|
||||
val selectedIndex = indexOfFirst { it.manga.id == item.manga.id }
|
||||
if (selectedIndex < 0) return@apply
|
||||
|
||||
val selectedItem = get(selectedIndex)
|
||||
if (selectedItem.selected == selected) return@apply
|
||||
|
||||
val firstSelection = none { it.selected }
|
||||
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||
selectedMangaIds.addOrRemove(item.manga.id, selected)
|
||||
|
||||
if (selected && userSelected && fromLongPress) {
|
||||
if (firstSelection) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
} else {
|
||||
// Try to select the items in-between when possible
|
||||
val range: IntRange
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
range = selectedIndex + 1 until selectedPositions[0]
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
range = (selectedPositions[1] + 1) until selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
} else {
|
||||
// Just select itself
|
||||
range = IntRange.EMPTY
|
||||
}
|
||||
|
||||
range.forEach {
|
||||
val inBetweenItem = get(it)
|
||||
if (!inBetweenItem.selected) {
|
||||
selectedMangaIds.add(inBetweenItem.manga.id)
|
||||
set(it, inBetweenItem.copy(selected = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (userSelected && !fromLongPress) {
|
||||
if (!selected) {
|
||||
if (selectedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = indexOfFirst { it.selected }
|
||||
} else if (selectedIndex == selectedPositions[1]) {
|
||||
selectedPositions[1] = indexOfLast { it.selected }
|
||||
}
|
||||
} else {
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
selectedPositions[1] = selectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state.copy(titleList = newItems.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAllSelection(selected: Boolean) {
|
||||
mutableState.update { state ->
|
||||
val newItems = state.titles.map {
|
||||
selectedMangaIds.addOrRemove(it.manga.id, selected)
|
||||
it.copy(selected = selected)
|
||||
}
|
||||
state.copy(titleList = newItems.toImmutableList())
|
||||
}
|
||||
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
mutableState.update { state ->
|
||||
val newItems = state.titles.map {
|
||||
selectedMangaIds.addOrRemove(it.manga.id, !it.selected)
|
||||
it.copy(selected = !it.selected)
|
||||
}
|
||||
state.copy(titleList = newItems.toImmutableList())
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val source: Source? = null,
|
||||
private val titleList: ImmutableList<Manga>? = null,
|
||||
private val titleList: ImmutableList<MigrateMangaItem>? = null,
|
||||
) {
|
||||
// KMK -->
|
||||
val selected = titles.filter { it.selected }
|
||||
val selectionMode = selected.isNotEmpty()
|
||||
// KMK <--
|
||||
|
||||
val titles: ImmutableList<Manga>
|
||||
val titles: ImmutableList<MigrateMangaItem>
|
||||
get() = titleList ?: persistentListOf()
|
||||
|
||||
val isLoading: Boolean
|
||||
|
|
@ -77,3 +193,11 @@ class MigrateMangaScreenModel(
|
|||
sealed interface MigrationMangaEvent {
|
||||
data object FailedFetchingFavorites : MigrationMangaEvent
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
@Immutable
|
||||
data class MigrateMangaItem(
|
||||
val manga: Manga,
|
||||
val selected: Boolean,
|
||||
)
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -13,18 +13,10 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||
import eu.kanade.presentation.browse.MigrateSourceScreen
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreen
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun Screen.migrateSourceTab(): TabContent {
|
||||
|
|
@ -53,24 +45,6 @@ fun Screen.migrateSourceTab(): TabContent {
|
|||
},
|
||||
onToggleSortingDirection = screenModel::toggleSortingDirection,
|
||||
onToggleSortingMode = screenModel::toggleSortingMode,
|
||||
// SY -->
|
||||
onClickAll = { source ->
|
||||
// TODO: Jay wtf, need to clean this up sometime
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
launchIO {
|
||||
val manga = Injekt.get<GetFavorites>().await()
|
||||
val sourceMangas =
|
||||
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
|
||||
withUIContext {
|
||||
PreMigrationScreen.navigateToMigration(
|
||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||
navigator,
|
||||
sourceMangas,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
// SY <--
|
||||
// KMK -->
|
||||
onChangeSearchQuery = screenModel::search,
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@
|
|||
|
||||
<!-- Migration -->
|
||||
<string name="current_">Current: %1$s</string>
|
||||
<string name="migrating_all_entries">Migrating all entries from source</string>
|
||||
|
||||
<!-- Misc -->
|
||||
<string name="label_to_be_updated">To be updated</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue