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 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:51:17 -04:00
co-authored by Claude Opus 5.5
parent ef759e7d8b
commit 3d03567633
10 changed files with 81 additions and 88 deletions
@@ -718,7 +718,7 @@ private fun blankDraft(): Note =
previews = emptyList(), previews = emptyList(),
createdAt = null, createdAt = null,
updatedAt = null, updatedAt = null,
permission = "owner", permission = NoteAccess.OWNER.wire,
shared = false, shared = false,
sharedBy = null, sharedBy = null,
) )
@@ -345,7 +345,7 @@ fun EditorLabelRow(
// `#` on every chip, matching the card. This row still shows the // `#` on every chip, matching the card. This row still shows the
// tags the BODY owns as well — it is the control surface, and 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. // "from tag" hint beside one is what says why it has no cross.
text = "#${label.name}", text = hashtag(label.name),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = tint.tagInk(dark), color = tint.tagInk(dark),
modifier = modifier =
@@ -286,7 +286,7 @@ private fun ExactReminderPicker(
) { Text(stringResource(R.string.picker_next)) } ) { Text(stringResource(R.string.picker_next)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.editor_cancel)) } TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
}, },
) { ) {
DatePicker(state = state) DatePicker(state = state)
@@ -312,7 +312,7 @@ private fun ExactReminderPicker(
}) { Text(stringResource(R.string.picker_set)) } }) { Text(stringResource(R.string.picker_set)) }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.editor_cancel)) } TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
}, },
) )
} }
@@ -10,15 +10,17 @@ import com.fabledsword.inkwell.core.Note
* refuses the rest anyway — this only keeps the editor from offering what would be * refuses the rest anyway — this only keeps the editor from offering what would be
* refused. * 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 val Note.access: NoteAccess
get() = get() = NoteAccess.entries.firstOrNull { it.wire == permission } ?: NoteAccess.OWNER
when (permission) {
"edit" -> NoteAccess.EDIT
"view" -> NoteAccess.VIEW
else -> NoteAccess.OWNER
}
/** Its content can't change here: it is in the trash, or shared with us to view. */ /** Its content can't change here: it is in the trash, or shared with us to view. */
val Note.readOnlyHere: Boolean val Note.readOnlyHere: Boolean
@@ -402,7 +402,7 @@ private fun LabelChips(labels: List<NoteLabel>) {
// this row is a tag — a tag lifted off its own line, or one attached // 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 // through the picker — and the hash is how you would type either. It
// also keeps a lifted chip reading as the `#todo` somebody wrote. // also keeps a lifted chip reading as the `#todo` somebody wrote.
text = "#${label.name}", text = hashtag(label.name),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = tint.tagInk(dark), color = tint.tagInk(dark),
maxLines = 1, maxLines = 1,
@@ -126,18 +126,51 @@ fun MenuItem(
fun ConfirmDeleteDialog( fun ConfirmDeleteDialog(
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> 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( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.editor_delete_forever_title)) }, title = { Text(title) },
text = { Text(stringResource(R.string.editor_delete_forever_body)) }, text = {
confirmButton = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = onConfirm) { Text(text = body, style = MaterialTheme.typography.bodyMedium)
Text(stringResource(R.string.editor_delete_forever_confirm)) footnote?.let {
Text(
text = it,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
}, },
confirmButton = {
TextButton(onClick = onConfirm) { Text(confirm) }
},
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.editor_cancel)) } TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) }
}, },
) )
} }
@@ -58,8 +58,8 @@ fun SharedByLine(note: Note) {
) )
} }
private const val VIEW = "view" private val VIEW = NoteAccess.VIEW.wire
private const val EDIT = "edit" private val EDIT = NoteAccess.EDIT.wire
/** /**
* The Share sheet: who the note is shared with, and adding someone or a group. * The Share sheet: who the note is shared with, and adding someone or a group.
@@ -11,7 +11,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
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.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -280,20 +279,14 @@ private fun LinkedPanel(
// Confirmed because it is not obvious what disconnecting does to the notes. // 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 // The copy answers that first — they stay — since the fear it raises is
// "will this delete something?", not "am I sure?". // "will this delete something?", not "am I sure?".
AlertDialog( ConfirmDialog(
onDismissRequest = { confirmingUnlink = false }, title = stringResource(R.string.sync_disconnect_title),
title = { Text(stringResource(R.string.sync_disconnect_title)) }, body = stringResource(R.string.sync_disconnect_body),
text = { Text(stringResource(R.string.sync_disconnect_body)) }, confirm = stringResource(R.string.sync_disconnect),
confirmButton = { onDismiss = { confirmingUnlink = false },
TextButton(onClick = { onConfirm = {
confirmingUnlink = false confirmingUnlink = false
onUnlink() onUnlink()
}) { Text(stringResource(R.string.sync_disconnect)) }
},
dismissButton = {
TextButton(onClick = { confirmingUnlink = false }) {
Text(stringResource(R.string.editor_cancel))
}
}, },
) )
} }
@@ -181,8 +181,8 @@ fun TagsScreen(
is TagDialog.ConfirmMerge -> is TagDialog.ConfirmMerge ->
ConfirmDialog( ConfirmDialog(
title = stringResource(R.string.tags_rename_merges_title, hash(open.into.name)), title = stringResource(R.string.tags_rename_merges_title, hashtag(open.into.name)),
body = stringResource(R.string.tags_rename_merges_body, hash(open.into.name)), body = stringResource(R.string.tags_rename_merges_body, hashtag(open.into.name)),
confirm = stringResource(R.string.tags_rename_merges_confirm), confirm = stringResource(R.string.tags_rename_merges_confirm),
onDismiss = { dialog = null }, onDismiss = { dialog = null },
onConfirm = { onConfirm = {
@@ -204,7 +204,7 @@ fun TagsScreen(
is TagDialog.Delete -> is TagDialog.Delete ->
ConfirmDialog( 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 // 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 // 40 notes" is a different decision from "delete this tag?". It comes
// from the LIST, the only call the core populates a count on. // 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 * It is the reason these are called tags at all — a dialog that said "Delete
* reason these are called tags at all — a dialog that said "Delete grocery?" would * grocery?" would be talking about something else.
* be talking about something else.
*/ */
private fun hash(name: String): String = "#$name" internal fun hashtag(name: String): String = "#$name"
@Composable @Composable
private fun NewTagField( private fun NewTagField(
@@ -333,7 +333,7 @@ private fun TagRow(
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = hash(tag.name), text = hashtag(tag.name),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = tint.tagInk(dark), color = tint.tagInk(dark),
) )
@@ -410,7 +410,7 @@ private fun RenameDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, 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 = { text = {
PlainTextField( PlainTextField(
value = text, value = text,
@@ -427,7 +427,7 @@ private fun RenameDialog(
} }
}, },
dismissButton = { 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( AlertDialog(
onDismissRequest = onDismiss, 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 = { text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( 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, style = MaterialTheme.typography.bodyMedium,
) )
if (others.isEmpty()) { if (others.isEmpty()) {
@@ -469,7 +469,7 @@ private fun MergeDialog(
others.forEach { other -> others.forEach { other ->
val tint = labelTintFor(other.name, other.color) val tint = labelTintFor(other.name, other.color)
Text( Text(
text = hash(other.name), text = hashtag(other.name),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = tint.tagInk(dark), color = tint.tagInk(dark),
modifier = modifier =
@@ -483,7 +483,7 @@ private fun MergeDialog(
}, },
confirmButton = {}, confirmButton = {},
dismissButton = { 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( AlertDialog(
onDismissRequest = onDismiss, 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 = { text = {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
NOTE_TINTS.forEach { (key, tint) -> NOTE_TINTS.forEach { (key, tint) ->
@@ -544,41 +544,7 @@ private fun ColourDialog(
}, },
confirmButton = {}, confirmButton = {},
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.tags_cancel)) } TextButton(onClick = onDismiss) { Text(stringResource(R.string.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)) }
}, },
) )
} }
+1 -2
View File
@@ -65,7 +65,7 @@
<string name="editor_unarchive">Unarchive</string> <string name="editor_unarchive">Unarchive</string>
<string name="editor_trash">Move to trash</string> <string name="editor_trash">Move to trash</string>
<string name="editor_restore">Restore</string> <string name="editor_restore">Restore</string>
<string name="editor_cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="editor_share">Share…</string> <string name="editor_share">Share…</string>
<!-- Sharing (#5175). Shares live on the server, so the sheet says so when this <!-- Sharing (#5175). Shares live on the server, so the sheet says so when this
@@ -159,7 +159,6 @@
said here rather than left as a surprise. --> said here rather than left as a surprise. -->
<string name="tags_delete_from_text">Tags written as #tag in a note come back when that note is next edited.</string> <string name="tags_delete_from_text">Tags written as #tag in a note come back when that note is next edited.</string>
<string name="tags_cancel">Cancel</string>
<!-- Pickers --> <!-- Pickers -->
<string name="label_picker_title">Tags</string> <string name="label_picker_title">Tags</string>