Checklists in the body, colour from tags, and commit-derived CalVer #4

Merged
bvandeusen merged 73 commits from dev into main 2026-08-29 13:39:45 -04:00
3 changed files with 34 additions and 14 deletions
Showing only changes of commit 315c5f19e6 - Show all commits
@@ -141,7 +141,12 @@ fun BoardScreen(
when {
state.loading -> LoadingBoard()
state.notes.isEmpty() -> EmptyBoard(state)
else -> NoteBoard(notes = state.notes, onOpenNote = onOpenNote)
else ->
NoteBoard(
notes = state.notes,
onOpenNote = onOpenNote,
onToggleItem = onToggleItem,
)
}
// `PullToRefreshBox` would be less code, but it takes no
// `enabled`, so the modifier and the indicator are wired by
@@ -324,6 +329,7 @@ private fun DrawerRow(
private fun NoteBoard(
notes: List<Note>,
onOpenNote: (Note) -> Unit,
onToggleItem: (Note, Int, Boolean) -> Unit,
) {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(BOARD_COLUMNS),
@@ -109,7 +109,11 @@ private fun NoteBody(
val index = itemAtLine[n]
val item = index?.let { note.items.getOrNull(it) }
when {
item != null -> ChecklistRow(item) { onToggleItem(index, !item.checked) }
// Both in the condition, not just `item`: Kotlin will not infer that a
// non-null item implies a non-null index, and `onToggleItem` needs the
// index smart-cast to act on it.
index != null && item != null ->
ChecklistRow(item) { onToggleItem(index, !item.checked) }
// Kept as a gap rather than dropped: it is the paragraph break
// somebody typed, and the card reads as a wall without it.
line.isBlank() -> Spacer(Modifier.height(4.dp))
@@ -396,21 +396,31 @@ private fun continueChecklist(
new: TextFieldValue,
): TextFieldValue {
val caret = new.selection.start
if (new.selection.length != 0) return new
if (new.text.length != old.text.length + 1) return new
if (caret == 0 || new.text[caret - 1] != '\n') return new
val typedNewline =
new.selection.length == 0 &&
new.text.length == old.text.length + 1 &&
caret > 0 &&
new.text[caret - 1] == '\n'
// Both guarded on `typedNewline`, because caret - 1 is only a real index once it
// is known to be the newline that was just typed.
val lineStart = if (typedNewline) new.text.take(caret - 1).lastIndexOf('\n') + 1 else 0
val marker =
if (typedNewline) checklistContinuation(new.text.substring(lineStart, caret - 1)) else null
val before = new.text.take(caret - 1)
val lineStart = before.lastIndexOf('\n') + 1
val marker = checklistContinuation(before.substring(lineStart)) ?: return new
if (marker.isEmpty()) {
return when {
marker == null -> new
// An empty item: take the marker line away rather than adding another.
val text = new.text.take(lineStart) + new.text.substring(caret)
return TextFieldValue(text, TextRange(lineStart))
marker.isEmpty() ->
TextFieldValue(
new.text.take(lineStart) + new.text.substring(caret),
TextRange(lineStart),
)
else ->
TextFieldValue(
new.text.take(caret) + marker + new.text.substring(caret),
TextRange(caret + marker.length),
)
}
val text = new.text.take(caret) + marker + new.text.substring(caret)
return TextFieldValue(text, TextRange(caret + marker.length))
}
private const val CHECKLIST_MARKER = "- [ ] "