From 5d1c69484a6bb48ed41a9101e5595821d037d863 Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 06:39:10 +0200 Subject: [PATCH] feat: [#25] drag & drop dans la vue liste du projet (long-press handle, reorderTasks) --- .../mobile/ui/components/DragHandle.kt | 109 ++++++++++++++++++ .../mobile/ui/project/ProjectScreen.kt | 48 ++++++-- .../mobile/ui/project/ProjectViewModel.kt | 4 + 3 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/planify/mobile/ui/components/DragHandle.kt diff --git a/app/src/main/java/com/planify/mobile/ui/components/DragHandle.kt b/app/src/main/java/com/planify/mobile/ui/components/DragHandle.kt new file mode 100644 index 0000000..a60b648 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/DragHandle.kt @@ -0,0 +1,109 @@ +package com.planify.mobile.ui.components + +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DragHandle +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.zIndex +import kotlinx.coroutines.launch + +/** State for a reorderable LazyColumn. */ +class ReorderState { + var draggedIndex by mutableStateOf(null) + var dragOffsetY by mutableFloatStateOf(0f) + var overIndex by mutableIntStateOf(-1) + + fun isDragged(index: Int) = draggedIndex == index + fun isOver(index: Int) = overIndex == index +} + +@Composable +fun rememberReorderState() = remember { ReorderState() } + +/** + * Modifier for a drag handle icon that drives a [ReorderState]. + * Call [onReorder] when the drag ends with the new list order. + */ +@Composable +fun Modifier.reorderDragHandle( + item: T, + index: Int, + items: List, + state: ReorderState, + listState: LazyListState, + onReorder: (List) -> Unit, +): Modifier { + val scope = rememberCoroutineScope() + return this.pointerInput(item, items) { + detectDragGesturesAfterLongPress( + onDragStart = { + state.draggedIndex = index + state.dragOffsetY = 0f + state.overIndex = index + }, + onDrag = { change, dragAmount -> + change.consume() + state.dragOffsetY += dragAmount.y + val currentInfo: LazyListItemInfo? = listState.layoutInfo.visibleItemsInfo + .firstOrNull { it.index == (state.draggedIndex ?: -1) } + if (currentInfo != null) { + val currentCenter = currentInfo.offset + currentInfo.size / 2 + state.dragOffsetY.toInt() + val newOver = listState.layoutInfo.visibleItemsInfo + .minByOrNull { kotlin.math.abs(it.offset + it.size / 2 - currentCenter) } + ?.index ?: state.overIndex + state.overIndex = newOver + } + }, + onDragEnd = { + val from = state.draggedIndex ?: return@detectDragGesturesAfterLongPress + val to = state.overIndex + if (from != to && to >= 0 && to < items.size) { + val mutable = items.toMutableList() + val moved = mutable.removeAt(from) + mutable.add(to, moved) + scope.launch { onReorder(mutable) } + } + state.draggedIndex = null + state.dragOffsetY = 0f + state.overIndex = -1 + }, + onDragCancel = { + state.draggedIndex = null + state.dragOffsetY = 0f + state.overIndex = -1 + }, + ) + } +} + +/** Modifier to apply drag visual feedback (elevation + offset) to a dragged item. */ +fun Modifier.draggedItemModifier(isDragged: Boolean, offsetY: Float): Modifier = + if (isDragged) this + .zIndex(1f) + .graphicsLayer { translationY = offsetY; shadowElevation = 8f } + else this + +@Composable +fun DragHandleIcon(modifier: Modifier = Modifier) { + Icon( + imageVector = Icons.Outlined.DragHandle, + contentDescription = "Réordonner", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/planify/mobile/ui/project/ProjectScreen.kt b/app/src/main/java/com/planify/mobile/ui/project/ProjectScreen.kt index a4c3752..615b7d1 100644 --- a/app/src/main/java/com/planify/mobile/ui/project/ProjectScreen.kt +++ b/app/src/main/java/com/planify/mobile/ui/project/ProjectScreen.kt @@ -2,6 +2,7 @@ package com.planify.mobile.ui.project import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.FolderOpen import androidx.compose.material3.Card @@ -21,14 +23,20 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.planify.mobile.domain.model.Task import com.planify.mobile.domain.model.ViewStyle +import com.planify.mobile.ui.components.DragHandleIcon import com.planify.mobile.ui.components.EmptyState +import com.planify.mobile.ui.components.ReorderState import com.planify.mobile.ui.components.SectionHeader import com.planify.mobile.ui.components.TaskRow +import com.planify.mobile.ui.components.draggedItemModifier +import com.planify.mobile.ui.components.reorderDragHandle +import com.planify.mobile.ui.components.rememberReorderState @Composable fun ProjectScreen( @@ -62,6 +70,7 @@ fun ProjectScreen( }, onTaskClick = onTaskClick, onCheckedChange = { task -> viewModel.toggleTask(task) }, + onReorder = { viewModel.reorderTasks(it) }, modifier = modifier, ) ViewStyle.BOARD -> ProjectBoardView( @@ -80,9 +89,13 @@ private fun ProjectListView( onToggleSection: (String) -> Unit, onTaskClick: (Task) -> Unit, onCheckedChange: (Task) -> Unit, + onReorder: (List) -> Unit, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier.fillMaxSize()) { + val listState = rememberLazyListState() + val reorderState = rememberReorderState() + + LazyColumn(state = listState, modifier = modifier.fillMaxSize()) { state.sections.forEach { group -> val key = group.section?.id ?: "unsectioned" val name = group.section?.name ?: "Sans section" @@ -98,12 +111,33 @@ private fun ProjectListView( } if (key !in collapsedSections) { - items(group.tasks, key = { it.id }) { task -> - TaskRow( - task = task, - onCheckedChange = { onCheckedChange(task) }, - onClick = { onTaskClick(task) }, - ) + itemsIndexed(group.tasks, key = { _, t -> t.id }) { index, task -> + Row( + modifier = Modifier + .fillMaxWidth() + .draggedItemModifier( + isDragged = reorderState.isDragged(index), + offsetY = if (reorderState.isDragged(index)) reorderState.dragOffsetY else 0f, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + DragHandleIcon( + modifier = Modifier.reorderDragHandle( + item = task, + index = index, + items = group.tasks, + state = reorderState, + listState = listState, + onReorder = onReorder, + ) + ) + TaskRow( + task = task, + onCheckedChange = { onCheckedChange(task) }, + onClick = { onTaskClick(task) }, + modifier = Modifier.weight(1f), + ) + } } item { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) } } diff --git a/app/src/main/java/com/planify/mobile/ui/project/ProjectViewModel.kt b/app/src/main/java/com/planify/mobile/ui/project/ProjectViewModel.kt index 8eb532d..46631c1 100644 --- a/app/src/main/java/com/planify/mobile/ui/project/ProjectViewModel.kt +++ b/app/src/main/java/com/planify/mobile/ui/project/ProjectViewModel.kt @@ -68,4 +68,8 @@ class ProjectViewModel @Inject constructor( fun toggleTask(task: Task) { viewModelScope.launch { taskRepository.checkTask(task.id, !task.checked) } } + + fun reorderTasks(reordered: List) { + viewModelScope.launch { taskRepository.reorderTasks(reordered.map { it.id }) } + } }