1package com.peachpasswords.ui.codex23import androidx.activity.compose.rememberLauncherForActivityResult4import androidx.activity.result.contract.ActivityResultContracts5import androidx.compose.foundation.background6import androidx.compose.foundation.clickable7import androidx.compose.foundation.layout.Arrangement8import androidx.compose.foundation.layout.Box9import androidx.compose.foundation.layout.Column10import androidx.compose.foundation.layout.Row11import androidx.compose.foundation.layout.Spacer12import androidx.compose.foundation.layout.fillMaxSize13import androidx.compose.foundation.layout.fillMaxWidth14import androidx.compose.foundation.layout.height15import androidx.compose.foundation.layout.padding16import androidx.compose.foundation.layout.size17import androidx.compose.foundation.shape.CircleShape18import androidx.compose.foundation.shape.RoundedCornerShape19import androidx.compose.foundation.text.BasicTextField20import androidx.compose.material3.LinearProgressIndicator21import androidx.compose.material3.Text22import androidx.compose.runtime.Composable23import androidx.compose.runtime.getValue24import androidx.compose.runtime.mutableStateOf25import androidx.compose.runtime.remember26import androidx.compose.runtime.setValue27import androidx.compose.ui.Alignment28import androidx.compose.ui.Modifier29import androidx.compose.ui.draw.clip30import androidx.compose.ui.graphics.Color31import androidx.compose.ui.graphics.SolidColor32import androidx.compose.ui.text.font.FontStyle33import androidx.compose.ui.unit.dp34import androidx.compose.ui.unit.sp35import androidx.hilt.navigation.compose.hiltViewModel36import androidx.lifecycle.compose.collectAsStateWithLifecycle37import com.peachpasswords.codex.CodexStep38import com.peachpasswords.ui.components.SecureFlagEffect39import com.peachpasswords.ui.theme.LocalPeachAccent40import com.peachpasswords.ui.theme.peachColors41import com.peachpasswords.ui.theme.PeachType42import com.peachpasswords.ui.theme.ShellBackground4344@Composable45fun CodexGenerateScreen(46 onBack: () -> Unit,47 onSetupRecovery: () -> Unit = {},48 onUpgradePro: () -> Unit = {},49 vm: CodexViewModel = hiltViewModel(),50) {51 val state by vm.generate.collectAsStateWithLifecycle()52 val accent = LocalPeachAccent.current53 val saveLauncher = rememberLauncherForActivityResult(54 ActivityResultContracts.CreateDocument("application/pdf"),55 ) { uri -> uri?.let { vm.writePdf(it) } }5657 SecureFlagEffect() 5859 ShellBackground {60 Column(Modifier.fillMaxSize().padding(horizontal = 24.dp)) {61 CodexHeader("Codex backup", onBack)62 Spacer(Modifier.height(8.dp))63 Text(64 "A printable PDF of Aztec codes that encodes your encrypted vault. Keep it with your recovery words — together they restore everything, even offline.",65 color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,66 )67 Spacer(Modifier.height(28.dp))6869 when (val s = state) {70 is GenerateState.Idle -> {71 when (codexGenerateGate(vm.isPro, vm.recoveryWordsSet)) {72 CodexGenerateGate.PRO_REQUIRED -> {73 Text("Codex generation is a Pro feature", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)74 Spacer(Modifier.height(6.dp))75 Text(76 "Restoring from an existing Codex is always free. Creating a new printable Codex backup requires Peach Pro.",77 color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,78 )79 Spacer(Modifier.height(20.dp))80 CodexButton("Open Pro", accent, filled = true, onClick = onUpgradePro)81 }82 CodexGenerateGate.RECOVERY_REQUIRED -> {83 Text("Set up recovery words first", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)84 Spacer(Modifier.height(6.dp))85 Text(86 "A Codex backup only restores alongside your recovery words. Set them up before generating one.",87 color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,88 )89 Spacer(Modifier.height(20.dp))90 CodexButton("Set up recovery words", accent, filled = true, onClick = onSetupRecovery)91 }92 CodexGenerateGate.READY -> {93 var w3 by remember { mutableStateOf("") }94 var w7 by remember { mutableStateOf("") }95 var w11 by remember { mutableStateOf("") }96 val ok = vm.gateMatches(w3, w7, w11)97 Text("Confirm your recovery words", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)98 Spacer(Modifier.height(6.dp))99 Text("Enter the words at positions 3, 7, and 11.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)100 Spacer(Modifier.height(16.dp))101 GateField("Word #3", w3, accent) { w3 = it }102 GateField("Word #7", w7, accent) { w7 = it }103 GateField("Word #11", w11, accent) { w11 = it }104 Spacer(Modifier.height(20.dp))105 CodexButton(106 "Generate Codex PDF",107 if (ok) accent else peachColors.T2,108 filled = ok,109 ) { if (ok) vm.startGenerate(System.currentTimeMillis()) }110 }111 }112 }113114 is GenerateState.Running -> Column {115 Text(stepLabel(s.step), color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 11.sp)116 Spacer(Modifier.height(10.dp))117 val frac = if (s.total > 0) s.current.toFloat() / s.total else 0f118 LinearProgressIndicator(119 progress = { if (s.step == CodexStep.AZTEC) frac else 0.5f },120 modifier = Modifier.fillMaxWidth().height(4.dp),121 color = accent, trackColor = peachColors.S2,122 )123 if (s.step == CodexStep.AZTEC) {124 Spacer(Modifier.height(6.dp))125 Text("${s.current} / ${s.total} codes", color = peachColors.T3, fontFamily = PeachType.Mono, fontSize = 9.sp)126 }127 }128129 is GenerateState.Done -> Column {130 SummaryRow("verify code", s.manifest.verifyCode, accent)131 SummaryRow("entries", s.manifest.entryCount.toString(), accent)132 SummaryRow("codes", "${s.manifest.totalChunks} (${s.manifest.parityChunks} recovery)", accent)133 SummaryRow("pages", s.manifest.pageCount.toString(), accent)134 Spacer(Modifier.height(24.dp))135 CodexButton("Save PDF…", accent, filled = true) { saveLauncher.launch("peach-codex.pdf") }136 Spacer(Modifier.height(10.dp))137 CodexButton("Generate again", peachColors.T2, filled = false) { vm.startGenerate(System.currentTimeMillis()) }138 }139140 is GenerateState.Error -> Column {141 Text("Couldn't generate", color = peachColors.Breach, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)142 Spacer(Modifier.height(6.dp))143 Text(s.message, color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)144 Spacer(Modifier.height(20.dp))145 CodexButton("Try again", accent, filled = true) { vm.startGenerate(System.currentTimeMillis()) }146 }147 }148 }149 }150}151152internal enum class CodexGenerateGate { PRO_REQUIRED, RECOVERY_REQUIRED, READY }153154internal fun codexGenerateGate(isPro: Boolean, recoveryWordsSet: Boolean): CodexGenerateGate = when {155 !isPro -> CodexGenerateGate.PRO_REQUIRED156 !recoveryWordsSet -> CodexGenerateGate.RECOVERY_REQUIRED157 else -> CodexGenerateGate.READY158}159160private fun stepLabel(step: CodexStep): String = when (step) {161 CodexStep.COMPRESS -> "compressing vault…"162 CodexStep.ERASURE -> "adding recovery codes…"163 CodexStep.AZTEC -> "drawing Aztec codes…"164 CodexStep.VERIFY -> "verifying it rebuilds…"165 CodexStep.PDF -> "laying out the page…"166}167168@Composable169internal fun CodexHeader(title: String, onBack: () -> Unit) {170 Row(171 modifier = Modifier.fillMaxWidth().padding(top = 12.dp).height(48.dp),172 verticalAlignment = Alignment.CenterVertically,173 ) {174 Text("‹", color = peachColors.T1, fontSize = 26.sp,175 modifier = Modifier.clip(CircleShape).clickable(onClick = onBack).padding(horizontal = 8.dp))176 Spacer(Modifier.size(8.dp))177 Text(title, color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)178 }179}180181@Composable182private fun SummaryRow(label: String, value: String, accent: Color) {183 Row(Modifier.fillMaxWidth().padding(vertical = 7.dp), horizontalArrangement = Arrangement.SpaceBetween) {184 Text(label, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)185 Text(value, color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)186 }187}188189@Composable190private fun GateField(label: String, value: String, accent: Color, onChange: (String) -> Unit) {191 Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {192 Text(label, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)193 BasicTextField(194 value = value,195 onValueChange = { onChange(it) },196 singleLine = true,197 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),198 cursorBrush = SolidColor(accent),199 modifier = Modifier.fillMaxWidth().padding(top = 4.dp),200 )201 Box(Modifier.fillMaxWidth().padding(top = 4.dp).height(1.dp).background(peachColors.T3))202 }203}204205@Composable206internal fun CodexButton(label: String, color: Color, filled: Boolean, onClick: () -> Unit) {207 val accent = LocalPeachAccent.current208 Box(209 modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp))210 .background(if (filled) accent.copy(alpha = 0.14f) else Color.Transparent)211 .clickable(onClick = onClick).padding(vertical = 15.dp),212 contentAlignment = Alignment.Center,213 ) {214 Text(label, color = if (filled) accent else color, fontFamily = PeachType.Mono, fontSize = 13.sp)215 }216}