drag to reorder Category list

This commit is contained in:
Cuong-Tran 2024-11-01 02:05:01 +07:00
parent 79e711efc2
commit d420f98d31
No known key found for this signature in database
GPG Key ID: 733AA7624B9315C2
7 changed files with 59 additions and 80 deletions

View File

@ -260,6 +260,7 @@ dependencies {
implementation(libs.swipe) implementation(libs.swipe)
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.reorderable)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)

View File

@ -5,12 +5,17 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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 androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem import eu.kanade.presentation.category.components.CategoryListItem
@ -18,6 +23,8 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.ui.category.CategoryScreenState import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@ -34,8 +41,7 @@ fun CategoryScreen(
onClickSortAlphabetically: () -> Unit, onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit, onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit, onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit, moveTo: (Category, Int) -> Unit,
onClickMoveDown: (Category) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
@ -81,8 +87,7 @@ fun CategoryScreen(
PaddingValues(horizontal = MaterialTheme.padding.medium), PaddingValues(horizontal = MaterialTheme.padding.medium),
onClickRename = onClickRename, onClickRename = onClickRename,
onClickDelete = onClickDelete, onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp, moveTo = moveTo,
onMoveDown = onClickMoveDown,
) )
} }
} }
@ -94,28 +99,41 @@ private fun CategoryContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
onClickRename: (Category) -> Unit, onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit, onClickDelete: (Category) -> Unit,
onMoveUp: (Category) -> Unit, moveTo: (Category, Int) -> Unit,
onMoveDown: (Category) -> Unit,
) { ) {
var reorderableList by remember { mutableStateOf(categories.toList()) }
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
reorderableList = reorderableList.toMutableList().apply {
moveTo(reorderableList[from.index], to.index - from.index)
add(to.index, removeAt(from.index))
}
}
LaunchedEffect(categories) {
if (!reorderableLazyColumnState.isAnyItemDragging) {
reorderableList = categories.toList()
}
}
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,
contentPadding = paddingValues, contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
itemsIndexed( items(
items = categories, items = reorderableList,
key = { _, category -> "category-${category.id}" }, key = { category -> category.key() },
) { index, category -> ) { category ->
ReorderableItem(reorderableLazyColumnState, category.key()) {
CategoryListItem( CategoryListItem(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
category = category, category = category,
canMoveUp = index != 0,
canMoveDown = index != categories.lastIndex,
onMoveUp = onMoveUp,
onMoveDown = onMoveDown,
onRename = { onClickRename(category) }, onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) }, onDelete = { onClickDelete(category) },
) )
} }
} }
} }
}
fun Category.key() = "category-$id"

View File

@ -2,15 +2,12 @@ package eu.kanade.presentation.category.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.Delete
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.rounded.DragHandle
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -19,18 +16,15 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import sh.calvin.reorderable.ReorderableCollectionItemScope
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
fun CategoryListItem( fun ReorderableCollectionItemScope.CategoryListItem(
category: Category, category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onRename: () -> Unit, onRename: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -42,34 +36,22 @@ fun CategoryListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onRename() } .clickable { onRename() }
.padding( .padding(MaterialTheme.padding.medium),
start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) IconButton(
modifier = Modifier
.draggableHandle(),
onClick = {},
) {
Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
}
Text( Text(
text = category.name, text = category.name,
modifier = Modifier modifier = Modifier
.weight(1f)
.padding(start = MaterialTheme.padding.medium), .padding(start = MaterialTheme.padding.medium),
) )
}
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) { IconButton(onClick = onRename) {
Icon( Icon(
imageVector = Icons.Outlined.Edit, imageVector = Icons.Outlined.Edit,

View File

@ -43,8 +43,7 @@ class CategoryScreen : Screen() {
onClickSortAlphabetically = { screenModel.showDialog(CategoryDialog.SortAlphabetically) }, onClickSortAlphabetically = { screenModel.showDialog(CategoryDialog.SortAlphabetically) },
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) }, onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) }, onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
onClickMoveUp = screenModel::moveUp, moveTo = screenModel::moveTo,
onClickMoveDown = screenModel::moveDown,
navigateUp = navigator::pop, navigateUp = navigator::pop,
) )

View File

@ -74,18 +74,9 @@ class CategoryScreenModel(
} }
} }
fun moveUp(category: Category) { fun moveTo(category: Category, offset: Int) {
screenModelScope.launch { screenModelScope.launch {
when (reorderCategory.moveUp(category)) { when (reorderCategory.await(category, offset)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
}
}
}
fun moveDown(category: Category) {
screenModelScope.launch {
when (reorderCategory.moveDown(category)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {} else -> {}
} }

View File

@ -8,7 +8,6 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.category.model.CategoryUpdate import tachiyomi.domain.category.model.CategoryUpdate
import tachiyomi.domain.category.repository.CategoryRepository import tachiyomi.domain.category.repository.CategoryRepository
import java.util.Collections
class ReorderCategory( class ReorderCategory(
private val categoryRepository: CategoryRepository, private val categoryRepository: CategoryRepository,
@ -16,11 +15,7 @@ class ReorderCategory(
private val mutex = Mutex() private val mutex = Mutex()
suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP) suspend fun await(category: Category, offset: Int) = withNonCancellableContext {
suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN)
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
mutex.withLock { mutex.withLock {
val categories = categoryRepository.getAll() val categories = categoryRepository.getAll()
.filterNot(Category::isSystemCategory) .filterNot(Category::isSystemCategory)
@ -31,13 +26,10 @@ class ReorderCategory(
return@withNonCancellableContext Result.Unchanged return@withNonCancellableContext Result.Unchanged
} }
val newPosition = when (moveTo) { val newPosition = currentIndex + offset
MoveTo.UP -> currentIndex - 1
MoveTo.DOWN -> currentIndex + 1
}.toInt()
try { try {
Collections.swap(categories, currentIndex, newPosition) categories.add(newPosition, categories.removeAt(currentIndex))
val updates = categories.mapIndexed { index, category -> val updates = categories.mapIndexed { index, category ->
CategoryUpdate( CategoryUpdate(
@ -81,9 +73,4 @@ class ReorderCategory(
data object Unchanged : Result data object Unchanged : Result
data class InternalError(val error: Throwable) : Result data class InternalError(val error: Throwable) : Result
} }
private enum class MoveTo {
UP,
DOWN,
}
} }

View File

@ -65,6 +65,7 @@ compose-materialmotion = "io.github.fornewid:material-motion-compose-core:2.0.1"
compose-webview = "io.github.kevinnzou:compose-webview:0.33.6" compose-webview = "io.github.kevinnzou:compose-webview:0.33.6"
compose-grid = "io.woong.compose.grid:grid:1.2.2" compose-grid = "io.woong.compose.grid:grid:1.2.2"
compose-stablemarker = "com.github.skydoves:compose-stable-marker:1.0.5" compose-stablemarker = "com.github.skydoves:compose-stable-marker:1.0.5"
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.4.0" }
swipe = "me.saket.swipe:swipe:1.3.0" swipe = "me.saket.swipe:swipe:1.3.0"