diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt index e8e10a04..0cb54278 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt @@ -37,8 +37,10 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.toLocalDateTime +import net.opatry.tasks.app.ui.model.DateRange import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.app.ui.model.compareTo import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.data.TaskRepository import net.opatry.tasks.data.model.TaskDataModel @@ -47,13 +49,28 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds private fun TaskListDataModel.asTaskListUIModel(): TaskListUIModel { - // TODO children - // TODO date formatter + val (completedTasks, remainingTasks) = tasks.map(TaskDataModel::asTaskUIModel).partition(TaskUIModel::isCompleted) + + val taskGroups = when (sorting) { + // no grouping + TaskListSorting.Manual -> mapOf(null to remainingTasks) + TaskListSorting.DueDate -> remainingTasks + .sortedWith { o1, o2 -> o1.dateRange.compareTo(o2.dateRange) } + .groupBy { task -> + when (task.dateRange) { + // merge all overdue tasks to the same range + is DateRange.Overdue -> DateRange.Overdue(LocalDate.fromEpochDays(-1), 1) + else -> task.dateRange + } + } + } + return TaskListUIModel( id = id, title = title, lastUpdate = lastUpdate.toString(), - tasks = tasks.map(TaskDataModel::asTaskUIModel), + remainingTasks = taskGroups.toMap(), + completedTasks = completedTasks, sorting = sorting, ) } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt index ad45f763..fed5793d 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListMenu.kt @@ -166,7 +166,7 @@ private fun TaskListMenuPreview() { ) { IconButton(onClick = { showMenu = true }) { Icon(LucideIcons.EllipsisVertical, null) - TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE", tasks = emptyList(), sorting = TaskListSorting.Manual), showMenu) {} + TaskListMenu(TaskListUIModel(0L, "My task list", "TODO DATE"), showMenu) {} } } } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt index 859507da..7491160a 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt @@ -36,10 +36,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.text.style.TextOverflow import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.model.TaskUIModel @@ -73,15 +69,13 @@ fun TaskMenu( expanded: Boolean, onAction: (TaskAction?) -> Unit ) { - val currentTaskList = taskLists.firstOrNull { it.tasks.map(TaskUIModel::id).contains(task.id) } - val taskPosition by remember(currentTaskList) { mutableStateOf(currentTaskList?.tasks?.indexOf(task) ?: -1) } - val canMoveToTop by remember(task) { derivedStateOf { taskPosition > 0 && task.canIndent } } + val currentTaskList = taskLists.firstOrNull { it.containsTask(task) } DropdownMenu( expanded = expanded, onDismissRequest = { onAction(null) } ) { - if (canMoveToTop) { + if (task.canMoveToTop) { DropdownMenuItem( text = { RowWithIcon(stringResource(Res.string.task_menu_move_to_top)) @@ -101,7 +95,7 @@ fun TaskMenu( ) } - if (task.canIndent && taskPosition > 0) { + if (task.canIndent) { DropdownMenuItem( text = { RowWithIcon(stringResource(Res.string.task_menu_indent)) diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt index 97d76ac0..61d317ec 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt @@ -29,10 +29,17 @@ data class TaskListUIModel( val id: Long, val title: String, val lastUpdate: String, - val tasks: List, - val sorting: TaskListSorting, + val remainingTasks: Map> = emptyMap(), + val completedTasks: List = emptyList(), + val sorting: TaskListSorting = TaskListSorting.Manual, ) { - val isEmpty: Boolean = tasks.isEmpty() - val hasCompletedTasks: Boolean = tasks.any { it.isCompleted } + fun containsTask(task: TaskUIModel, includeCompleted: Boolean = false): Boolean { + return remainingTasks.values.flatten().contains(task) + || (includeCompleted && completedTasks.contains(task)) + } + + val isEmpty: Boolean = remainingTasks.isEmpty() && completedTasks.isEmpty() + val hasCompletedTasks: Boolean = completedTasks.isNotEmpty() + val isEmptyRemainingTasksVisible: Boolean = remainingTasks.isEmpty() && hasCompletedTasks val canDelete: Boolean = true // FIXME default list can't be deleted, how to know it? } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt index f7f61faa..d6448613 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt @@ -77,7 +77,8 @@ data class TaskUIModel( } } + val canMoveToTop: Boolean = false // TODO not in first position in list val canUnindent: Boolean = indent > 0 - val canIndent: Boolean = indent < 1 + val canIndent: Boolean = indent < 1 // TODO & not first position in list val canCreateSubTask: Boolean = indent == 0 } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt index 9ebbcafc..a125760b 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt @@ -53,7 +53,6 @@ import net.opatry.tasks.app.ui.component.RowWithIcon import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview -import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_lists_screen_add_task_list_cta import org.jetbrains.compose.resources.stringResource @@ -136,8 +135,6 @@ private fun TaskListRowScaffold( id = 0L, title = title, "TODO DATE", - tasks = emptyList(), - sorting = TaskListSorting.Manual, ), isSelected = isSelected, onClick = {} diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt index 73292324..70bb570d 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt @@ -125,7 +125,6 @@ import net.opatry.tasks.app.ui.component.TaskMenu import net.opatry.tasks.app.ui.model.DateRange import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.model.TaskUIModel -import net.opatry.tasks.app.ui.model.compareTo import net.opatry.tasks.app.ui.tooling.TaskfolioPreview import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview import net.opatry.tasks.data.TaskListSorting @@ -590,30 +589,11 @@ fun TasksColumn( ) { var showCompleted by remember(taskList.id) { mutableStateOf(false) } - // FIXME remember computation & derived states - val (completedTasks, remainingTasks) = taskList.tasks.partition(TaskUIModel::isCompleted) - - val taskGroups = when (taskList.sorting) { - // no grouping - TaskListSorting.Manual -> mapOf(null to remainingTasks) - TaskListSorting.DueDate -> remainingTasks - .map { it.copy(indent = 0) } - .sortedWith { o1, o2 -> o1.dateRange.compareTo(o2.dateRange) } - .groupBy { task -> - when (task.dateRange) { - // merge all overdue tasks to the same range - is DateRange.Overdue -> DateRange.Overdue(LocalDate.fromEpochDays(-1), 1) - - else -> task.dateRange - } - } - } - LazyColumn( contentPadding = PaddingValues(vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - if (completedTasks.isNotEmpty() && remainingTasks.isEmpty()) { + if (taskList.isEmptyRemainingTasksVisible) { item(key = "all_tasks_complete") { EmptyState( icon = LucideIcons.CheckCheck, @@ -624,7 +604,7 @@ fun TasksColumn( } } - taskGroups.forEach { (dateRange, tasks) -> + taskList.remainingTasks.forEach { (dateRange, tasks) -> if (dateRange != null) { stickyHeader(key = dateRange) { Box( @@ -663,7 +643,7 @@ fun TasksColumn( } } - if (completedTasks.isNotEmpty()) { + if (taskList.hasCompletedTasks) { stickyHeader(key = "completed") { Box( Modifier @@ -682,7 +662,7 @@ fun TasksColumn( } ) { Text( - stringResource(Res.string.task_list_pane_completed_section_title_with_count, completedTasks.size), + stringResource(Res.string.task_list_pane_completed_section_title_with_count, taskList.completedTasks.size), style = MaterialTheme.typography.titleSmall ) } @@ -691,7 +671,7 @@ fun TasksColumn( } if (showCompleted) { - items(completedTasks, key = TaskUIModel::id) { task -> + items(taskList.completedTasks, key = TaskUIModel::id) { task -> CompletedTaskRow( task, onAction = { action -> diff --git a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt index 3c173c85..effa7e08 100644 --- a/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt +++ b/tasks-core/src/commonMain/kotlin/net/opatry/tasks/data/TaskRepository.kt @@ -141,7 +141,7 @@ fun sortTasksManualOrdering(tasks: List): List // Step 3: Sort the child tasks by position tree.forEach { (_, children) -> - children.sortBy { it.position } + children.sortBy(TaskEntity::position) } // Step 4: Recursive function to traverse tasks and assign indentation levels