Checklists in the body, colour from tags, and commit-derived CalVer #4
@@ -263,11 +263,11 @@ class BoardViewModel(
|
|||||||
}
|
}
|
||||||
EditorAction.DismissError -> dismissError()
|
EditorAction.DismissError -> dismissError()
|
||||||
is EditorAction.SaveText -> createFromDraft(action.body)
|
is EditorAction.SaveText -> createFromDraft(action.body)
|
||||||
// Starting a checklist is the one toolbar action that means something on
|
// The first checklist item is the one toolbar action that means something
|
||||||
// a note with no text: a note whose whole content is its items is a note
|
// on a note with no text: a note whose whole content is its items is a
|
||||||
// this app already has (it is named from its first item). So it may
|
// note this app already has, named from the first of them. So it may
|
||||||
// create an empty one — anything else needs words first.
|
// create a body-less one — anything else needs words first.
|
||||||
EditorAction.AddChecklist ->
|
is EditorAction.AddItem ->
|
||||||
createFromDraft(draft.body, allowEmpty = true) { created ->
|
createFromDraft(draft.body, allowEmpty = true) { created ->
|
||||||
onEditorAction(created, action)
|
onEditorAction(created, action)
|
||||||
}
|
}
|
||||||
@@ -386,10 +386,6 @@ class BoardViewModel(
|
|||||||
null
|
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 ->
|
is EditorAction.AddItem ->
|
||||||
action.text.trim().takeIf { it.isNotEmpty() }?.let { text ->
|
action.text.trim().takeIf { it.isNotEmpty() }?.let { text ->
|
||||||
mutate { it.addItem(id, text) }
|
mutate { it.addItem(id, text) }
|
||||||
|
|||||||
@@ -29,15 +29,6 @@ sealed interface EditorAction {
|
|||||||
val color: String,
|
val color: String,
|
||||||
) : EditorAction
|
) : 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(
|
data class SetPinned(
|
||||||
val pinned: Boolean,
|
val pinned: Boolean,
|
||||||
) : EditorAction
|
) : EditorAction
|
||||||
@@ -52,6 +43,16 @@ sealed interface EditorAction {
|
|||||||
|
|
||||||
data object DeleteForever : 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(
|
data class AddItem(
|
||||||
val text: String,
|
val text: String,
|
||||||
) : EditorAction
|
) : EditorAction
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.focus.onFocusChanged
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -45,6 +48,7 @@ import com.fabledsword.thoughtsync.core.Note
|
|||||||
fun ChecklistEditor(
|
fun ChecklistEditor(
|
||||||
note: Note,
|
note: Note,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
|
focusAddRow: Boolean,
|
||||||
onAction: (EditorAction) -> Unit,
|
onAction: (EditorAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
@@ -52,7 +56,10 @@ fun ChecklistEditor(
|
|||||||
ChecklistRow(item = item, readOnly = readOnly, onAction = onAction)
|
ChecklistRow(item = item, readOnly = readOnly, onAction = onAction)
|
||||||
}
|
}
|
||||||
if (!readOnly) {
|
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
|
* 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
|
* 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.
|
* 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
|
@Composable
|
||||||
private fun AddItemRow(onAdd: (String) -> Unit) {
|
private fun AddItemRow(
|
||||||
|
autoFocus: Boolean,
|
||||||
|
onAdd: (String) -> Unit,
|
||||||
|
) {
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (autoFocus) focus.requestFocus()
|
||||||
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Add,
|
Icons.Filled.Add,
|
||||||
@@ -130,7 +150,7 @@ private fun AddItemRow(onAdd: (String) -> Unit) {
|
|||||||
PlainTextField(
|
PlainTextField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { text = it },
|
onValueChange = { text = it },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f).focusRequester(focus),
|
||||||
hint = R.string.editor_add_item,
|
hint = R.string.editor_add_item,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.List
|
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.Close
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -76,7 +79,9 @@ fun EditorTopBar(
|
|||||||
note: Note,
|
note: Note,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
tint: NoteTint,
|
tint: NoteTint,
|
||||||
|
showAddChecklist: Boolean,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
|
onStartChecklist: () -> Unit,
|
||||||
onPicker: (Picker) -> Unit,
|
onPicker: (Picker) -> Unit,
|
||||||
onConfirmDelete: () -> Unit,
|
onConfirmDelete: () -> Unit,
|
||||||
onAction: (EditorAction) -> Unit,
|
onAction: (EditorAction) -> Unit,
|
||||||
@@ -115,12 +120,13 @@ fun EditorTopBar(
|
|||||||
contentDescription = stringResource(R.string.editor_reminder),
|
contentDescription = stringResource(R.string.editor_reminder),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Adds the first checklist item, which is what makes the checklist
|
// REVEALS the checklist; it does not write one. This used to add an
|
||||||
// editor appear. Hidden once the note already has one — there is
|
// empty item so the section would have something to render, which
|
||||||
// nothing left to add that the checklist's own "+" row doesn't do
|
// left a blank row above the add-row — two empty fields, and the
|
||||||
// better.
|
// caret in the lower one. Hidden once the checklist is showing, since
|
||||||
if (note.items.isEmpty()) {
|
// its own "+" row does the rest better.
|
||||||
IconButton(onClick = { onAction(EditorAction.AddChecklist) }) {
|
if (showAddChecklist) {
|
||||||
|
IconButton(onClick = onStartChecklist) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.List,
|
Icons.AutoMirrored.Filled.List,
|
||||||
contentDescription = stringResource(R.string.editor_add_checklist),
|
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
|
* 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.
|
* 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
|
* A checkmark, at the operator's ask. I had shipped the word "Done" here on the
|
||||||
* reason. A tick in a notes app is a checklist item to anyone who has used one, and
|
* argument that a tick in a NOTES app reads as a checklist item; overruled, and the
|
||||||
* "Done" cannot be misread — including aloud.
|
* 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
|
* [DateUtils] rather than a hand-rolled formatter: it is localised, it already
|
||||||
* knows the difference between minutes, hours and yesterday, and getting plurals
|
* knows the difference between minutes, hours and yesterday, and getting plurals
|
||||||
@@ -191,9 +200,11 @@ fun EditorTopBar(
|
|||||||
fun EditorFooter(
|
fun EditorFooter(
|
||||||
updatedAt: String?,
|
updatedAt: String?,
|
||||||
saving: Boolean,
|
saving: Boolean,
|
||||||
|
tint: NoteTint,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val dark = isSystemInDarkTheme()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
@@ -204,9 +215,9 @@ fun EditorFooter(
|
|||||||
// inset down.
|
// inset down.
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
// Asymmetric: the button brings its own padding, the text does not.
|
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
.padding(start = 16.dp, end = 4.dp),
|
// The gap is what keeps the timestamp from reading as the button's label.
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -214,8 +225,17 @@ fun EditorFooter(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
TextButton(onClick = onClose) {
|
FilledTonalIconButton(
|
||||||
Text(stringResource(R.string.editor_done))
|
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 picker by remember(sessionKey) { mutableStateOf(Picker.NONE) }
|
||||||
var confirmingDelete by remember(sessionKey) { mutableStateOf(false) }
|
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
|
// 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
|
// 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.
|
// Restore and Delete forever as the only things to do with it.
|
||||||
@@ -177,7 +185,9 @@ fun NoteEditorScreen(
|
|||||||
note = note,
|
note = note,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
tint = tint,
|
tint = tint,
|
||||||
|
showAddChecklist = !checklistOpen && note.items.isEmpty(),
|
||||||
onClose = leave,
|
onClose = leave,
|
||||||
|
onStartChecklist = { checklistOpen = true },
|
||||||
onPicker = { picker = it },
|
onPicker = { picker = it },
|
||||||
onConfirmDelete = { confirmingDelete = true },
|
onConfirmDelete = { confirmingDelete = true },
|
||||||
onAction = onAction,
|
onAction = onAction,
|
||||||
@@ -191,6 +201,7 @@ fun NoteEditorScreen(
|
|||||||
EditorFooter(
|
EditorFooter(
|
||||||
updatedAt = note.updatedAt,
|
updatedAt = note.updatedAt,
|
||||||
saving = saving,
|
saving = saving,
|
||||||
|
tint = tint,
|
||||||
onClose = leave,
|
onClose = leave,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -231,11 +242,17 @@ fun NoteEditorScreen(
|
|||||||
modifier = Modifier.focusRequester(bodyFocus),
|
modifier = Modifier.focusRequester(bodyFocus),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Below the body, not instead of it, and only once the note has
|
// Below the body, not instead of it. Shown once the note has
|
||||||
// items — the toolbar's add-checklist action is what puts the
|
// items, or once the toolbar has been asked for a checklist — in
|
||||||
// first one there.
|
// which case the add row takes focus, because being asked for a
|
||||||
if (note.items.isNotEmpty()) {
|
// list means being about to type one.
|
||||||
ChecklistEditor(note = note, readOnly = readOnly, onAction = onAction)
|
if (note.items.isNotEmpty() || checklistOpen) {
|
||||||
|
ChecklistEditor(
|
||||||
|
note = note,
|
||||||
|
readOnly = readOnly,
|
||||||
|
focusAddRow = checklistOpen && note.items.isEmpty(),
|
||||||
|
onAction = onAction,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.labels.isNotEmpty()) {
|
if (note.labels.isNotEmpty()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user