Tasks lists are now re-orderable

Edit task bottom sheet displays proper header
This commit is contained in:
Geoffroy Bonneville
2025-09-18 18:54:53 -04:00
parent 2be67abffa
commit 336755666b
6 changed files with 150 additions and 41 deletions

View File

@@ -56,6 +56,7 @@ dependencies {
implementation("androidx.navigation:navigation-compose:2.9.4") implementation("androidx.navigation:navigation-compose:2.9.4")
implementation("androidx.hilt:hilt-navigation-compose:1.3.0") implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
implementation("androidx.test.ext:junit-ktx: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(platform("androidx.compose:compose-bom:2025.09.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling")

View File

@@ -31,6 +31,7 @@ interface TaskListDao {
LEFT JOIN tasks t ON t.task_list_id = tl.id LEFT JOIN tasks t ON t.task_list_id = tl.id
WHERE tl.deleted = 0 WHERE tl.deleted = 0
GROUP BY tl.id GROUP BY tl.id
ORDER BY tl.display_order ASC
""") """)
fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>> fun getTaskListsWithOverdue(nowMillis: Long): Flow<List<TaskListWithOverdue>>

View File

@@ -1,5 +1,10 @@
package com.wismna.geoffroy.donext.presentation.screen 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
@@ -32,12 +38,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.ManageListsViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.ManageListsViewModel
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -46,17 +58,83 @@ fun ManageListsScreen(
viewModel: ManageListsViewModel = hiltViewModel(), viewModel: ManageListsViewModel = hiltViewModel(),
showAddListSheet: () -> Unit 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 -> itemsIndexed(lists, key = { _, list -> list.id!! }) { index, list ->
var isInEditMode by remember { mutableStateOf(false) } var isInEditMode by remember { mutableStateOf(false) }
var editedName by remember { mutableStateOf(list.name) } var editedName by remember { mutableStateOf(list.name) }
ListItem( ReorderableItem(
modifier = Modifier.animateItem(), state = reorderState,
headlineContent = { key = list.id!!
if (isInEditMode) { ) {
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( OutlinedTextField(
value = editedName, value = editedName,
onValueChange = { editedName = it }, onValueChange = { editedName = it },
@@ -65,9 +143,15 @@ fun ManageListsScreen(
} else { } else {
Text(list.name) Text(list.name)
} }
}
AnimatedContent(
targetState = isInEditMode,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}, },
trailingContent = { label = "Trailing transition"
if (isInEditMode) { ) { editing ->
if (editing) {
Row { Row {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
IconButton(onClick = { isInEditMode = false }) { IconButton(onClick = { isInEditMode = false }) {
@@ -88,14 +172,18 @@ fun ManageListsScreen(
Icon(Icons.Default.Edit, contentDescription = "Edit") Icon(Icons.Default.Edit, contentDescription = "Edit")
} }
IconButton(onClick = { viewModel.deleteTaskList(list.id!!) }) { IconButton(onClick = { viewModel.deleteTaskList(list.id!!) }) {
Icon(Icons.Default.Delete, contentDescription = "Delete") Icon(
} Icons.Default.Delete,
} contentDescription = "Delete"
}
}
}
) )
HorizontalDivider() }
}
}
}
}
}
}
}
} }
} }
} }

View File

@@ -61,7 +61,7 @@ fun TaskBottomSheet(
ModalBottomSheet(onDismissRequest = onDismiss) { ModalBottomSheet(onDismissRequest = onDismiss) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(16.dp)) {
Text( Text(
"New Task", if (viewModel.isEditing()) "Edit Task" else "New Task",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))

View File

@@ -40,7 +40,9 @@ class MainViewModel @Inject constructor(
AppDestination.TaskList(taskList.id!!, taskList.name) AppDestination.TaskList(taskList.id!!, taskList.name)
} + AppDestination.ManageLists } + AppDestination.ManageLists
isLoading = false isLoading = false
if (!destinations.isEmpty()) startDestination = destinations.first() if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) {
startDestination = destinations.first()
}
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }

View File

@@ -50,4 +50,21 @@ class ManageListsViewModel @Inject constructor(
deleteTaskListUseCase(taskId) 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)
}
}
}
}
} }