mirror of
https://github.com/mihonapp/mihon.git
synced 2025-01-13 03:37:15 +01:00
drag to reorder Category list
This commit is contained in:
parent
79e711efc2
commit
d420f98d31
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 -> {}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user