diff --git a/donextv2/build.gradle.kts b/donextv2/build.gradle.kts index a94586b..2156648 100644 --- a/donextv2/build.gradle.kts +++ b/donextv2/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.9.4") implementation("androidx.hilt:hilt-navigation-compose:1.3.0") implementation("androidx.test.ext:junit-ktx:1.3.0") + implementation("sh.calvin.reorderable:reorderable:3.0.0") androidTestImplementation(platform("androidx.compose:compose-bom:2025.09.00")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt index 285d8ec..1a9b715 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt @@ -31,6 +31,7 @@ interface TaskListDao { LEFT JOIN tasks t ON t.task_list_id = tl.id WHERE tl.deleted = 0 GROUP BY tl.id + ORDER BY tl.display_order ASC """) fun getTaskListsWithOverdue(nowMillis: Long): Flow> diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt index c27e9ec..db7b2c3 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/ManageListsScreen.kt @@ -1,5 +1,10 @@ package com.wismna.geoffroy.donext.presentation.screen +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,17 +14,18 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -32,12 +38,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.customActions import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.ManageListsViewModel +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -46,56 +58,132 @@ fun ManageListsScreen( viewModel: ManageListsViewModel = hiltViewModel(), showAddListSheet: () -> Unit ) { - val lists = viewModel.taskLists + var lists = viewModel.taskLists.toMutableList() + val lazyListState = rememberLazyListState() + val reorderState = rememberReorderableLazyListState( + lazyListState = lazyListState, + onMove = { from, to -> + viewModel.moveTaskList(from.index, to.index) + } + ) - LazyColumn(modifier = modifier.fillMaxWidth().padding()) { + LazyColumn(modifier = modifier.fillMaxWidth().padding(), state = lazyListState) { itemsIndexed(lists, key = { _, list -> list.id!! }) { index, list -> var isInEditMode by remember { mutableStateOf(false) } var editedName by remember { mutableStateOf(list.name) } - ListItem( - modifier = Modifier.animateItem(), - headlineContent = { - if (isInEditMode) { - OutlinedTextField( - value = editedName, - onValueChange = { editedName = it }, - singleLine = true - ) - } else { - Text(list.name) - } - }, - trailingContent = { - if (isInEditMode) { - Row { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { - IconButton(onClick = { isInEditMode = false }) { - Icon(Icons.Default.Close, contentDescription = "Cancel") - } - IconButton(onClick = { - viewModel.updateTaskListName(list.copy(name = editedName)) - isInEditMode = false - }) { - Icon(Icons.Default.Check, contentDescription = "Save") - } + ReorderableItem( + state = reorderState, + key = list.id!! + ) { + val interactionSource = remember { MutableInteractionSource() } + Card( + onClick = {}, + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 5.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + ), + modifier = Modifier.draggableHandle( + onDragStopped = { + viewModel.commitTaskListOrder() + }, + interactionSource = interactionSource, + ) + .clearAndSetSemantics { + customActions = listOf( + CustomAccessibilityAction( + label = "Move Up", + action = { + if (index > 0) { + lists = lists.toMutableList().apply { + add(index - 1, removeAt(index)) + } + true + } else { + false + } + } + ), + CustomAccessibilityAction( + label = "Move Down", + action = { + if (index < lists.size - 1) { + lists = lists.toMutableList().apply { + add(index + 1, removeAt(index)) + } + true + } else { + false + } + } + ), + ) + }, + interactionSource = interactionSource, + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedContent( + //modifier = Modifier.padding(start = 12.dp), + targetState = isInEditMode, + transitionSpec = { + fadeIn() togetherWith fadeOut() + }, + label = "Headline transition" + ) { isEditing -> + if (isEditing) { + OutlinedTextField( + value = editedName, + onValueChange = { editedName = it }, + singleLine = true + ) + } else { + Text(list.name) } } - } else { - Row { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { - IconButton(onClick = { isInEditMode = true }) { - Icon(Icons.Default.Edit, contentDescription = "Edit") + AnimatedContent( + targetState = isInEditMode, + transitionSpec = { + fadeIn() togetherWith fadeOut() + }, + label = "Trailing transition" + ) { editing -> + if (editing) { + Row { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { + IconButton(onClick = { isInEditMode = false }) { + Icon(Icons.Default.Close, contentDescription = "Cancel") + } + IconButton(onClick = { + viewModel.updateTaskListName(list.copy(name = editedName)) + isInEditMode = false + }) { + Icon(Icons.Default.Check, contentDescription = "Save") + } + } } - IconButton(onClick = { viewModel.deleteTaskList(list.id!!) }) { - Icon(Icons.Default.Delete, contentDescription = "Delete") + } else { + Row { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { + IconButton(onClick = { isInEditMode = true }) { + Icon(Icons.Default.Edit, contentDescription = "Edit") + } + IconButton(onClick = { viewModel.deleteTaskList(list.id!!) }) { + Icon( + Icons.Default.Delete, + contentDescription = "Delete" + ) + } + } } } } } } - ) - HorizontalDivider() + } } } } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt index e17fcfb..6c343fb 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt @@ -61,7 +61,7 @@ fun TaskBottomSheet( ModalBottomSheet(onDismissRequest = onDismiss) { Column(Modifier.padding(16.dp)) { Text( - "New Task", + if (viewModel.isEditing()) "Edit Task" else "New Task", style = MaterialTheme.typography.titleLarge ) Spacer(Modifier.height(8.dp)) diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt index a0e0a79..04af77f 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt @@ -40,7 +40,9 @@ class MainViewModel @Inject constructor( AppDestination.TaskList(taskList.id!!, taskList.name) } + AppDestination.ManageLists isLoading = false - if (!destinations.isEmpty()) startDestination = destinations.first() + if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) { + startDestination = destinations.first() + } } .launchIn(viewModelScope) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt index 038cbcf..a53af14 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/ManageListsViewModel.kt @@ -50,4 +50,21 @@ class ManageListsViewModel @Inject constructor( deleteTaskListUseCase(taskId) } } + + fun moveTaskList(fromIndex: Int, toIndex: Int) { + val mutable = taskLists.toMutableList() + val item = mutable.removeAt(fromIndex) + mutable.add(toIndex, item) + taskLists = mutable + } + + fun commitTaskListOrder() { + viewModelScope.launch { + taskLists.forEachIndexed { index, list -> + if (list.order != index) { + updateTaskListUseCase(list.id!!, list.name, index) + } + } + } + } } \ No newline at end of file