From 3397e57794dc571d324f675d95edb682795631b9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 23 Jun 2026 10:25:08 +0200 Subject: [PATCH] UI: fix subtask progress chip on the task list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Count a parent's subtask progress over the full task set in the repository (attached to Task as subtaskTotal/subtaskDone) so completed children are counted even on smart lists that hide closed tasks — the chip showed "0 / 2" instead of "18 / 20". List membership (and the home overview counts) are unchanged; only the parent's derived progress is. Also wrap the task-row metadata in a FlowRow so the subtask chip drops to a further line when a dated task with subtasks can't fit due + chips on one line, rather than squeezing the trailing chip. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../floret/data/tasks/TasksRepositoryImpl.kt | 22 ++++++++++++--- .../de/jeanlucmakiola/floret/domain/Models.kt | 7 +++++ .../floret/ui/tasklist/TaskListScreen.kt | 27 ++++++++++--------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/floret/data/tasks/TasksRepositoryImpl.kt b/app/src/main/java/de/jeanlucmakiola/floret/data/tasks/TasksRepositoryImpl.kt index 22c34be..9c0dda1 100644 --- a/app/src/main/java/de/jeanlucmakiola/floret/data/tasks/TasksRepositoryImpl.kt +++ b/app/src/main/java/de/jeanlucmakiola/floret/data/tasks/TasksRepositoryImpl.kt @@ -2,7 +2,6 @@ package de.jeanlucmakiola.floret.data.tasks import de.jeanlucmakiola.floret.data.di.IoDispatcher import de.jeanlucmakiola.floret.domain.DayWindow -import de.jeanlucmakiola.floret.domain.SmartList import de.jeanlucmakiola.floret.domain.Task import de.jeanlucmakiola.floret.domain.TaskDetail import de.jeanlucmakiola.floret.domain.TaskFilter @@ -44,11 +43,28 @@ class TasksRepositoryImpl @Inject constructor( private fun loadTasks(filter: TaskFilter): List { val query = when (filter) { is TaskFilter.OfList -> TaskQuery(listId = filter.listId, includeCompleted = true) - is TaskFilter.Smart -> TaskQuery(includeCompleted = filter.list == SmartList.COMPLETED) + // Read completed tasks too, then let [TaskFiltering.matches] enforce each + // smart list's own open/closed semantics. We over-read on purpose so a + // parent's subtask-progress reflects ALL its children — completed ones + // included — even on smart lists that hide completed tasks themselves. + is TaskFilter.Smart -> TaskQuery(includeCompleted = true) } val (todayStart, todayEnd) = DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) - return dataSource.tasks(query) + val all = dataSource.tasks(query) + // (done, total) of direct children per parent, over the unfiltered set. + val progress: Map> = all + .filter { (it.parentId ?: 0L) > 0L } + .groupingBy { it.parentId!! } + .fold(0 to 0) { (done, total), t -> + (if (t.isCompleted) done + 1 else done) to (total + 1) + } + return all .filter { TaskFiltering.matches(it, filter, todayStart, todayEnd) } + .map { task -> + progress[task.taskId]?.let { (done, total) -> + task.copy(subtaskDone = done, subtaskTotal = total) + } ?: task + } .sortedWith(TaskSorting.DEFAULT) } diff --git a/app/src/main/java/de/jeanlucmakiola/floret/domain/Models.kt b/app/src/main/java/de/jeanlucmakiola/floret/domain/Models.kt index aee2a93..2777437 100644 --- a/app/src/main/java/de/jeanlucmakiola/floret/domain/Models.kt +++ b/app/src/main/java/de/jeanlucmakiola/floret/domain/Models.kt @@ -52,6 +52,13 @@ data class Task( val distanceFromCurrent: Int?, val created: Instant?, val lastModified: Instant?, + /** + * Direct-subtask progress, filled in by the repository over the *full* task + * set so a parent's progress chip counts completed children even on smart + * lists that hide them. Both 0 when the task has no subtasks. + */ + val subtaskTotal: Int = 0, + val subtaskDone: Int = 0, ) { val isCompleted: Boolean get() = status == TaskStatus.COMPLETED val isClosed: Boolean get() = status == TaskStatus.COMPLETED || status == TaskStatus.CANCELLED diff --git a/app/src/main/java/de/jeanlucmakiola/floret/ui/tasklist/TaskListScreen.kt b/app/src/main/java/de/jeanlucmakiola/floret/ui/tasklist/TaskListScreen.kt index 1bab709..620d864 100644 --- a/app/src/main/java/de/jeanlucmakiola/floret/ui/tasklist/TaskListScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/floret/ui/tasklist/TaskListScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -147,12 +149,10 @@ private fun TaskListBody( viewModel: TaskListViewModel, ) { val (todayStart, todayEnd) = remember { DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) } - // A parent represents its children via a progress chip, so drop children - // from the flat list — unless their parent isn't in this view (can happen in - // smart lists), where we keep them standalone so nothing silently vanishes. - val childrenByParent = remember(state.tasks) { - state.tasks.filter { (it.parentId ?: 0L) > 0L }.groupBy { it.parentId!! } - } + // A parent represents its children via a progress chip (counts come from the + // repository, over the full set), so drop children from the flat list — unless + // their parent isn't in this view (can happen in smart lists), where we keep + // them standalone so nothing silently vanishes. val displayTasks = remember(state.tasks) { val present = state.tasks.mapTo(HashSet()) { it.taskId } state.tasks.filter { (it.parentId ?: 0L) <= 0L || it.parentId !in present } @@ -201,13 +201,12 @@ private fun TaskListBody( } if (!collapsible || completedExpanded) { itemsIndexed(section.tasks, key = { _, t -> t.taskId }) { index, task -> - val children = childrenByParent[task.taskId] TaskRow( task = task, position = positionOf(index, section.tasks.size), showListName = showListName, - subtaskDone = children?.count { it.isCompleted } ?: 0, - subtaskTotal = children?.size ?: 0, + subtaskDone = task.subtaskDone, + subtaskTotal = task.subtaskTotal, onToggle = { viewModel.toggleComplete(task) }, onDelete = { viewModel.delete(task) }, onClick = { onOpenTask(task.taskId) }, @@ -300,7 +299,7 @@ private fun TaskRow( } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable private fun TaskRowContent( task: Task, @@ -361,9 +360,13 @@ private fun TaskRowContent( ) if (hasSupporting) { Spacer(Modifier.height(3.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, + // Flow so the chips wrap to a further line when due date + list + // + subtask progress can't share one (e.g. a dated task with + // subtasks), rather than squeezing the trailing chip. + FlowRow( + verticalArrangement = Arrangement.spacedBy(3.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), + itemVerticalAlignment = Alignment.CenterVertically, ) { if (due != null) { Text(