Phone card: numbered steps and a two-minute countdown that renews the code (from release 0.108.49)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-06 10:15:42 +00:00
co-authored by Claude Opus 5.5
parent c8c10ab55a
commit 014a4739ec
@@ -179,28 +179,62 @@ private fun OrbitField(value: String, onChange: (String) -> Unit, hint: String,
}
}
/** On your phone: a QR code and a code to type, both one-time, while this screen shows. */
/**
* On your phone: a QR code and a code to type, both one-time, while this screen shows. Laid out as numbered steps (Paul,
* 6 November 2026: entering the code and then the PIN was confusing), with a two-minute countdown; when it runs out the
* code and QR code are replaced by new ones on their own, until a phone has joined.
*/
@Composable
private fun PhoneCard(modifier: Modifier) {
val pairing = remember { PhoneLink.beginPairing() }
val url = remember { PhoneLink.pairingUrl(pairing) }
var pairing by remember { mutableStateOf(PhoneLink.beginPairing()) }
val url = remember(pairing) { PhoneLink.pairingUrl(pairing) }
val qr = remember(url) { url?.let { qrBitmap(it, 360) } }
Column(modifier.background(Panel, RoundedCornerShape(12.dp)).padding(14.dp), verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.CenterHorizontally) {
val joined = PhoneLink.active
var left by remember { mutableLongStateOf(pairing.untilMs - System.currentTimeMillis()) }
LaunchedEffect(pairing, joined) {
while (joined == null) {
left = pairing.untilMs - System.currentTimeMillis()
if (left <= 0) { pairing = PhoneLink.beginPairing(); return@LaunchedEffect }
kotlinx.coroutines.delay(1_000)
}
}
Column(modifier.background(Panel, RoundedCornerShape(12.dp)).padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("On your phone", color = Ink, fontFamily = Anton, fontSize = 22.sp)
if (url == null || qr == null) { Text("This TV isn't on a home Wi-Fi network, so a phone can't reach it.", color = Muted, fontSize = 12.sp); return@Column }
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
androidx.compose.foundation.Image(qr, "QR code for the phone page", Modifier.size(130.dp).background(Color.White, RoundedCornerShape(6.dp)).padding(6.dp))
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("Scan with your phone's camera", color = Muted, fontSize = 12.sp)
Text("or go to", color = Muted, fontSize = 12.sp)
Text(url.substringBefore("/join"), color = Ink, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Text("and enter", color = Muted, fontSize = 12.sp)
Text(pairing.code, Modifier.background(Navy, RoundedCornerShape(6.dp)).padding(horizontal = 10.dp, vertical = 2.dp), color = Yellow, fontFamily = Anton, fontSize = 24.sp)
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.Top) {
androidx.compose.foundation.Image(qr, "QR code for the phone page", Modifier.size(124.dp).background(Color.White, RoundedCornerShape(6.dp)).padding(6.dp))
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
PhoneStep(1, "Scan the code with your phone's camera") {
Text("or go to ${url.substringBefore("/join")} and type", color = Muted, fontSize = 11.sp)
Text(pairing.code, Modifier.padding(top = 2.dp).background(Navy, RoundedCornerShape(6.dp)).padding(horizontal = 10.dp, vertical = 1.dp), color = Yellow, fontFamily = Anton, fontSize = 22.sp)
}
// The phone asks for the After Dark PIN next, also four digits: a step of its own, so it isn't mistaken for the code.
if (AfterDarkLock.pinSet) PhoneStep(2, "Enter your Adult PIN on the phone") { Text("The one for After Dark, not the code above", color = Muted, fontSize = 11.sp) }
}
}
Text(PhoneLink.active?.let { "Connected: $it" } ?: "Same Wi-Fi as this TV. The code works once, for two minutes.", color = if (PhoneLink.active != null) Mint else Muted, fontSize = 11.sp)
// The phone asks for the After Dark PIN next, which is also four digits: say so here, so it isn't mistaken for this code.
if (PhoneLink.active == null && AfterDarkLock.pinSet) Text("Then your phone asks for your Adult PIN, the one for After Dark.", color = Muted, fontSize = 11.sp)
if (joined != null) Text("Connected: $joined", color = Mint, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
else {
val fraction = (left.coerceIn(0, 120_000) / 120_000f)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Box(Modifier.weight(1f).height(4.dp).background(Navy, RoundedCornerShape(2.dp))) {
Box(Modifier.fillMaxWidth(fraction).fillMaxHeight().background(Yellow, RoundedCornerShape(2.dp)))
}
val seconds = ((left + 999) / 1000).coerceAtLeast(0)
Text("New code in %d:%02d".format(seconds / 60, seconds % 60), color = Muted, fontSize = 11.sp)
}
Text("Same Wi-Fi as this TV. Each code works once.", color = Muted, fontSize = 11.sp)
}
}
}
/** A numbered step on the phone card: its number in a circle, what to do, and any detail under it. */
@Composable
private fun PhoneStep(number: Int, title: String, detail: @Composable ColumnScope.() -> Unit) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Box(Modifier.size(20.dp).background(Yellow, androidx.compose.foundation.shape.CircleShape), contentAlignment = Alignment.Center) {
Text("$number", color = Navy, fontSize = 12.sp, fontWeight = FontWeight.Bold)
}
Column { Text(title, color = Ink, fontSize = 13.sp, fontWeight = FontWeight.SemiBold); detail() }
}
}