diff --git a/android/app/src/main/java/tv/orbit/classic/CreateChannel.kt b/android/app/src/main/java/tv/orbit/classic/CreateChannel.kt index 07bc4d9..4306ca0 100644 --- a/android/app/src/main/java/tv/orbit/classic/CreateChannel.kt +++ b/android/app/src/main/java/tv/orbit/classic/CreateChannel.kt @@ -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() } } }