diff --git a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunStore.kt b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunStore.kt index cf0a733..d2d26d3 100644 --- a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunStore.kt +++ b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunStore.kt @@ -128,20 +128,24 @@ class RunStore(context: Context, private val settings: Settings) { */ private fun describeFacts(p: app.echo_lot.protocol.Profile): String { val lines = ArrayList() - lines += "${p.name} · server ${p.serverVersion}" + // "label|value" per line, laid out as real columns by the UI rather than padded with + // spaces here. Space padding only lines up in a monospaced font, which makes the layout + // depend on a typeface choice made somewhere else entirely. + fun row(label: String, value: String) = lines.add("$label|$value") + + row("server", "${p.name} · ${p.serverVersion}") for (t in p.targets) { - t.ip4?.let { lines += "IPv4 $it" } - t.ip4Alt?.let { lines += "IPv4 $it (alternate, for NAT behaviour tests)" } - t.ip6?.let { lines += "IPv6 $it" } - t.ip6Alt?.let { lines += "IPv6 $it (alternate, for NAT behaviour tests)" } - lines += "ports udp ${t.udpPort} · tcp ${t.tcpPort} · stun ${t.stunPort}" + t.ip4?.let { row("IPv4", it) } + t.ip6?.let { row("IPv6", it) } + // Marked rather than listed apart: it is the same server, and what matters is being + // able to tell which address a NAT-behaviour result came from. + t.ip4Alt?.let { row("IPv4 alt", it) } + t.ip6Alt?.let { row("IPv6 alt", it) } + row("ports", "udp ${t.udpPort} · tcp ${t.tcpPort} · stun ${t.stunPort}") } - if (p.canaryZone.isNotBlank()) lines += "DNS canary zone ${p.canaryZone}" - if (p.capabilities.isNotEmpty()) { - lines += "can measure ${p.capabilities.joinToString(", ")}" - } - val b = StringBuilder(lines.joinToString("\n")) - return b.toString().trimEnd() + if (p.canaryZone.isNotBlank()) row("dns zone", p.canaryZone) + if (p.capabilities.isNotEmpty()) row("measures", p.capabilities.joinToString(", ")) + return lines.joinToString(System.lineSeparator()) } fun enroll(link: String, deviceName: String?): String { diff --git a/echolot-app/app/src/main/kotlin/app/echo_lot/app/SettingsScreen.kt b/echolot-app/app/src/main/kotlin/app/echo_lot/app/SettingsScreen.kt index 58485c8..57b3091 100644 --- a/echolot-app/app/src/main/kotlin/app/echo_lot/app/SettingsScreen.kt +++ b/echolot-app/app/src/main/kotlin/app/echo_lot/app/SettingsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.rememberScrollState @@ -33,6 +34,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import app.echo_lot.privacy.PrivacyLevel /** @@ -255,34 +257,15 @@ fun SettingsScreen( }, label = { Text("Server URL") }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) - // What the server reports about itself. Read-only on purpose: these are facts to - // check, not settings to apply, and an editable box that changes nothing is worse - // than no box at all. - if (serverFacts.isNotBlank()) { - Surface( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(6.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text( - "What this server reports", - style = MaterialTheme.typography.labelLarge, - ) - Text( - serverFacts, - style = MaterialTheme.typography.bodySmall, - fontFamily = FontFamily.Monospace, - ) - } - } - } + // Directly under the field it explains. Anywhere else it reads as a stray sentence + // about some other part of the screen. if (settings.serverUrl.isNotBlank() && settings.serverUrl != settings.serverPublicUrl) { Text( "Connects to ${settings.serverUrl} — this server publishes one name and " + "points devices at another, so its pinned certificate can share a port " + "with its web interface.", style = MaterialTheme.typography.bodySmall, + color = LocalContentColor.current.copy(alpha = 0.7f), ) } OutlinedTextField( @@ -311,6 +294,52 @@ fun SettingsScreen( serverStatus?.let { Text(it, style = MaterialTheme.typography.bodySmall) } + // What the server reported, placed under the button that asks it rather than among + // the fields above: these are facts to read, not settings to apply, and an + // editable-looking box that changes nothing is worse than no box at all. + // + // Monospaced so the addresses line up under each other — column alignment is most + // of what makes a list of IPs quicker to read than prose. + if (serverFacts.isNotBlank()) { + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + "WHAT THIS SERVER REPORTS", + style = MaterialTheme.typography.labelSmall, + color = LocalContentColor.current.copy(alpha = 0.7f), + ) + // Real columns rather than padded text: the label column has a fixed + // width, so values line up whatever the font does, and a long value + // wraps inside its own column instead of under the labels. + for (line in serverFacts.lines()) { + val label = line.substringBefore('|') + val value = line.substringAfter('|', "") + Row(Modifier.fillMaxWidth()) { + Text( + label, + style = MaterialTheme.typography.bodySmall, + color = LocalContentColor.current.copy(alpha = 0.7f), + modifier = Modifier.width(72.dp), + ) + Text( + value, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + ), + modifier = Modifier.weight(1f), + ) + } + } + } + } + } Text( "This app is ${BuildConfig.APP_SEMVER} and speaks probe protocol " + "${app.echo_lot.protocol.Compat.PROTOCOL_VERSION}. It works with servers " +