diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt index 2d45f3d..eaa4ac5 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/BoardViewModel.kt @@ -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) } diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt index 7bdcd1d..a5589c3 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorAction.kt @@ -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 diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChecklist.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChecklist.kt index e51adfa..a0c9c05 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChecklist.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChecklist.kt @@ -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), diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt index ec7b8b5..3ab0513 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/EditorChrome.kt @@ -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)) } } } diff --git a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt index 1bb2f21..b8d4649 100644 --- a/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt +++ b/android/app/src/main/java/com/fabledsword/thoughtsync/ui/NoteEditorScreen.kt @@ -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()) {