android: the two endings get their borders back
Build Debug APK / build (push) Successful in 5m12s

Both Retire and Reset are now FlatButton's footprint in danger colours — a
full-width bordered rectangle, 56dp tall, tinted with the error colour — as
the old Reset button was. A bare TextButton next to a paragraph of body text
had no edge to read as a control at all, which matters more here than
elsewhere: these two are the destructive pair.

Stacked rather than in a row, unlike the desktop's, because "Retire this
node…" beside "Reset this node…" is ~170dp each on a phone and both wrap.
Same width-driven split the label/value lists already make.

The spinner keeps the Reset button's place while a wipe runs and both
buttons fade, so nothing moves.
This commit is contained in:
Greg Shuflin
2026-08-28 04:18:13 -07:00
parent 0717e62f86
commit ab4edae5a2
@@ -2,6 +2,7 @@ package com.gregshuflin.synchronicity.ensemble
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -18,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
@@ -488,36 +490,26 @@ private fun ThisNodeSection(nativeLib: NativeLib, onRetire: () -> Unit, onLeaveN
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 8.dp),
)
// The two read as one pair of alternatives, so they are the same control in the same
// weight — the desktop draws them the same way, for the same reason. Retire is first:
// it is the one that leaves the ensemble in a coherent state, and the sentence above
// The two read as one pair of alternatives, so they are the same control at the same
// size — the desktop draws them the same way, for the same reason. Retire is first: it
// is the one that leaves the ensemble in a coherent state, and the sentence above
// introduces them in that order.
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
//
// Stacked rather than side by side, unlike the desktop's row, for the same reason the
// label/value lists differ: "Retire this node…" beside "Reset this node…" is ~170dp
// each on a phone and both labels wrap. It is a width decision, not a taste one.
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
EndingButton(
label = copy.retireAction,
enabled = !resetting,
onClick = { confirming = Ending.RETIRE },
)
if (resetting) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp),
)
Text(
text = copy.retireWiping,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.error,
)
} else {
EndingButton(
label = copy.resetAction,
enabled = true,
onClick = { confirming = Ending.RESET },
)
}
EndingButton(
label = if (resetting) copy.retireWiping else copy.resetAction,
enabled = !resetting,
busy = resetting,
onClick = { confirming = Ending.RESET },
)
}
resetError?.let { error ->
Text(
@@ -609,18 +601,56 @@ private enum class Ending {
RESET,
}
/** Retire and Reset, in the one weight they share. */
/**
* Retire and Reset, in the one shape they share.
*
* [com.gregshuflin.synchronicity.ui.FlatButton]'s footprint — a full-width bordered rectangle, 56dp
* tall — in the error colour ADR 0032 §4 reserves for destructive acts. Not FlatButton itself,
* which is neutral by design. It stays local until something else wants a danger button, at which
* point it belongs beside FlatButton.
*/
@Composable
private fun EndingButton(label: String, enabled: Boolean, onClick: () -> Unit) {
TextButton(
onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
private fun EndingButton(
label: String,
enabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
busy: Boolean = false,
) {
val error = MaterialTheme.colorScheme.error
val tint = if (enabled) error else error.copy(alpha = DISABLED_ALPHA)
Surface(
onClick = { if (enabled) onClick() },
modifier =
modifier.fillMaxWidth().height(ENDING_BUTTON_HEIGHT).border(1.dp, tint, RectangleShape),
shape = RectangleShape,
color = error.copy(alpha = ENDING_BUTTON_FILL_ALPHA),
contentColor = tint,
) {
Text(label)
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
if (busy) {
CircularProgressIndicator(color = tint, modifier = Modifier.size(SPINNER_SIZE))
}
Text(text = label, style = MaterialTheme.typography.bodyLarge)
}
}
}
/** Matches [com.gregshuflin.synchronicity.ui.FlatButton], which these are a danger variant of. */
private val ENDING_BUTTON_HEIGHT = 56.dp
/** Enough tint to read as filled without competing with the border. */
private const val ENDING_BUTTON_FILL_ALPHA = 0.1f
/** How far a disabled [EndingButton]'s border and label are faded. */
private const val DISABLED_ALPHA = 0.5f
private val SPINNER_SIZE = 20.dp
/** Said at the top of the roster when this node's own credential has been revoked. */
@Composable
private fun SelfRevokedNotice() {