Checklists in the body, colour from tags, and commit-derived CalVer #4
@@ -263,11 +263,11 @@ class BoardViewModel(
|
||||
}
|
||||
EditorAction.DismissError -> dismissError()
|
||||
is EditorAction.SaveText -> createFromDraft(action.body)
|
||||
// Starting a checklist is the one toolbar action that means something on
|
||||
// a note with no text: a note whose whole content is its items is a note
|
||||
// this app already has (it is named from its first item). So it may
|
||||
// create an empty one — anything else needs words first.
|
||||
EditorAction.AddChecklist ->
|
||||
// The first checklist item is the one toolbar action that means something
|
||||
// on a note with no text: a note whose whole content is its items is a
|
||||
// note this app already has, named from the first of them. So it may
|
||||
// create a body-less one — anything else needs words first.
|
||||
is EditorAction.AddItem ->
|
||||
createFromDraft(draft.body, allowEmpty = true) { created ->
|
||||
onEditorAction(created, action)
|
||||
}
|
||||
@@ -386,10 +386,6 @@ class BoardViewModel(
|
||||
null
|
||||
}
|
||||
|
||||
// An empty first item: the checklist editor appears the moment the note
|
||||
// has one, and an empty row is what someone can type straight into.
|
||||
EditorAction.AddChecklist -> mutate { it.addItem(id, "") }
|
||||
|
||||
is EditorAction.AddItem ->
|
||||
action.text.trim().takeIf { it.isNotEmpty() }?.let { text ->
|
||||
mutate { it.addItem(id, text) }
|
||||
|
||||
@@ -29,15 +29,6 @@ sealed interface EditorAction {
|
||||
val color: String,
|
||||
) : EditorAction
|
||||
|
||||
/**
|
||||
* Give this note a checklist.
|
||||
*
|
||||
* Not a conversion — a note HAS a checklist rather than BEING one (M13 step 2),
|
||||
* so nothing moves and nothing is swapped: the body stays exactly where it is and
|
||||
* the note gains a first, empty item for someone to type into.
|
||||
*/
|
||||
data object AddChecklist : EditorAction
|
||||
|
||||
data class SetPinned(
|
||||
val pinned: Boolean,
|
||||
) : EditorAction
|
||||
@@ -52,6 +43,16 @@ sealed interface EditorAction {
|
||||
|
||||
data object DeleteForever : EditorAction
|
||||
|
||||
/**
|
||||
* Add an item, given its text.
|
||||
*
|
||||
* There is deliberately no "add an EMPTY item" action. Starting a checklist used
|
||||
* to be one, and it wrote a blank row to the store the moment the toolbar button
|
||||
* was tapped — so the note grew an empty item that had to be typed into while the
|
||||
* add-row sat below it, also empty and also asking to be typed into. Showing the
|
||||
* checklist is now UI state, and the store hears nothing until there is an item
|
||||
* with words in it.
|
||||
*/
|
||||
data class AddItem(
|
||||
val text: String,
|
||||
) : EditorAction
|
||||
|
||||
@@ -13,12 +13,15 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -45,6 +48,7 @@ import com.fabledsword.thoughtsync.core.Note
|
||||
fun ChecklistEditor(
|
||||
note: Note,
|
||||
readOnly: Boolean,
|
||||
focusAddRow: Boolean,
|
||||
onAction: (EditorAction) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
@@ -52,7 +56,10 @@ fun ChecklistEditor(
|
||||
ChecklistRow(item = item, readOnly = readOnly, onAction = onAction)
|
||||
}
|
||||
if (!readOnly) {
|
||||
AddItemRow(onAdd = { onAction(EditorAction.AddItem(it)) })
|
||||
AddItemRow(
|
||||
autoFocus = focusAddRow,
|
||||
onAdd = { onAction(EditorAction.AddItem(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,10 +123,23 @@ private fun ChecklistRow(
|
||||
* through — "milk ⏎ eggs ⏎ bread" — rather than costing a tap between each. That
|
||||
* is the same speed the capture sheet's one-item-per-line field buys, carried into
|
||||
* the editor so refining a list never feels slower than making one.
|
||||
*
|
||||
* [autoFocus] is set when the toolbar was just asked for a checklist, which is the
|
||||
* one moment this row is certainly what someone is reaching for. Keyed on `Unit`
|
||||
* rather than on the flag: the row only enters the composition when the checklist
|
||||
* opens, so once is exactly right, and re-requesting later would yank the caret out
|
||||
* of whatever was being typed.
|
||||
*/
|
||||
@Composable
|
||||
private fun AddItemRow(onAdd: (String) -> Unit) {
|
||||
private fun AddItemRow(
|
||||
autoFocus: Boolean,
|
||||
onAdd: (String) -> Unit,
|
||||
) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
val focus = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
if (autoFocus) focus.requestFocus()
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
@@ -130,7 +150,7 @@ private fun AddItemRow(onAdd: (String) -> Unit) {
|
||||
PlainTextField(
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier.weight(1f).focusRequester(focus),
|
||||
hint = R.string.editor_add_item,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
|
||||
@@ -18,14 +18,17 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -76,7 +79,9 @@ fun EditorTopBar(
|
||||
note: Note,
|
||||
readOnly: Boolean,
|
||||
tint: NoteTint,
|
||||
showAddChecklist: Boolean,
|
||||
onClose: () -> Unit,
|
||||
onStartChecklist: () -> Unit,
|
||||
onPicker: (Picker) -> Unit,
|
||||
onConfirmDelete: () -> Unit,
|
||||
onAction: (EditorAction) -> Unit,
|
||||
@@ -115,12 +120,13 @@ fun EditorTopBar(
|
||||
contentDescription = stringResource(R.string.editor_reminder),
|
||||
)
|
||||
}
|
||||
// Adds the first checklist item, which is what makes the checklist
|
||||
// editor appear. Hidden once the note already has one — there is
|
||||
// nothing left to add that the checklist's own "+" row doesn't do
|
||||
// better.
|
||||
if (note.items.isEmpty()) {
|
||||
IconButton(onClick = { onAction(EditorAction.AddChecklist) }) {
|
||||
// REVEALS the checklist; it does not write one. This used to add an
|
||||
// empty item so the section would have something to render, which
|
||||
// left a blank row above the add-row — two empty fields, and the
|
||||
// caret in the lower one. Hidden once the checklist is showing, since
|
||||
// its own "+" row does the rest better.
|
||||
if (showAddChecklist) {
|
||||
IconButton(onClick = onStartChecklist) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.List,
|
||||
contentDescription = stringResource(R.string.editor_add_checklist),
|
||||
@@ -179,9 +185,12 @@ fun EditorTopBar(
|
||||
* back gesture and TalkBack all expect of a full-screen surface, and removing it
|
||||
* would strand the reflex to strike a duplicate that costs one icon slot.
|
||||
*
|
||||
* A WORD rather than a checkmark, like the overflow menu below and for the same
|
||||
* reason. A tick in a notes app is a checklist item to anyone who has used one, and
|
||||
* "Done" cannot be misread — including aloud.
|
||||
* A checkmark, at the operator's ask. I had shipped the word "Done" here on the
|
||||
* argument that a tick in a NOTES app reads as a checklist item; overruled, and the
|
||||
* filled treatment is what settles it — a tonal button in the note's own colour is
|
||||
* plainly a control, where a bare glyph beside a checklist would not be. It carries
|
||||
* "Done" as its content description, so the reasoning survives where it actually
|
||||
* mattered: read aloud.
|
||||
*
|
||||
* [DateUtils] rather than a hand-rolled formatter: it is localised, it already
|
||||
* knows the difference between minutes, hours and yesterday, and getting plurals
|
||||
@@ -191,9 +200,11 @@ fun EditorTopBar(
|
||||
fun EditorFooter(
|
||||
updatedAt: String?,
|
||||
saving: Boolean,
|
||||
tint: NoteTint,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dark = isSystemInDarkTheme()
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
@@ -204,9 +215,9 @@ fun EditorFooter(
|
||||
// inset down.
|
||||
.imePadding()
|
||||
.navigationBarsPadding()
|
||||
// Asymmetric: the button brings its own padding, the text does not.
|
||||
.padding(start = 16.dp, end = 4.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
// The gap is what keeps the timestamp from reading as the button's label.
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
@@ -214,8 +225,17 @@ fun EditorFooter(
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
TextButton(onClick = onClose) {
|
||||
Text(stringResource(R.string.editor_done))
|
||||
FilledTonalIconButton(
|
||||
onClick = onClose,
|
||||
// The note's own colour rather than the scheme's secondaryContainer,
|
||||
// which would be the one element on a tinted card ignoring the tint.
|
||||
colors =
|
||||
IconButtonDefaults.filledTonalIconButtonColors(
|
||||
containerColor = tint.chipBackground(dark),
|
||||
contentColor = tint.chipForeground(dark),
|
||||
),
|
||||
) {
|
||||
Icon(Icons.Filled.Check, contentDescription = stringResource(R.string.editor_done))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,14 @@ fun NoteEditorScreen(
|
||||
var picker by remember(sessionKey) { mutableStateOf(Picker.NONE) }
|
||||
var confirmingDelete by remember(sessionKey) { mutableStateOf(false) }
|
||||
|
||||
// Whether the checklist is SHOWING, which is not the same as whether the note has
|
||||
// one. Asking for a checklist used to write an empty item, purely so the section
|
||||
// would have something to render — which left a blank row with the add-row beneath
|
||||
// it, both empty, and the caret in the wrong one. Nothing reaches the store now
|
||||
// until an item has words in it. Saveable so a rotation does not close a list
|
||||
// someone is halfway through typing.
|
||||
var checklistOpen by rememberSaveable(sessionKey) { mutableStateOf(false) }
|
||||
|
||||
// A note in the trash is a record, not a document: editing one would silently
|
||||
// resurrect work that was meant to be thrown away. It renders read-only, with
|
||||
// Restore and Delete forever as the only things to do with it.
|
||||
@@ -177,7 +185,9 @@ fun NoteEditorScreen(
|
||||
note = note,
|
||||
readOnly = readOnly,
|
||||
tint = tint,
|
||||
showAddChecklist = !checklistOpen && note.items.isEmpty(),
|
||||
onClose = leave,
|
||||
onStartChecklist = { checklistOpen = true },
|
||||
onPicker = { picker = it },
|
||||
onConfirmDelete = { confirmingDelete = true },
|
||||
onAction = onAction,
|
||||
@@ -191,6 +201,7 @@ fun NoteEditorScreen(
|
||||
EditorFooter(
|
||||
updatedAt = note.updatedAt,
|
||||
saving = saving,
|
||||
tint = tint,
|
||||
onClose = leave,
|
||||
)
|
||||
},
|
||||
@@ -231,11 +242,17 @@ fun NoteEditorScreen(
|
||||
modifier = Modifier.focusRequester(bodyFocus),
|
||||
)
|
||||
|
||||
// Below the body, not instead of it, and only once the note has
|
||||
// items — the toolbar's add-checklist action is what puts the
|
||||
// first one there.
|
||||
if (note.items.isNotEmpty()) {
|
||||
ChecklistEditor(note = note, readOnly = readOnly, onAction = onAction)
|
||||
// Below the body, not instead of it. Shown once the note has
|
||||
// items, or once the toolbar has been asked for a checklist — in
|
||||
// which case the add row takes focus, because being asked for a
|
||||
// list means being about to type one.
|
||||
if (note.items.isNotEmpty() || checklistOpen) {
|
||||
ChecklistEditor(
|
||||
note = note,
|
||||
readOnly = readOnly,
|
||||
focusAddRow = checklistOpen && note.items.isEmpty(),
|
||||
onAction = onAction,
|
||||
)
|
||||
}
|
||||
|
||||
if (note.labels.isNotEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user