From 00b75ded186931deb93e019ae4afbc7edcb06ba0 Mon Sep 17 00:00:00 2001 From: Olivier Patry Date: Mon, 30 Sep 2024 14:04:29 +0200 Subject: [PATCH] =?UTF-8?q?Add=20"Add=20task=20list=E2=80=A6"=20button=20a?= =?UTF-8?q?t=20top=20of=20the=20tasks=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/screen/taskListScreenAndroid.kt | 9 ++-- .../net/opatry/tasks/app/ui/TasksApp.kt | 23 +++++++++- .../tasks/app/ui/component/taskListsUI.kt | 44 ++++++++++++++++--- .../tasks/app/ui/screen/taskListScreen.kt | 5 ++- .../tasks/app/ui/screen/taskListScreenJvm.kt | 9 ++-- 5 files changed, 77 insertions(+), 13 deletions(-) diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenAndroid.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenAndroid.kt index b6b7b434..4630a218 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenAndroid.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenAndroid.kt @@ -45,7 +45,10 @@ import org.jetbrains.compose.resources.stringResource @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -actual fun TaskListsMasterDetail(viewModel: TaskListsViewModel) { +actual fun TaskListsMasterDetail( + viewModel: TaskListsViewModel, + onNewTaskList: (String) -> Unit +) { val taskLists by viewModel.taskLists.collectAsState(emptyList()) // need to store a saveable (Serializable/Parcelable) object @@ -63,16 +66,16 @@ actual fun TaskListsMasterDetail(viewModel: TaskListsViewModel) { listPane = { AnimatedPane { if (taskLists.isEmpty()) { - // TODO dialog to ask for the new task list name val newTaskListName = stringResource(Res.string.default_task_list_title) NoTaskListEmptyState { - viewModel.createTaskList(newTaskListName) + onNewTaskList(newTaskListName) } } else { Row { TaskListsColumn( taskLists, selectedItem = taskLists.find { it.id == navigator.currentDestination?.content }, + onNewTaskList = { onNewTaskList("") }, onItemClick = { taskList -> navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, taskList.id) } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt index defa146d..20679d1e 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp +import net.opatry.tasks.app.ui.component.EditTextDialog import net.opatry.tasks.app.ui.component.MissingScreen import net.opatry.tasks.app.ui.component.ProfileIcon import net.opatry.tasks.app.ui.screen.TaskListsMasterDetail @@ -88,6 +89,9 @@ fun TasksApp(userViewModel: UserViewModel, tasksViewModel: TaskListsViewModel) { tasksViewModel.enableAutoRefresh(isFocused && isSigned) } + var newTaskListDefaultTitle by remember { mutableStateOf("") } + var showNewTaskListDialog by remember { mutableStateOf(false) } + NavigationSuiteScaffold(navigationSuiteItems = { // Only if expanded state if (false) { @@ -134,7 +138,24 @@ fun TasksApp(userViewModel: UserViewModel, tasksViewModel: TaskListsViewModel) { } } - TaskListsMasterDetail(tasksViewModel) + TaskListsMasterDetail(tasksViewModel) { title -> + newTaskListDefaultTitle = title + showNewTaskListDialog = true + } + + if (showNewTaskListDialog) { + EditTextDialog( + onDismissRequest = { showNewTaskListDialog = false }, + validateLabel = "Create", + onValidate = { title -> + showNewTaskListDialog = false + tasksViewModel.createTaskList(title) + }, + dialogTitle = "New task list", + initialText = newTaskListDefaultTitle, + allowBlank = false + ) + } } AppTasksScreen.Calendar -> MissingScreen(stringResource(AppTasksScreen.Calendar.labelRes), LucideIcons.Calendar) diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/taskListsUI.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/taskListsUI.kt index d8df3679..fd77b824 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/taskListsUI.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/taskListsUI.kt @@ -22,18 +22,28 @@ package net.opatry.tasks.app.ui.component +import CircleFadingPlus +import LucideIcons +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background 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.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,16 +54,39 @@ import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +@OptIn(ExperimentalFoundationApi::class) @Composable fun TaskListsColumn( taskLists: List, selectedItem: TaskListUIModel? = null, + onNewTaskList: () -> Unit, onItemClick: (TaskListUIModel) -> Unit ) { - LazyColumn(contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + val listState = rememberLazyListState() + LazyColumn(state = listState, verticalArrangement = Arrangement.spacedBy(8.dp)) { + stickyHeader { + Box( + Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + // TODO could be a "in-place" replace with a text field (no border) + TextButton( + onClick = onNewTaskList, + ) { + RowWithIcon("Add task list…", LucideIcons.CircleFadingPlus) + } + } + + AnimatedVisibility(listState.firstVisibleItemScrollOffset > 0) { + HorizontalDivider() + } + } items(taskLists) { taskList -> TaskListRow( taskList, + Modifier.padding(horizontal = 8.dp), isSelected = taskList.id == selectedItem?.id, onClick = { onItemClick(taskList) } ) @@ -64,6 +97,7 @@ fun TaskListsColumn( @Composable fun TaskListRow( taskList: TaskListUIModel, + modifier: Modifier = Modifier, isSelected: Boolean = false, onClick: () -> Unit ) { @@ -72,7 +106,7 @@ fun TaskListRow( else -> Color.Transparent } - Card(colors = CardDefaults.outlinedCardColors(), onClick = onClick) { + Card(onClick = onClick, modifier = modifier, colors = CardDefaults.outlinedCardColors()) { ListItem( headlineContent = { Text(taskList.title, overflow = TextOverflow.Ellipsis, maxLines = 1) @@ -96,8 +130,8 @@ private fun TaskListRowScaffold( "TODO DATE", tasks = emptyList(), ), - isSelected, - {} + isSelected = isSelected, + onClick = {} ) } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt index 5a4a0dea..ec55781b 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt @@ -26,4 +26,7 @@ import androidx.compose.runtime.Composable import net.opatry.tasks.app.ui.TaskListsViewModel @Composable -expect fun TaskListsMasterDetail(viewModel: TaskListsViewModel) \ No newline at end of file +expect fun TaskListsMasterDetail( + viewModel: TaskListsViewModel, + onNewTaskList: (String) -> Unit +) \ No newline at end of file diff --git a/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenJvm.kt b/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenJvm.kt index 853c6101..afb97a7d 100644 --- a/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenJvm.kt +++ b/tasks-app-shared/src/jvmMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreenJvm.kt @@ -43,7 +43,10 @@ import net.opatry.tasks.resources.default_task_list_title import org.jetbrains.compose.resources.stringResource @Composable -actual fun TaskListsMasterDetail(viewModel: TaskListsViewModel) { +actual fun TaskListsMasterDetail( + viewModel: TaskListsViewModel, + onNewTaskList: (String) -> Unit +) { val taskLists by viewModel.taskLists.collectAsState(emptyList()) // Store the list id, and not the list object to prevent keeping @@ -52,16 +55,16 @@ actual fun TaskListsMasterDetail(viewModel: TaskListsViewModel) { Row(Modifier.fillMaxWidth()) { if (taskLists.isEmpty()) { - // TODO dialog to ask for the new task list name val newTaskListName = stringResource(Res.string.default_task_list_title) NoTaskListEmptyState { - viewModel.createTaskList(newTaskListName) + onNewTaskList(newTaskListName) } } else { Box(Modifier.weight(.3f)) { TaskListsColumn( taskLists, selectedItem = taskLists.find { it.id == currentTaskListId }, + onNewTaskList = { onNewTaskList("") }, onItemClick = { taskList -> currentTaskListId = taskList.id }