UI: fix subtask progress chip on the task list

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 10:25:08 +02:00
parent 24cf8fe331
commit 3397e57794
3 changed files with 41 additions and 15 deletions

View File

@@ -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<Task> {
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<Long, Pair<Int, Int>> = 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)
}

View File

@@ -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

View File

@@ -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(