diff --git a/CHANGELOG.md b/CHANGELOG.md index 846498ae4..43fd38873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736)) - Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774)) - Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161)) +- Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427)) ### Changed - Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603)) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2c38bc6ad..556d43775 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -267,6 +267,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 df553a203..43d03bfaf 100644 --- a/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/category/CategoryScreen.kt @@ -2,15 +2,19 @@ package eu.kanade.presentation.category import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize 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.remember +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.category.components.CategoryListItem @@ -18,6 +22,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 +40,7 @@ fun CategoryScreen( onClickSortAlphabetically: () -> Unit, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, - onClickMoveUp: (Category) -> Unit, - onClickMoveDown: (Category) -> Unit, + onChangeOrder: (Category, Int) -> Unit, navigateUp: () -> Unit, ) { val lazyListState = rememberLazyListState() @@ -76,13 +81,10 @@ fun CategoryScreen( CategoryContent( categories = state.categories, lazyListState = lazyListState, - paddingValues = paddingValues + - topSmallPaddingValues + - PaddingValues(horizontal = MaterialTheme.padding.medium), + paddingValues = paddingValues, onClickRename = onClickRename, onClickDelete = onClickDelete, - onMoveUp = onClickMoveUp, - onMoveDown = onClickMoveDown, + onChangeOrder = onChangeOrder, ) } } @@ -94,28 +96,44 @@ private fun CategoryContent( paddingValues: PaddingValues, onClickRename: (Category) -> Unit, onClickDelete: (Category) -> Unit, - onMoveUp: (Category) -> Unit, - onMoveDown: (Category) -> Unit, + onChangeOrder: (Category, Int) -> Unit, ) { + val categoriesState = remember { categories.toMutableStateList() } + val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to -> + val item = categoriesState.removeAt(from.index) + categoriesState.add(to.index, item) + onChangeOrder(item, to.index) + } + + LaunchedEffect(categories) { + if (!reorderableState.isAnyItemDragging) { + categoriesState.clear() + categoriesState.addAll(categories) + } + } + LazyColumn( + modifier = Modifier.fillMaxSize(), state = lazyListState, - contentPadding = paddingValues, + contentPadding = paddingValues + + topSmallPaddingValues + + PaddingValues(horizontal = MaterialTheme.padding.medium), 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 = categoriesState, + key = { category -> category.key }, + ) { category -> + ReorderableItem(reorderableState, category.key) { + CategoryListItem( + modifier = Modifier.animateItem(), + category = category, + onRename = { onClickRename(category) }, + onDelete = { onClickDelete(category) }, + ) + } } } } + +private val Category.key inline 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 5c387e542..88558b97a 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,57 +16,42 @@ 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, ) { - ElevatedCard( - modifier = modifier, - ) { + ElevatedCard(modifier = modifier) { Row( modifier = Modifier .fillMaxWidth() - .clickable { onRename() } + .clickable(onClick = 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), + modifier = Modifier.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, @@ -77,7 +59,10 @@ fun CategoryListItem( ) } IconButton(onClick = onDelete) { - Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete)) + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(MR.strings.action_delete), + ) } } } 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 20ebdba0f..4bafb277f 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, + onChangeOrder = 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 2fb78342f..09c543479 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, newIndex: 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, newIndex)) { 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 a64824801..4f91a9901 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, newIndex: Int) = withNonCancellableContext { mutex.withLock { val categories = categoryRepository.getAll() .filterNot(Category::isSystemCategory) @@ -31,13 +26,8 @@ class ReorderCategory( return@withNonCancellableContext Result.Unchanged } - val newPosition = when (moveTo) { - MoveTo.UP -> currentIndex - 1 - MoveTo.DOWN -> currentIndex + 1 - }.toInt() - try { - Collections.swap(categories, currentIndex, newPosition) + categories.add(newIndex, categories.removeAt(currentIndex)) val updates = categories.mapIndexed { index, category -> CategoryUpdate( @@ -81,9 +71,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 92f669e6b..5acc5bc52 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.3" } swipe = "me.saket.swipe:swipe:1.3.0"