From 3d035676339658b27c2edf15cd7e26d1e4d06694 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 8 Oct 2026 14:51:17 -0400 Subject: [PATCH] DRY pass #2, batch 6, F19: one confirm dialog, one permission vocabulary, one #name (#5372) - Panel.kt ConfirmDialog (moved from TagsScreen, where it was private): the delete-forever dialog, the sync disconnect and the tag dialogs all ask through it now. - strings.xml: editor_cancel and tags_cancel were both "Cancel"; they are one cancel string. - NoteAccess carries the core's permission string (wire). The access lookup, the share sheet's choices and the board's draft read it from there instead of writing "owner"/"edit"/"view" again. - hashtag(name): the #-prefixed tag name that TagsScreen, the card and the editor chips each wrote. Formatted and checked with ktlint in the CI image. Co-Authored-By: Claude Opus 5.5 --- .../fabledsword/inkwell/ui/BoardViewModel.kt | 2 +- .../fabledsword/inkwell/ui/EditorChrome.kt | 2 +- .../fabledsword/inkwell/ui/EditorPickers.kt | 4 +- .../com/fabledsword/inkwell/ui/NoteAccess.kt | 16 +++-- .../com/fabledsword/inkwell/ui/NoteCard.kt | 2 +- .../java/com/fabledsword/inkwell/ui/Panel.kt | 45 ++++++++++-- .../com/fabledsword/inkwell/ui/ShareSheet.kt | 4 +- .../com/fabledsword/inkwell/ui/SyncScreen.kt | 23 +++---- .../com/fabledsword/inkwell/ui/TagsScreen.kt | 68 +++++-------------- android/app/src/main/res/values/strings.xml | 3 +- 10 files changed, 81 insertions(+), 88 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardViewModel.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardViewModel.kt index 9092e3a..8cf5817 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/BoardViewModel.kt @@ -718,7 +718,7 @@ private fun blankDraft(): Note = previews = emptyList(), createdAt = null, updatedAt = null, - permission = "owner", + permission = NoteAccess.OWNER.wire, shared = false, sharedBy = null, ) diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorChrome.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorChrome.kt index a521d3e..249a42d 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorChrome.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorChrome.kt @@ -345,7 +345,7 @@ fun EditorLabelRow( // `#` on every chip, matching the card. This row still shows the // tags the BODY owns as well — it is the control surface, and the // "from tag" hint beside one is what says why it has no cross. - text = "#${label.name}", + text = hashtag(label.name), style = MaterialTheme.typography.labelLarge, color = tint.tagInk(dark), modifier = diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorPickers.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorPickers.kt index 73baf1b..bb6a2aa 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorPickers.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/EditorPickers.kt @@ -286,7 +286,7 @@ private fun ExactReminderPicker( ) { Text(stringResource(R.string.picker_next)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.editor_cancel)) } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) { DatePicker(state = state) @@ -312,7 +312,7 @@ private fun ExactReminderPicker( }) { Text(stringResource(R.string.picker_set)) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.editor_cancel)) } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) } diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteAccess.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteAccess.kt index 2540a05..f75cf45 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteAccess.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteAccess.kt @@ -10,15 +10,17 @@ import com.fabledsword.inkwell.core.Note * refuses the rest anyway — this only keeps the editor from offering what would be * refused. */ -enum class NoteAccess { OWNER, EDIT, VIEW } +enum class NoteAccess( + /** The core's `permission` string, which is also the server's and the web's. */ + val wire: String, +) { + OWNER("owner"), + EDIT("edit"), + VIEW("view"), +} val Note.access: NoteAccess - get() = - when (permission) { - "edit" -> NoteAccess.EDIT - "view" -> NoteAccess.VIEW - else -> NoteAccess.OWNER - } + get() = NoteAccess.entries.firstOrNull { it.wire == permission } ?: NoteAccess.OWNER /** Its content can't change here: it is in the trash, or shared with us to view. */ val Note.readOnlyHere: Boolean diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteCard.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteCard.kt index 827d459..00c83d7 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteCard.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/NoteCard.kt @@ -402,7 +402,7 @@ private fun LabelChips(labels: List) { // this row is a tag — a tag lifted off its own line, or one attached // through the picker — and the hash is how you would type either. It // also keeps a lifted chip reading as the `#todo` somebody wrote. - text = "#${label.name}", + text = hashtag(label.name), style = MaterialTheme.typography.labelSmall, color = tint.tagInk(dark), maxLines = 1, diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/Panel.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/Panel.kt index cebd169..7d161a3 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/Panel.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/Panel.kt @@ -126,18 +126,51 @@ fun MenuItem( fun ConfirmDeleteDialog( onConfirm: () -> Unit, onDismiss: () -> Unit, +) { + ConfirmDialog( + title = stringResource(R.string.editor_delete_forever_title), + body = stringResource(R.string.editor_delete_forever_body), + confirm = stringResource(R.string.editor_delete_forever_confirm), + onDismiss = onDismiss, + onConfirm = onConfirm, + ) +} + +/** + * A confirmation: what is about to happen, what it costs, and one way out. + * + * Every dialog that asks before acting is this one, so they all word their buttons + * and lay out their consequences the same way. + */ +@Composable +fun ConfirmDialog( + title: String, + body: String, + confirm: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit, + footnote: String? = null, ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.editor_delete_forever_title)) }, - text = { Text(stringResource(R.string.editor_delete_forever_body)) }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(stringResource(R.string.editor_delete_forever_confirm)) + title = { Text(title) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = body, style = MaterialTheme.typography.bodyMedium) + footnote?.let { + Text( + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } }, + confirmButton = { + TextButton(onClick = onConfirm) { Text(confirm) } + }, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.editor_cancel)) } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) } diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/ShareSheet.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/ShareSheet.kt index 9078493..9097aab 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/ShareSheet.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/ShareSheet.kt @@ -58,8 +58,8 @@ fun SharedByLine(note: Note) { ) } -private const val VIEW = "view" -private const val EDIT = "edit" +private val VIEW = NoteAccess.VIEW.wire +private val EDIT = NoteAccess.EDIT.wire /** * The Share sheet: who the note is shared with, and adding someone or a group. diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/SyncScreen.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/SyncScreen.kt index d5a4a2a..185f4e6 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/SyncScreen.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/SyncScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -280,20 +279,14 @@ private fun LinkedPanel( // Confirmed because it is not obvious what disconnecting does to the notes. // The copy answers that first — they stay — since the fear it raises is // "will this delete something?", not "am I sure?". - AlertDialog( - onDismissRequest = { confirmingUnlink = false }, - title = { Text(stringResource(R.string.sync_disconnect_title)) }, - text = { Text(stringResource(R.string.sync_disconnect_body)) }, - confirmButton = { - TextButton(onClick = { - confirmingUnlink = false - onUnlink() - }) { Text(stringResource(R.string.sync_disconnect)) } - }, - dismissButton = { - TextButton(onClick = { confirmingUnlink = false }) { - Text(stringResource(R.string.editor_cancel)) - } + ConfirmDialog( + title = stringResource(R.string.sync_disconnect_title), + body = stringResource(R.string.sync_disconnect_body), + confirm = stringResource(R.string.sync_disconnect), + onDismiss = { confirmingUnlink = false }, + onConfirm = { + confirmingUnlink = false + onUnlink() }, ) } diff --git a/android/app/src/main/java/com/fabledsword/inkwell/ui/TagsScreen.kt b/android/app/src/main/java/com/fabledsword/inkwell/ui/TagsScreen.kt index 83e091d..a9d1e1b 100644 --- a/android/app/src/main/java/com/fabledsword/inkwell/ui/TagsScreen.kt +++ b/android/app/src/main/java/com/fabledsword/inkwell/ui/TagsScreen.kt @@ -181,8 +181,8 @@ fun TagsScreen( is TagDialog.ConfirmMerge -> ConfirmDialog( - title = stringResource(R.string.tags_rename_merges_title, hash(open.into.name)), - body = stringResource(R.string.tags_rename_merges_body, hash(open.into.name)), + title = stringResource(R.string.tags_rename_merges_title, hashtag(open.into.name)), + body = stringResource(R.string.tags_rename_merges_body, hashtag(open.into.name)), confirm = stringResource(R.string.tags_rename_merges_confirm), onDismiss = { dialog = null }, onConfirm = { @@ -204,7 +204,7 @@ fun TagsScreen( is TagDialog.Delete -> ConfirmDialog( - title = stringResource(R.string.tags_delete_title, hash(open.tag.name)), + title = stringResource(R.string.tags_delete_title, hashtag(open.tag.name)), // The count is the part that makes the consequence real — "it is on // 40 notes" is a different decision from "delete this tag?". It comes // from the LIST, the only call the core populates a count on. @@ -235,13 +235,13 @@ fun TagsScreen( } /** - * `#` on the name, everywhere it is spoken about. + * `#` on the name, everywhere it is spoken about: the chips on a card and in the + * editor (`NoteCard.kt`, `EditorChrome.kt`) and every dialog here. * - * The chips already wear it (`NoteCard.kt`, `EditorChrome.kt`) and it is the - * reason these are called tags at all — a dialog that said "Delete grocery?" would - * be talking about something else. + * It is the reason these are called tags at all — a dialog that said "Delete + * grocery?" would be talking about something else. */ -private fun hash(name: String): String = "#$name" +internal fun hashtag(name: String): String = "#$name" @Composable private fun NewTagField( @@ -333,7 +333,7 @@ private fun TagRow( Column(modifier = Modifier.weight(1f)) { Text( - text = hash(tag.name), + text = hashtag(tag.name), style = MaterialTheme.typography.bodyLarge, color = tint.tagInk(dark), ) @@ -410,7 +410,7 @@ private fun RenameDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.tags_rename_title, hash(tag.name))) }, + title = { Text(stringResource(R.string.tags_rename_title, hashtag(tag.name))) }, text = { PlainTextField( value = text, @@ -427,7 +427,7 @@ private fun RenameDialog( } }, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.tags_cancel)) } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) } @@ -452,11 +452,11 @@ private fun MergeDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.tags_merge_title, hash(tag.name))) }, + title = { Text(stringResource(R.string.tags_merge_title, hashtag(tag.name))) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - text = stringResource(R.string.tags_merge_body, hash(tag.name)), + text = stringResource(R.string.tags_merge_body, hashtag(tag.name)), style = MaterialTheme.typography.bodyMedium, ) if (others.isEmpty()) { @@ -469,7 +469,7 @@ private fun MergeDialog( others.forEach { other -> val tint = labelTintFor(other.name, other.color) Text( - text = hash(other.name), + text = hashtag(other.name), style = MaterialTheme.typography.bodyLarge, color = tint.tagInk(dark), modifier = @@ -483,7 +483,7 @@ private fun MergeDialog( }, confirmButton = {}, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.tags_cancel)) } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) } @@ -507,7 +507,7 @@ private fun ColourDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.tags_colour_of, hash(tag.name))) }, + title = { Text(stringResource(R.string.tags_colour_of, hashtag(tag.name))) }, text = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { NOTE_TINTS.forEach { (key, tint) -> @@ -544,41 +544,7 @@ private fun ColourDialog( }, confirmButton = {}, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.tags_cancel)) } - }, - ) -} - -/** A destructive confirmation: what it is, what it costs, and one way out. */ -@Composable -private fun ConfirmDialog( - title: String, - body: String, - confirm: String, - onDismiss: () -> Unit, - onConfirm: () -> Unit, - footnote: String? = null, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text(text = body, style = MaterialTheme.typography.bodyMedium) - footnote?.let { - Text( - text = it, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { Text(confirm) } - }, - dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.tags_cancel)) } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) } diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 72f2529..9edba18 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -65,7 +65,7 @@ Unarchive Move to trash Restore - Cancel + Cancel Share… Tags written as #tag in a note come back when that note is next edited. - Cancel Tags