Improve support for drag-and-drop category/feed reordering (mihonapp/mihon#1427)

This commit is contained in:
Cuong-Tran 2025-02-26 12:37:10 +07:00
parent 0a6caab64b
commit 69adc4605e
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
15 changed files with 80 additions and 82 deletions

View file

@ -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))

View file

@ -2,16 +2,14 @@ package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
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.browse.components.FeedOrderListItem
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenState
@ -29,7 +27,7 @@ import tachiyomi.presentation.core.util.plus
fun FeedOrderScreen(
state: FeedScreenState,
onClickDelete: (FeedSavedSearch) -> Unit,
changeOrder: (FeedSavedSearch, Int) -> Unit,
onChangeOrder: (FeedSavedSearch, Int) -> Unit,
) {
when {
state.isLoading -> LoadingScreen()
@ -41,31 +39,32 @@ fun FeedOrderScreen(
val lazyListState = rememberLazyListState()
val feeds = state.items
var reorderableList by remember { mutableStateOf(feeds) }
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
reorderableList = reorderableList.toMutableList().apply {
changeOrder(reorderableList[from.index].feed, to.index - from.index)
add(to.index, removeAt(from.index))
}
val feedsState = remember { feeds.toMutableList() }
val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
val item = feedsState.removeAt(from.index)
feedsState.add(to.index, item)
onChangeOrder(item.feed, to.index)
}
LaunchedEffect(feeds) {
if (!reorderableLazyColumnState.isAnyItemDragging) {
reorderableList = feeds
if (!reorderableState.isAnyItemDragging) {
feedsState.clear()
feedsState.addAll(feeds)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
items(
items = reorderableList,
items = feedsState,
key = { it.feed.key },
) { feed ->
ReorderableItem(reorderableLazyColumnState, feed.feed.key) {
ReorderableItem(reorderableState, feed.feed.key) {
FeedOrderListItem(
modifier = Modifier.animateItem(),
title = feed.title,
@ -78,4 +77,4 @@ fun FeedOrderScreen(
}
}
internal val FeedSavedSearch.key get() = "feed-$id"
internal val FeedSavedSearch.key inline get() = "feed-$id"

View file

@ -2,6 +2,7 @@ package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@ -10,10 +11,7 @@ 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.browse.components.FeedOrderListItem
import eu.kanade.presentation.components.AppBar
@ -37,7 +35,7 @@ import tachiyomi.presentation.core.util.plus
fun SourceFeedOrderScreen(
state: SourceFeedState,
onClickDelete: (FeedSavedSearch) -> Unit,
changeOrder: (FeedSavedSearch, Int) -> Unit,
onChangeOrder: (FeedSavedSearch, Int) -> Unit,
onClickSortAlphabetically: () -> Unit,
navigateUp: (() -> Unit)? = null,
) {
@ -75,31 +73,33 @@ fun SourceFeedOrderScreen(
val feeds = state.items
.filterIsInstance<SourceFeedUI.SourceSavedSearch>()
var reorderableList by remember { mutableStateOf(feeds) }
val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to ->
reorderableList = reorderableList.toMutableList().apply {
changeOrder(reorderableList[from.index].feed, to.index - from.index)
add(to.index, removeAt(from.index))
}
val feedsState = remember { feeds.toMutableList() }
val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to ->
val item = feedsState.removeAt(from.index)
feedsState.add(to.index, item)
onChangeOrder(item.feed, to.index)
}
LaunchedEffect(feeds) {
if (!reorderableLazyColumnState.isAnyItemDragging) {
reorderableList = feeds
if (!reorderableState.isAnyItemDragging) {
feedsState.clear()
feedsState.addAll(feeds)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = paddingValues + topSmallPaddingValues +
contentPadding = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
items(
items = reorderableList,
items = feedsState,
key = { it.feed.key },
) { feed ->
ReorderableItem(reorderableLazyColumnState, feed.feed.key) {
ReorderableItem(reorderableState, feed.feed.key) {
FeedOrderListItem(
modifier = Modifier.animateItem(),
title = feed.title,

View file

@ -25,12 +25,11 @@ fun ReorderableCollectionItemScope.FeedOrderListItem(
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
ElevatedCard(
modifier = modifier,
) {
ElevatedCard(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.small)
.padding(
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.medium,
@ -46,11 +45,13 @@ fun ReorderableCollectionItemScope.FeedOrderListItem(
)
Text(
text = title,
modifier = Modifier
.weight(1f),
modifier = Modifier.weight(1f),
)
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),
)
}
}
}

View file

@ -2,6 +2,7 @@ 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
@ -12,10 +13,8 @@ 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.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem
@ -42,7 +41,7 @@ fun CategoryScreen(
onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
changeOrder: (Category, Int) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
// KMK -->
onClickHide: (Category) -> Unit,
// KMK <--
@ -86,11 +85,10 @@ fun CategoryScreen(
CategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues + topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
paddingValues = paddingValues,
onClickRename = onClickRename,
onClickDelete = onClickDelete,
changeOrder = changeOrder,
onChangeOrder = onChangeOrder,
// KMK -->
onClickHide = onClickHide,
// KMK <--
@ -105,35 +103,38 @@ private fun CategoryContent(
paddingValues: PaddingValues,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
changeOrder: (Category, Int) -> Unit,
onChangeOrder: (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))
}
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 (!reorderableLazyColumnState.isAnyItemDragging) {
reorderableList = categories.toList()
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),
) {
items(
items = reorderableList,
items = categoriesState,
key = { category -> category.key },
) { category ->
ReorderableItem(reorderableLazyColumnState, category.key) {
ReorderableItem(reorderableState, category.key) {
CategoryListItem(
modifier = Modifier.animateItem(),
category = category,
@ -148,4 +149,4 @@ private fun CategoryContent(
}
}
private val Category.key get() = "category-$id"
private val Category.key inline get() = "category-$id"

View file

@ -37,13 +37,12 @@ fun ReorderableCollectionItemScope.CategoryListItem(
// KMK <--
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.small,
end = MaterialTheme.padding.medium,
@ -63,8 +62,7 @@ fun ReorderableCollectionItemScope.CategoryListItem(
color = LocalContentColor.current.let { if (category.hidden) it.copy(alpha = 0.6f) else it },
textDecoration = TextDecoration.LineThrough.takeIf { category.hidden },
// KMK <--
modifier = Modifier
.weight(1f),
modifier = Modifier.weight(1f),
)
IconButton(onClick = onRename) {
Icon(
@ -88,7 +86,10 @@ fun ReorderableCollectionItemScope.CategoryListItem(
)
// KMK <--
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),
)
}
}
}

View file

@ -218,9 +218,9 @@ open class FeedScreenModel(
}
// KMK -->
fun changeOrder(feed: FeedSavedSearch, newOrder: Int) {
fun changeOrder(feed: FeedSavedSearch, newIndex: Int) {
screenModelScope.launch {
reorderFeed.changeOrder(feed, newOrder)
reorderFeed.changeOrder(feed, newIndex)
}
}

View file

@ -142,7 +142,7 @@ fun feedTab(
FeedOrderScreen(
state = state,
onClickDelete = screenModel::openDeleteDialog,
changeOrder = screenModel::changeOrder,
onChangeOrder = screenModel::changeOrder,
)
} else {
// KMK <--

View file

@ -105,7 +105,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
SourceFeedOrderScreen(
state = state,
onClickDelete = screenModel::openDeleteFeed,
changeOrder = screenModel::changeOrder,
onChangeOrder = screenModel::changeOrder,
onClickSortAlphabetically = {
screenModel.showDialog(SourceFeedScreenModel.Dialog.SortAlphabetically)
},

View file

@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
@ -156,9 +155,9 @@ open class SourceFeedScreenModel(
}
// KMK -->
fun changeOrder(feed: FeedSavedSearch, newOrder: Int) {
fun changeOrder(feed: FeedSavedSearch, newIndex: Int) {
screenModelScope.launch {
reorderFeed.changeOrder(feed, newOrder, false)
reorderFeed.changeOrder(feed, newIndex, false)
}
}

View file

@ -43,7 +43,7 @@ class CategoryScreen : Screen() {
onClickSortAlphabetically = { screenModel.showDialog(CategoryDialog.SortAlphabetically) },
onClickRename = { screenModel.showDialog(CategoryDialog.Rename(it)) },
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
changeOrder = screenModel::changeOrder,
onChangeOrder = screenModel::changeOrder,
// KMK -->
onClickHide = screenModel::hideCategory,
// KMK <--

View file

@ -89,9 +89,9 @@ class CategoryScreenModel(
}
}
fun changeOrder(category: Category, newOrder: Int) {
fun changeOrder(category: Category, newIndex: Int) {
screenModelScope.launch {
when (reorderCategory.changeOrder(category, newOrder)) {
when (reorderCategory.changeOrder(category, newIndex)) {
is ReorderCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
else -> {}
}

View file

@ -15,7 +15,7 @@ class ReorderCategory(
private val mutex = Mutex()
suspend fun changeOrder(category: Category, newOrder: Int) = withNonCancellableContext {
suspend fun changeOrder(category: Category, newIndex: Int) = withNonCancellableContext {
mutex.withLock {
val categories = categoryRepository.getAll()
.filterNot(Category::isSystemCategory)
@ -26,10 +26,8 @@ class ReorderCategory(
return@withNonCancellableContext Result.Unchanged
}
val newPosition = currentIndex + newOrder
try {
categories.add(newPosition, categories.removeAt(currentIndex))
categories.add(newIndex, categories.removeAt(currentIndex))
val updates = categories.mapIndexed { index, category ->
CategoryUpdate(

View file

@ -15,7 +15,7 @@ class ReorderFeed(
private val mutex = Mutex()
suspend fun changeOrder(feed: FeedSavedSearch, newOrder: Int, global: Boolean = true) = withNonCancellableContext {
suspend fun changeOrder(feed: FeedSavedSearch, newIndex: Int, global: Boolean = true) = withNonCancellableContext {
mutex.withLock {
val feeds = if (global) {
feedSavedSearchRepository.getGlobal()
@ -30,10 +30,8 @@ class ReorderFeed(
return@withNonCancellableContext Result.Unchanged
}
val newPosition = currentIndex + newOrder
try {
feeds.add(newPosition, feeds.removeAt(currentIndex))
feeds.add(newIndex, feeds.removeAt(currentIndex))
val updates = feeds.mapIndexed { index, feed ->
FeedSavedSearchUpdate(

View file

@ -64,10 +64,10 @@ 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" }
palette-ktx = "androidx.palette:palette-ktx:1.0.0"
materialKolor = "com.materialkolor:material-kolor:2.0.2"
haze = "dev.chrisbanes.haze:haze:1.3.1"
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.4.3" }
swipe = "me.saket.swipe:swipe:1.3.0"