Drag & Drop to reorder category quickly in Settings/Category screen (#508)
* drag to reorder Category list * smaller padding * cleanup code * fix build
This commit is contained in:
parent
bc6a4959b2
commit
23d53a4560
7 changed files with 60 additions and 84 deletions
|
|
@ -293,6 +293,7 @@ dependencies {
|
|||
implementation(libs.swipe)
|
||||
implementation(libs.compose.webview)
|
||||
implementation(libs.compose.grid)
|
||||
implementation(libs.reorderable)
|
||||
|
||||
// KMK -->
|
||||
implementation(libs.palette.ktx)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,17 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.category.components.CategoryListItem
|
||||
|
|
@ -19,6 +24,8 @@ import eu.kanade.presentation.components.AppBarActions
|
|||
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
|
@ -35,8 +42,7 @@ fun CategoryScreen(
|
|||
onClickSortAlphabetically: () -> Unit,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onClickMoveUp: (Category) -> Unit,
|
||||
onClickMoveDown: (Category) -> Unit,
|
||||
changeOrder: (Category, Int) -> Unit,
|
||||
// KMK -->
|
||||
onClickHide: (Category) -> Unit,
|
||||
// KMK <--
|
||||
|
|
@ -84,8 +90,7 @@ fun CategoryScreen(
|
|||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
onClickRename = onClickRename,
|
||||
onClickDelete = onClickDelete,
|
||||
onMoveUp = onClickMoveUp,
|
||||
onMoveDown = onClickMoveDown,
|
||||
changeOrder = changeOrder,
|
||||
// KMK -->
|
||||
onClickHide = onClickHide,
|
||||
// KMK <--
|
||||
|
|
@ -100,34 +105,47 @@ private fun CategoryContent(
|
|||
paddingValues: PaddingValues,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
changeOrder: (Category, Int) -> Unit,
|
||||
// KMK -->
|
||||
onClickHide: (Category) -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
var reorderableList by remember { mutableStateOf(categories.toList()) }
|
||||
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
reorderableList = reorderableList.toMutableList().apply {
|
||||
changeOrder(reorderableList[from.index], to.index - from.index)
|
||||
add(to.index, removeAt(from.index))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(categories) {
|
||||
if (!reorderableLazyColumnState.isAnyItemDragging) {
|
||||
reorderableList = categories.toList()
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = categories,
|
||||
key = { _, category -> "category-${category.id}" },
|
||||
) { index, category ->
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
category = category,
|
||||
canMoveUp = index != 0,
|
||||
canMoveDown = index != categories.lastIndex,
|
||||
onMoveUp = onMoveUp,
|
||||
onMoveDown = onMoveDown,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
// KMK -->
|
||||
onHide = { onClickHide(category) },
|
||||
// KMK <--
|
||||
)
|
||||
items(
|
||||
items = reorderableList,
|
||||
key = { category -> category.key },
|
||||
) { category ->
|
||||
ReorderableItem(reorderableLazyColumnState, category.key) {
|
||||
CategoryListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
category = category,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
// KMK -->
|
||||
onHide = { onClickHide(category) },
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Category.key get() = "category-$id"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ package eu.kanade.presentation.category.components
|
|||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DragHandle
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
|
|
@ -23,6 +20,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
|
|
@ -30,12 +28,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun CategoryListItem(
|
||||
fun ReorderableCollectionItemScope.CategoryListItem(
|
||||
category: Category,
|
||||
canMoveUp: Boolean,
|
||||
canMoveDown: Boolean,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
// KMK -->
|
||||
|
|
@ -50,19 +44,19 @@ fun CategoryListItem(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onRename() }
|
||||
.padding(vertical = MaterialTheme.padding.small)
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
start = MaterialTheme.padding.small,
|
||||
end = MaterialTheme.padding.medium,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Label,
|
||||
imageVector = Icons.Outlined.DragHandle,
|
||||
contentDescription = null,
|
||||
// KMK -->
|
||||
tint = LocalContentColor.current.let { if (category.hidden) it.copy(alpha = 0.6f) else it },
|
||||
// KMK <--
|
||||
modifier = Modifier
|
||||
.padding(MaterialTheme.padding.medium)
|
||||
.draggableHandle(),
|
||||
)
|
||||
Text(
|
||||
text = category.name,
|
||||
|
|
@ -71,23 +65,8 @@ fun CategoryListItem(
|
|||
textDecoration = TextDecoration.LineThrough.takeIf { category.hidden },
|
||||
// KMK <--
|
||||
modifier = Modifier
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ class CategoryScreen : Screen() {
|
|||
onClickSortAlphabetically = { screenModel.showDialog(CategoryDialog.SortAlphabetically) },
|
||||
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) },
|
||||
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
|
||||
onClickMoveUp = screenModel::moveUp,
|
||||
onClickMoveDown = screenModel::moveDown,
|
||||
changeOrder = screenModel::changeOrder,
|
||||
// KMK -->
|
||||
onClickHide = screenModel::hideCategory,
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -89,18 +89,9 @@ class CategoryScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun moveUp(category: Category) {
|
||||
fun changeOrder(category: Category, newOrder: Int) {
|
||||
screenModelScope.launch {
|
||||
when (reorderCategory.moveUp(category)) {
|
||||
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveDown(category: Category) {
|
||||
screenModelScope.launch {
|
||||
when (reorderCategory.moveDown(category)) {
|
||||
when (reorderCategory.changeOrder(category, newOrder)) {
|
||||
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import tachiyomi.core.common.util.system.logcat
|
|||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import java.util.Collections
|
||||
|
||||
class ReorderCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
|
|
@ -16,11 +15,7 @@ class ReorderCategory(
|
|||
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP)
|
||||
|
||||
suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN)
|
||||
|
||||
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
|
||||
suspend fun changeOrder(category: Category, newOrder: Int) = withNonCancellableContext {
|
||||
mutex.withLock {
|
||||
val categories = categoryRepository.getAll()
|
||||
.filterNot(Category::isSystemCategory)
|
||||
|
|
@ -31,13 +26,10 @@ class ReorderCategory(
|
|||
return@withNonCancellableContext Result.Unchanged
|
||||
}
|
||||
|
||||
val newPosition = when (moveTo) {
|
||||
MoveTo.UP -> currentIndex - 1
|
||||
MoveTo.DOWN -> currentIndex + 1
|
||||
}.toInt()
|
||||
val newPosition = currentIndex + newOrder
|
||||
|
||||
try {
|
||||
Collections.swap(categories, currentIndex, newPosition)
|
||||
categories.add(newPosition, categories.removeAt(currentIndex))
|
||||
|
||||
val updates = categories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
|
|
@ -81,9 +73,4 @@ class ReorderCategory(
|
|||
data object Unchanged : Result
|
||||
data class InternalError(val error: Throwable) : Result
|
||||
}
|
||||
|
||||
private enum class MoveTo {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ compose-stablemarker = "com.github.skydoves:compose-stable-marker:1.0.5"
|
|||
palette-ktx = "androidx.palette:palette-ktx:1.0.0"
|
||||
materialKolor = "com.materialkolor:material-kolor:2.0.0"
|
||||
haze = "dev.chrisbanes.haze:haze:1.1.1"
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.4.0" }
|
||||
|
||||
swipe = "me.saket.swipe:swipe:1.3.0"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue