diff --git a/app/src/main/java/com/flowintent/workspace/ui/SwipeableCard.kt b/app/src/main/java/com/flowintent/workspace/ui/SwipeableCard.kt index 15afdec..8e22be1 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/SwipeableCard.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/SwipeableCard.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text 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 @@ -44,6 +45,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.flowintent.core.db.Task import com.flowintent.workspace.nav.OpenTaskDialog import com.flowintent.workspace.ui.vm.TaskViewModel @@ -54,9 +56,10 @@ import kotlin.math.roundToInt fun SwipeableCard( modifier: Modifier = Modifier, task: Task, - viewModel: TaskViewModel, + viewModel: TaskViewModel = hiltViewModel(), onDelete: () -> Unit, onEdit: () -> Unit, + onHeightChange: (Dp) -> Unit, content: @Composable () -> Unit ) { val isExpanded = viewModel.expandedMap[task.uid] ?: false @@ -71,6 +74,10 @@ fun SwipeableCard( label = "cardHeight" ) + LaunchedEffect(cardHeight) { + onHeightChange(cardHeight) + } + fun interpolateDp(offset: Float, maxSwipe: Float, start: Dp, end: Dp): Dp { val fraction = (-offset / maxSwipe).coerceIn(0f, 1f) return end + (start - end) * fraction @@ -104,7 +111,6 @@ fun SwipeableCard( Box( modifier = modifier .fillMaxWidth() - .padding(start = 12.dp, top = 12.dp, end = 12.dp) .height(cardHeight) .background(Color.Transparent) ) { diff --git a/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt b/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt index ebe0a14..b21c6a3 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt @@ -1,6 +1,8 @@ package com.flowintent.workspace.ui +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -8,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Label import androidx.compose.material.icons.filled.Category @@ -20,16 +22,27 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.flowintent.core.db.DragInfo +import com.flowintent.core.db.Task +import com.flowintent.core.db.calculateNewIndex +import com.flowintent.core.db.swap import com.flowintent.workspace.nav.ToDoNavTopBar import com.flowintent.workspace.ui.vm.TaskViewModel import com.flowintent.workspace.util.asString @@ -101,36 +114,92 @@ fun ListActionBar(paddingTopOffset: PaddingValues) { @Composable private fun ListCardContent(viewModel: TaskViewModel = hiltViewModel()) { val taskList by viewModel.tasks.collectAsStateWithLifecycle() + val list = remember(taskList) { taskList.toMutableStateList() } + var draggingItem by remember { mutableStateOf(null) } + var itemHeight by remember { mutableStateOf(50.dp) } + LazyColumn( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - items(taskList) { task -> - SwipeableCard( - task = task, - onDelete = { viewModel.deleteTask(task) }, - onEdit = { viewModel.setUpdateTaskId(task.uid) }, - viewModel = viewModel + itemsIndexed(list, key = { index: Int, task: Task -> task.uid }) { index, task -> + val isDragging = draggingItem?.index == index + + Box( + modifier = Modifier + .graphicsLayer { + if (isDragging) { + val heightPx = itemHeight.toPx() + translationY = draggingItem?.let { drag -> + drag.offsetY - drag.touchOffset + heightPx / 2f + } ?: 0f + shadowElevation = 16.dp.toPx() + scaleX = 1.02f + scaleY = 1.02f + } + } + .zIndex(if (isDragging) 1f else 0f) + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDragStart = { offset -> + draggingItem = DragInfo( + index = index, + offsetY = 0f, + touchOffset = offset.y + ) + }, + onDrag = { change, dragAmount -> + change.consume() + val drag = draggingItem ?: return@detectDragGesturesAfterLongPress + + draggingItem = drag.copy( + offsetY = drag.offsetY + dragAmount.y + ) + + val newIndex = calculateNewIndex( + draggingItem!!, + list.size, + itemHeight = itemHeight.toPx() + ) + if (newIndex != draggingItem!!.index) { + list.swap(draggingItem!!.index, newIndex) + draggingItem = draggingItem!!.copy( + index = newIndex, + offsetY = 0f + ) + } + }, + onDragEnd = { draggingItem = null }, + onDragCancel = { draggingItem = null } + ) + } ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) + SwipeableCard( + task = task, + onDelete = { viewModel.deleteTask(task) }, + onEdit = { viewModel.setUpdateTaskId(task.uid) }, + onHeightChange = { itemHeight = it }, + modifier = Modifier.padding(start = 12.dp, top = 12.dp, end = 12.dp) ) { - Text( - text = task.title, - fontSize = 16.sp, - fontFamily = FontFamily.SansSerif, - fontWeight = FontWeight.Bold - ) - Text( - text = task.content.asString(), - modifier = Modifier.padding(top = 12.dp), - fontSize = 16.sp, - fontFamily = FontFamily.SansSerif, - fontWeight = FontWeight.Bold - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) { + Text( + text = task.title, + fontSize = 16.sp, + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold + ) + Text( + text = task.content.asString(), + modifier = Modifier.padding(top = 12.dp), + fontSize = 16.sp, + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold + ) + } } } } diff --git a/app/src/main/java/com/flowintent/workspace/ui/vm/TaskViewModel.kt b/app/src/main/java/com/flowintent/workspace/ui/vm/TaskViewModel.kt index a261532..e1434cc 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/vm/TaskViewModel.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/vm/TaskViewModel.kt @@ -30,9 +30,6 @@ class TaskViewModel @Inject constructor( private val _updateTaskId = MutableStateFlow(null) val updateTaskId: StateFlow = _updateTaskId.asStateFlow() - private val _deleteResult = MutableStateFlow(null) - val deleteResult: StateFlow = _deleteResult.asStateFlow() - private val _expandedMap = mutableStateMapOf() val expandedMap: Map get() = _expandedMap @@ -60,8 +57,7 @@ class TaskViewModel @Inject constructor( fun deleteTask(task: Task) { viewModelScope.launch { - val isDeleted = repository.deleteTask(task) > 0 - _deleteResult.value = isDeleted + repository.deleteTask(task) } } diff --git a/core/src/main/java/com/flowintent/core/db/DragInfo.kt b/core/src/main/java/com/flowintent/core/db/DragInfo.kt new file mode 100644 index 0000000..5f929c0 --- /dev/null +++ b/core/src/main/java/com/flowintent/core/db/DragInfo.kt @@ -0,0 +1,20 @@ +package com.flowintent.core.db + +data class DragInfo( + val index: Int, + val offsetY: Float, + val touchOffset: Float = 0f +) + +fun calculateNewIndex(dragInfo: DragInfo, size: Int, itemHeight: Float): Int { + val offsetItems = (dragInfo.offsetY / itemHeight).toInt() + return (dragInfo.index + offsetItems).coerceIn(0, size - 1) +} + +fun MutableList.swap(from: Int, to: Int) { + if (from == to) { + return + } + val item = removeAt(from) + add(to, item) +}