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:
@@ -2,7 +2,6 @@ package de.jeanlucmakiola.floret.data.tasks
|
|||||||
|
|
||||||
import de.jeanlucmakiola.floret.data.di.IoDispatcher
|
import de.jeanlucmakiola.floret.data.di.IoDispatcher
|
||||||
import de.jeanlucmakiola.floret.domain.DayWindow
|
import de.jeanlucmakiola.floret.domain.DayWindow
|
||||||
import de.jeanlucmakiola.floret.domain.SmartList
|
|
||||||
import de.jeanlucmakiola.floret.domain.Task
|
import de.jeanlucmakiola.floret.domain.Task
|
||||||
import de.jeanlucmakiola.floret.domain.TaskDetail
|
import de.jeanlucmakiola.floret.domain.TaskDetail
|
||||||
import de.jeanlucmakiola.floret.domain.TaskFilter
|
import de.jeanlucmakiola.floret.domain.TaskFilter
|
||||||
@@ -44,11 +43,28 @@ class TasksRepositoryImpl @Inject constructor(
|
|||||||
private fun loadTasks(filter: TaskFilter): List<Task> {
|
private fun loadTasks(filter: TaskFilter): List<Task> {
|
||||||
val query = when (filter) {
|
val query = when (filter) {
|
||||||
is TaskFilter.OfList -> TaskQuery(listId = filter.listId, includeCompleted = true)
|
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())
|
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) }
|
.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)
|
.sortedWith(TaskSorting.DEFAULT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ data class Task(
|
|||||||
val distanceFromCurrent: Int?,
|
val distanceFromCurrent: Int?,
|
||||||
val created: Instant?,
|
val created: Instant?,
|
||||||
val lastModified: 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 isCompleted: Boolean get() = status == TaskStatus.COMPLETED
|
||||||
val isClosed: Boolean get() = status == TaskStatus.COMPLETED || status == TaskStatus.CANCELLED
|
val isClosed: Boolean get() = status == TaskStatus.COMPLETED || status == TaskStatus.CANCELLED
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -147,12 +149,10 @@ private fun TaskListBody(
|
|||||||
viewModel: TaskListViewModel,
|
viewModel: TaskListViewModel,
|
||||||
) {
|
) {
|
||||||
val (todayStart, todayEnd) = remember { DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) }
|
val (todayStart, todayEnd) = remember { DayWindow.today(Clock.System.now(), ZoneId.systemDefault()) }
|
||||||
// A parent represents its children via a progress chip, so drop children
|
// A parent represents its children via a progress chip (counts come from the
|
||||||
// from the flat list — unless their parent isn't in this view (can happen in
|
// repository, over the full set), so drop children from the flat list — unless
|
||||||
// smart lists), where we keep them standalone so nothing silently vanishes.
|
// their parent isn't in this view (can happen in smart lists), where we keep
|
||||||
val childrenByParent = remember(state.tasks) {
|
// them standalone so nothing silently vanishes.
|
||||||
state.tasks.filter { (it.parentId ?: 0L) > 0L }.groupBy { it.parentId!! }
|
|
||||||
}
|
|
||||||
val displayTasks = remember(state.tasks) {
|
val displayTasks = remember(state.tasks) {
|
||||||
val present = state.tasks.mapTo(HashSet()) { it.taskId }
|
val present = state.tasks.mapTo(HashSet()) { it.taskId }
|
||||||
state.tasks.filter { (it.parentId ?: 0L) <= 0L || it.parentId !in present }
|
state.tasks.filter { (it.parentId ?: 0L) <= 0L || it.parentId !in present }
|
||||||
@@ -201,13 +201,12 @@ private fun TaskListBody(
|
|||||||
}
|
}
|
||||||
if (!collapsible || completedExpanded) {
|
if (!collapsible || completedExpanded) {
|
||||||
itemsIndexed(section.tasks, key = { _, t -> t.taskId }) { index, task ->
|
itemsIndexed(section.tasks, key = { _, t -> t.taskId }) { index, task ->
|
||||||
val children = childrenByParent[task.taskId]
|
|
||||||
TaskRow(
|
TaskRow(
|
||||||
task = task,
|
task = task,
|
||||||
position = positionOf(index, section.tasks.size),
|
position = positionOf(index, section.tasks.size),
|
||||||
showListName = showListName,
|
showListName = showListName,
|
||||||
subtaskDone = children?.count { it.isCompleted } ?: 0,
|
subtaskDone = task.subtaskDone,
|
||||||
subtaskTotal = children?.size ?: 0,
|
subtaskTotal = task.subtaskTotal,
|
||||||
onToggle = { viewModel.toggleComplete(task) },
|
onToggle = { viewModel.toggleComplete(task) },
|
||||||
onDelete = { viewModel.delete(task) },
|
onDelete = { viewModel.delete(task) },
|
||||||
onClick = { onOpenTask(task.taskId) },
|
onClick = { onOpenTask(task.taskId) },
|
||||||
@@ -300,7 +299,7 @@ private fun TaskRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TaskRowContent(
|
private fun TaskRowContent(
|
||||||
task: Task,
|
task: Task,
|
||||||
@@ -361,9 +360,13 @@ private fun TaskRowContent(
|
|||||||
)
|
)
|
||||||
if (hasSupporting) {
|
if (hasSupporting) {
|
||||||
Spacer(Modifier.height(3.dp))
|
Spacer(Modifier.height(3.dp))
|
||||||
Row(
|
// Flow so the chips wrap to a further line when due date + list
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
// + 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),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
itemVerticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (due != null) {
|
if (due != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
Reference in New Issue
Block a user