diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 20814d2cb7..c939ca7682 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -260,6 +260,7 @@ dependencies { implementation(libs.swipe) implementation(libs.compose.webview) implementation(libs.compose.grid) + implementation(libs.reorderable) // Logging implementation(libs.logcat) diff --git a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt index df553a203e..e644678e5a 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -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 @@ -18,6 +23,8 @@ import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.tachiyomi.ui.category.CategoryScreenState 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 @@ -34,8 +41,7 @@ fun CategoryScreen( onClickSortAlphabetically: () -> Unit, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, - onClickMoveUp: (Category) -> Unit, - onClickMoveDown: (Category) -> Unit, + changeOrder: (Category, Int) -> Unit, navigateUp: () -> Unit, ) { val lazyListState = rememberLazyListState() @@ -81,8 +87,7 @@ fun CategoryScreen( PaddingValues(horizontal = MaterialTheme.padding.medium), onClickRename = onClickRename, onClickDelete = onClickDelete, - onMoveUp = onClickMoveUp, - onMoveDown = onClickMoveDown, + changeOrder = changeOrder, ) } } @@ -94,28 +99,41 @@ private fun CategoryContent( paddingValues: PaddingValues, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, - onMoveUp: (Category) -> Unit, - onMoveDown: (Category) -> Unit, + changeOrder: (Category, Int) -> Unit, ) { + 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) }, - ) + items( + items = reorderableList, + key = { category -> category.key }, + ) { category -> + ReorderableItem(reorderableLazyColumnState, category.key) { + CategoryListItem( + modifier = Modifier.animateItem(), + category = category, + onRename = { onClickRename(category) }, + onDelete = { onClickDelete(category) }, + ) + } } } } + +private val Category.key get() = "category-$id" diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt index 5c387e542c..af42653522 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt @@ -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.material3.ElevatedCard import androidx.compose.material3.Icon @@ -19,18 +16,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import sh.calvin.reorderable.ReorderableCollectionItemScope import tachiyomi.domain.category.model.Category import tachiyomi.i18n.MR 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, modifier: Modifier = Modifier, @@ -42,34 +36,25 @@ 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, contentDescription = null) + Icon( + imageVector = Icons.Outlined.DragHandle, + contentDescription = null, + modifier = Modifier + .padding(MaterialTheme.padding.medium) + .draggableHandle(), + ) Text( text = category.name, 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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt index 20ebdba0ff..1aa79d7362 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt @@ -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, navigateUp = navigator::pop, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt index 2fb78342fd..991893a48e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreenModel.kt @@ -74,18 +74,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 -> {} } diff --git a/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt b/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt index a648248017..b0f299953c 100644 --- a/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt +++ b/domain/src/main/java/tachiyomi/domain/category/interactor/ReorderCategory.kt @@ -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, - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07679239d4..1e65b2444e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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-grid = "io.woong.compose.grid:grid:1.2.2" 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"