tasks: quiet single-line row meta, lazy subtasks in smart lists, polish
- Collapse a task row's supporting info (priority, due, subtask progress, list name) into one low-emphasis meta line instead of stacked filled chips: priority is a tinted flag, the list name carries its list colour, and dates are compact (formatDateTimeCompact). - Expand subtasks in smart lists too, by lazily fetching a parent's full child set on demand (TasksRepository.subtasks + the view model's live lazyChildren), while per-list views stay query-free. - animateItem on the rows so (un)expanding a group fades its subtasks in/out and slides the rows below — clean whether the children were already loaded or arrive a frame later. - Sit the undo chip beside the FAB at its height rather than overlapping or floating above it. Bumps the floret-kit submodule for formatDateTimeCompact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,13 @@ enum class ProviderStatus { READY, NEEDS_PERMISSION, NO_PROVIDER }
|
||||
interface TasksRepository {
|
||||
fun taskLists(): Flow<List<TaskList>>
|
||||
fun tasks(filter: TaskFilter): Flow<List<Task>>
|
||||
|
||||
/**
|
||||
* The direct children of [parentId], re-emitting live like the other flows.
|
||||
* Lets a smart-list view pull a parent's full subtask set on demand — those
|
||||
* children are usually filtered out of the list's own (date-based) query.
|
||||
*/
|
||||
fun subtasks(parentId: Long): Flow<List<Task>>
|
||||
fun taskDetail(taskId: Long): Flow<TaskDetail?>
|
||||
|
||||
suspend fun createTask(form: TaskForm): Long
|
||||
|
||||
@@ -35,6 +35,12 @@ class TasksRepositoryImpl @Inject constructor(
|
||||
|
||||
override fun tasks(filter: TaskFilter): Flow<List<Task>> = observing { loadTasks(filter) }
|
||||
|
||||
override fun subtasks(parentId: Long): Flow<List<Task>> = observing {
|
||||
dataSource.subtasks(parentId)
|
||||
.filter { it.taskId != parentId }
|
||||
.sortedWith(TaskSorting.DEFAULT)
|
||||
}
|
||||
|
||||
override fun taskDetail(taskId: Long): Flow<TaskDetail?> = observing {
|
||||
dataSource.task(taskId)?.let { task ->
|
||||
TaskDetail(
|
||||
|
||||
@@ -45,6 +45,27 @@ fun priorityFill(priority: Priority, dark: Boolean): Color {
|
||||
return Color(android.graphics.Color.HSVToColor(hsv))
|
||||
}
|
||||
|
||||
/**
|
||||
* Foreground tint (icon/text) for a priority level — the same red/amber/green as
|
||||
* [priorityFill], but kept saturated enough to read as a small glyph directly on
|
||||
* the surface rather than as a soft fill. Used by the task list's quiet meta line,
|
||||
* where priority is a tinted flag instead of a filled pill. [Priority.NONE] is
|
||||
* unspecified (callers render it neutral / omit it).
|
||||
*/
|
||||
fun priorityAccent(priority: Priority, dark: Boolean): Color {
|
||||
val base = when (priority) {
|
||||
Priority.HIGH -> 0xFFE53935.toInt() // red
|
||||
Priority.MEDIUM -> 0xFFFB8C00.toInt() // orange-amber (darker than the fill for contrast)
|
||||
Priority.LOW -> 0xFF43A047.toInt() // green
|
||||
Priority.NONE -> return Color.Unspecified
|
||||
}
|
||||
val hsv = FloatArray(3)
|
||||
android.graphics.Color.colorToHSV(base, hsv)
|
||||
hsv[1] = if (dark) 0.55f else 0.85f
|
||||
hsv[2] = if (dark) 0.85f else 0.62f // light on dark surfaces; deep on light ones
|
||||
return Color(android.graphics.Color.HSVToColor(hsv))
|
||||
}
|
||||
|
||||
/**
|
||||
* A coloured status pill for a task's priority: a flag plus the level's [label],
|
||||
* filled in that level's pastel hue. Used read-only in the list and detail
|
||||
@@ -91,14 +112,14 @@ fun PriorityChip(
|
||||
|
||||
val body: @Composable () -> Unit = {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (showIcon) {
|
||||
Icon(Icons.Rounded.Flag, contentDescription = null, tint = iconTint, modifier = Modifier.size(15.dp))
|
||||
Icon(Icons.Rounded.Flag, contentDescription = null, tint = iconTint, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
Text(label, style = MaterialTheme.typography.labelMedium, color = content)
|
||||
Text(label, style = MaterialTheme.typography.labelSmall, color = content)
|
||||
}
|
||||
}
|
||||
val shape = RoundedCornerShape(50)
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Checklist
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Flag
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
@@ -88,10 +89,9 @@ import de.jeanlucmakiola.agendula.domain.Task
|
||||
import de.jeanlucmakiola.agendula.domain.TaskFilter
|
||||
import de.jeanlucmakiola.agendula.domain.TaskSection
|
||||
import de.jeanlucmakiola.agendula.domain.TaskSections
|
||||
import de.jeanlucmakiola.agendula.ui.common.ListNameChip
|
||||
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import de.jeanlucmakiola.agendula.ui.common.PriorityChip
|
||||
import de.jeanlucmakiola.floret.time.formatDateTime
|
||||
import de.jeanlucmakiola.floret.time.formatDateTimeCompact
|
||||
import de.jeanlucmakiola.floret.components.pastelize
|
||||
import de.jeanlucmakiola.floret.components.positionOf
|
||||
import java.time.ZoneId
|
||||
@@ -163,16 +163,24 @@ fun TaskListScreen(
|
||||
TaskListUiState.Failure -> CenteredMessage(stringResource(R.string.task_list_failure), inner)
|
||||
is TaskListUiState.Content -> TaskListBody(s, filter, inner, onOpenTask, onRequestDelete, viewModel)
|
||||
}
|
||||
UndoChip(
|
||||
visible = undoTarget != null,
|
||||
onUndo = {
|
||||
undoTarget?.let { viewModel.undoDelete(it.taskId) }
|
||||
undoTarget = null
|
||||
},
|
||||
// A FAB-height band anchored at the bottom-start with the FAB's own
|
||||
// margin; centring the chip in it lines the chip up beside the bottom-end
|
||||
// FAB at exactly its height, rather than sitting a touch above it.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = inner.calculateBottomPadding() + 24.dp),
|
||||
)
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(start = 16.dp, bottom = inner.calculateBottomPadding() + 16.dp)
|
||||
.height(56.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
UndoChip(
|
||||
visible = undoTarget != null,
|
||||
onUndo = {
|
||||
undoTarget?.let { viewModel.undoDelete(it.taskId) }
|
||||
undoTarget = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +259,23 @@ private fun TaskListBody(
|
||||
val childrenByParent = remember(state.tasks) {
|
||||
state.tasks.filter { it.isSubtask }.groupBy { it.parentId!! }
|
||||
}
|
||||
// In a smart list a parent's subtasks are usually filtered out (they're undated
|
||||
// or due other days), so we pull them on demand. Report only expanded parents
|
||||
// whose full child set isn't already in this view; the VM then observes those
|
||||
// children live. In a real list that set is empty, so no extra queries run.
|
||||
val tasksById = remember(state.tasks) { state.tasks.associateBy { it.taskId } }
|
||||
val parentsNeedingChildren = expandedParents.filter { id ->
|
||||
val parent = tasksById[id]
|
||||
parent != null && (childrenByParent[id]?.size ?: 0) != parent.subtaskTotal
|
||||
}.toSet()
|
||||
LaunchedEffect(parentsNeedingChildren) { viewModel.setLazyChildParents(parentsNeedingChildren) }
|
||||
val lazyChildren by viewModel.lazyChildren.collectAsStateWithLifecycle()
|
||||
// A parent's full child set: the rows already in this view when we hold them
|
||||
// all, otherwise the lazily-loaded set fetched for smart lists.
|
||||
fun childrenOf(task: Task): List<Task> {
|
||||
val inList = childrenByParent[task.taskId].orEmpty()
|
||||
return if (inList.size == task.subtaskTotal) inList else lazyChildren[task.taskId].orEmpty()
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -295,19 +320,21 @@ private fun TaskListBody(
|
||||
val rows = buildList {
|
||||
val topLevel = section.tasks
|
||||
topLevel.forEachIndexed { p, task ->
|
||||
val children = childrenByParent[task.taskId].orEmpty()
|
||||
// Only expandable when we hold every child: true in a real
|
||||
// list, but a smart list may omit children due on other
|
||||
// days, and a half-shown set would mislead.
|
||||
val canExpand = task.subtaskTotal > 0 && children.size == task.subtaskTotal
|
||||
// The full child set — already in this view for a real list,
|
||||
// or fetched on demand for a smart list (childrenOf). A parent
|
||||
// is expandable whenever it has children; until a smart list's
|
||||
// set has loaded the group simply shows nothing extra.
|
||||
val children = childrenOf(task)
|
||||
val canExpand = task.subtaskTotal > 0
|
||||
val isExpanded = canExpand && task.taskId in expandedParents
|
||||
val hasExpansionContent = children.isNotEmpty() || state.showAddSubtaskRow
|
||||
// Top-level tasks keep their own connected run — full top
|
||||
// only on the first, full bottom only on the last — so the
|
||||
// task after an expanded one stays mid-run (small top). An
|
||||
// expanded parent's bottom opens (small) to meet its subtasks.
|
||||
val parentPosition = cornerPosition(
|
||||
topFull = p == 0,
|
||||
bottomFull = if (isExpanded) false else p == topLevel.lastIndex,
|
||||
bottomFull = if (isExpanded && hasExpansionContent) false else p == topLevel.lastIndex,
|
||||
)
|
||||
add(RenderRow(ListRow.Parent(task, canExpand, isExpanded), parentPosition))
|
||||
if (isExpanded) {
|
||||
@@ -331,8 +358,13 @@ private fun TaskListBody(
|
||||
}
|
||||
itemsIndexed(rows, key = { _, r -> r.row.key }) { index, r ->
|
||||
val lastInRun = index == rows.lastIndex
|
||||
// animateItem so an (un)expanding group fades its subtasks in/out
|
||||
// and slides the rows below — clean whether the children were
|
||||
// already loaded (real list) or arrive a frame later (smart list).
|
||||
val rowModifier = Modifier.animateItem()
|
||||
when (val row = r.row) {
|
||||
is ListRow.Parent -> TaskRow(
|
||||
modifier = rowModifier,
|
||||
task = row.task,
|
||||
position = r.position,
|
||||
lastInRun = lastInRun,
|
||||
@@ -356,6 +388,7 @@ private fun TaskListBody(
|
||||
onClick = { onOpenTask(row.task.taskId) },
|
||||
)
|
||||
is ListRow.Sub -> SubtaskRow(
|
||||
modifier = rowModifier,
|
||||
task = row.task,
|
||||
position = r.position,
|
||||
lastInRun = lastInRun,
|
||||
@@ -363,6 +396,7 @@ private fun TaskListBody(
|
||||
onClick = { onOpenTask(row.task.taskId) },
|
||||
)
|
||||
is ListRow.AddSub -> AddSubtaskRow(
|
||||
modifier = rowModifier,
|
||||
position = r.position,
|
||||
lastInRun = lastInRun,
|
||||
onAdd = { viewModel.quickAddSubtask(row.parent, it) },
|
||||
@@ -435,6 +469,7 @@ private fun TaskRow(
|
||||
onToggle: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = { value ->
|
||||
@@ -455,7 +490,7 @@ private fun TaskRow(
|
||||
|
||||
SwipeToDismissBox(
|
||||
state = dismissState,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||
// dismissDirection (not targetValue) tracks the live drag, so the colour +
|
||||
// icon reveal as you swipe rather than only once the threshold is crossed.
|
||||
backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) },
|
||||
@@ -482,10 +517,11 @@ private fun TaskRowContent(
|
||||
val full by animateDpAsState(if (pressed) 36.dp else 22.dp, label = "fullCorner")
|
||||
val small by animateDpAsState(if (pressed) 36.dp else 6.dp, label = "smallCorner")
|
||||
val dark = isSystemInDarkTheme()
|
||||
val due = task.due?.formatDateTime(task.isAllDay)
|
||||
val due = task.due?.formatDateTimeCompact(task.isAllDay)
|
||||
val listName = task.listName?.takeIf { showListName && it.isNotBlank() }
|
||||
val hasSupporting = due != null || listName != null || subtaskTotal > 0 ||
|
||||
task.priority != Priority.NONE
|
||||
// Everything secondary collapses into one quiet supporting line below the title.
|
||||
val hasMeta = task.priority != Priority.NONE || due != null || subtaskTotal > 0 ||
|
||||
listName != null
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
@@ -526,29 +562,17 @@ private fun TaskRowContent(
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
)
|
||||
if (hasSupporting) {
|
||||
if (hasMeta) {
|
||||
Spacer(Modifier.height(3.dp))
|
||||
// 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 (task.priority != Priority.NONE) {
|
||||
PriorityChip(task.priority, priorityLabel(task.priority))
|
||||
}
|
||||
if (due != null) {
|
||||
Text(
|
||||
text = due,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (listName != null) ListNameChip(listName, task.effectiveColor)
|
||||
if (subtaskTotal > 0) SubtaskCountChip(subtaskDone, subtaskTotal)
|
||||
}
|
||||
TaskMetaLine(
|
||||
priority = task.priority,
|
||||
due = due,
|
||||
subtaskDone = subtaskDone,
|
||||
subtaskTotal = subtaskTotal,
|
||||
listName = listName,
|
||||
listColor = task.effectiveColor,
|
||||
dark = dark,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Trailing area is only ever the expand control, so the row's right
|
||||
@@ -561,29 +585,83 @@ private fun TaskRowContent(
|
||||
}
|
||||
}
|
||||
|
||||
/** A small neutral pill showing subtask progress, e.g. "3 / 5". */
|
||||
/**
|
||||
* The single low-emphasis supporting line under a task title: priority, due date,
|
||||
* subtask progress and (in mixed views) the list name, joined by middots. The text
|
||||
* is quiet `onSurfaceVariant` throughout; the only colour is the priority flag and
|
||||
* the list name (tinted to its list) — so even a fully-populated row stays calm and
|
||||
* one line instead of a stack of filled pills.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SubtaskCountChip(done: Int, total: Int) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
shape = RoundedCornerShape(50),
|
||||
private fun TaskMetaLine(
|
||||
priority: Priority,
|
||||
due: String?,
|
||||
subtaskDone: Int,
|
||||
subtaskTotal: Int,
|
||||
listName: String?,
|
||||
listColor: Int,
|
||||
dark: Boolean,
|
||||
) {
|
||||
val muted = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val style = MaterialTheme.typography.bodySmall
|
||||
val segments = buildList<@Composable () -> Unit> {
|
||||
if (priority != Priority.NONE) {
|
||||
add {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Flag,
|
||||
contentDescription = null,
|
||||
tint = priorityAccent(priority, dark),
|
||||
modifier = Modifier.size(13.dp),
|
||||
)
|
||||
Text(priorityLabel(priority), style = style, color = muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (due != null) add { Text(due, style = style, color = muted) }
|
||||
if (subtaskTotal > 0) {
|
||||
add {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ListAlt,
|
||||
contentDescription = null,
|
||||
tint = muted,
|
||||
modifier = Modifier.size(13.dp),
|
||||
)
|
||||
Text("$subtaskDone / $subtaskTotal", style = style, color = muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listName != null) {
|
||||
add {
|
||||
Text(
|
||||
listName,
|
||||
style = style,
|
||||
color = pastelize(listColor, dark),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowRow(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ListAlt,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
Text(
|
||||
text = "$done / $total",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
segments.forEachIndexed { i, segment ->
|
||||
// A middot between segments — its own flow item, so the line can still
|
||||
// wrap gracefully when truly everything is set on a narrow screen.
|
||||
if (i > 0) {
|
||||
Text("·", style = style, color = muted, modifier = Modifier.padding(horizontal = 6.dp))
|
||||
}
|
||||
segment()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,6 +677,7 @@ private fun AddSubtaskRow(
|
||||
position: Position,
|
||||
lastInRun: Boolean,
|
||||
onAdd: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
fun submit() {
|
||||
@@ -612,7 +691,7 @@ private fun AddSubtaskRow(
|
||||
shape = cardShape(position, 22.dp, 6.dp),
|
||||
// Same tone as the subtask rows it closes, so the group reads as one unit.
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp).padding(start = 12.dp, end = 8.dp),
|
||||
@@ -725,6 +804,7 @@ private fun SubtaskRow(
|
||||
lastInRun: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
val pressed by interaction.collectIsPressedAsState()
|
||||
@@ -738,7 +818,7 @@ private fun SubtaskRow(
|
||||
// reads as a distinct, recessive tone — the nesting cue alongside shape.
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
interactionSource = interaction,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -87,6 +88,32 @@ class TaskListViewModel @Inject constructor(
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), TaskListUiState.Loading)
|
||||
|
||||
/**
|
||||
* Parents the screen has expanded whose children it can't pull from the list
|
||||
* itself — i.e. smart lists, where the date filter drops a parent's subtasks.
|
||||
* For each we observe the full child set live (below), so the group can expand.
|
||||
*/
|
||||
private val lazyChildParents = MutableStateFlow<Set<Long>>(emptySet())
|
||||
|
||||
/**
|
||||
* The on-demand child sets, keyed by parent id, kept live. Empty for real
|
||||
* lists (the screen reports no parents there, since it already holds every
|
||||
* child) so no redundant queries run.
|
||||
*/
|
||||
val lazyChildren: StateFlow<Map<Long, List<Task>>> =
|
||||
lazyChildParents
|
||||
.flatMapLatest { ids ->
|
||||
if (ids.isEmpty()) {
|
||||
flowOf(emptyMap())
|
||||
} else {
|
||||
combine(ids.map { id -> repository.subtasks(id).map { id to it } }) { it.toMap() }
|
||||
}
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
|
||||
|
||||
/** The screen reports which expanded parents need their children fetched. */
|
||||
fun setLazyChildParents(ids: Set<Long>) { lazyChildParents.value = ids }
|
||||
|
||||
fun bind(taskFilter: TaskFilter) { filter.value = taskFilter }
|
||||
|
||||
fun toggleComplete(task: Task) = viewModelScope.launch {
|
||||
|
||||
Submodule floret-kit updated: 86f44805d1...aee26a28f3
Reference in New Issue
Block a user