CodexGenerateScreen.kt

11.7 KB · raw

1package com.peachpasswords.ui.codex23import android.content.Intent4import androidx.activity.compose.rememberLauncherForActivityResult5import androidx.activity.result.contract.ActivityResultContracts6import androidx.compose.foundation.background7import androidx.compose.foundation.clickable8import androidx.compose.foundation.layout.Arrangement9import androidx.compose.foundation.layout.Box10import androidx.compose.foundation.layout.Column11import androidx.compose.foundation.layout.Row12import androidx.compose.foundation.layout.Spacer13import androidx.compose.foundation.layout.fillMaxSize14import androidx.compose.foundation.layout.fillMaxWidth15import androidx.compose.foundation.layout.height16import androidx.compose.foundation.layout.padding17import androidx.compose.foundation.layout.size18import androidx.compose.foundation.shape.CircleShape19import androidx.compose.foundation.shape.RoundedCornerShape20import androidx.compose.foundation.text.BasicTextField21import androidx.compose.material3.LinearProgressIndicator22import androidx.compose.material3.Text23import androidx.compose.runtime.Composable24import androidx.compose.runtime.getValue25import androidx.compose.runtime.mutableStateOf26import androidx.compose.runtime.remember27import androidx.compose.runtime.setValue28import androidx.compose.ui.Alignment29import androidx.compose.ui.Modifier30import androidx.compose.ui.draw.clip31import androidx.compose.ui.graphics.Color32import androidx.compose.ui.graphics.SolidColor33import androidx.compose.ui.platform.LocalContext34import androidx.compose.ui.text.font.FontStyle35import androidx.compose.ui.unit.dp36import androidx.compose.ui.unit.sp37import androidx.hilt.navigation.compose.hiltViewModel38import androidx.lifecycle.compose.collectAsStateWithLifecycle39import com.peachpasswords.codex.CodexStep40import com.peachpasswords.ui.components.SecureFlagEffect41import com.peachpasswords.ui.theme.LocalPeachAccent42import com.peachpasswords.ui.theme.peachColors43import com.peachpasswords.ui.theme.PeachType44import com.peachpasswords.ui.theme.ShellBackground45import java.time.LocalDate4647@Composable48fun CodexGenerateScreen(49    onBack: () -> Unit,50    onSetupRecovery: () -> Unit = {},51    onUpgradePro: () -> Unit = {},52    vm: CodexViewModel = hiltViewModel(),53) {54    val state by vm.generate.collectAsStateWithLifecycle()55    val accent = LocalPeachAccent.current56    val context = LocalContext.current57    val saveLauncher = rememberLauncherForActivityResult(58        ActivityResultContracts.CreateDocument("application/pdf"),59    ) { uri -> uri?.let { vm.writePdf(it) } }6061    SecureFlagEffect() // MN-3: never screenshot the codex generation / verify code6263    ShellBackground {64        Column(Modifier.fillMaxSize().padding(horizontal = 24.dp)) {65            CodexHeader("Codex backup", onBack)66            Spacer(Modifier.height(8.dp))67            Text(68                "A printable PDF of Aztec codes that encodes your encrypted vault. Keep it with your recovery words — together they restore everything, even offline.",69                color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,70            )71            Spacer(Modifier.height(28.dp))7273            when (val s = state) {74                is GenerateState.Idle -> {75                    when (codexGenerateGate(vm.isPro, vm.recoveryWordsSet)) {76                        CodexGenerateGate.PRO_REQUIRED -> {77                            Text("Codex generation is a Pro feature", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)78                            Spacer(Modifier.height(6.dp))79                            Text(80                                "Restoring from an existing Codex is always free. Creating a new printable Codex backup requires Peach Pro.",81                                color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,82                            )83                            Spacer(Modifier.height(20.dp))84                            CodexButton("Open Pro", accent, filled = true, onClick = onUpgradePro)85                        }86                        CodexGenerateGate.RECOVERY_REQUIRED -> {87                        Text("Set up recovery words first", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)88                        Spacer(Modifier.height(6.dp))89                        Text(90                            "A Codex backup only restores alongside your recovery words. Set them up before generating one.",91                            color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,92                        )93                        Spacer(Modifier.height(20.dp))94                        CodexButton("Set up recovery words", accent, filled = true, onClick = onSetupRecovery)95                        }96                        CodexGenerateGate.READY -> {97                        var w3 by remember { mutableStateOf("") }98                        var w7 by remember { mutableStateOf("") }99                        var w11 by remember { mutableStateOf("") }100                        val ok = vm.gateMatches(w3, w7, w11)101                        Text("Confirm your recovery words", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)102                        Spacer(Modifier.height(6.dp))103                        Text("Enter the words at positions 3, 7, and 11.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)104                        Spacer(Modifier.height(16.dp))105                        GateField("Word #3", w3, accent) { w3 = it }106                        GateField("Word #7", w7, accent) { w7 = it }107                        GateField("Word #11", w11, accent) { w11 = it }108                        Spacer(Modifier.height(20.dp))109                        CodexButton(110                            "Generate Codex PDF",111                            if (ok) accent else peachColors.T2,112                            filled = ok,113                        ) { if (ok) vm.startGenerate(System.currentTimeMillis()) }114                        }115                    }116                }117118                is GenerateState.Running -> Column {119                    Text(stepLabel(s.step), color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 11.sp)120                    Spacer(Modifier.height(10.dp))121                    val frac = if (s.total > 0) s.current.toFloat() / s.total else 0f122                    LinearProgressIndicator(123                        progress = { if (s.step == CodexStep.AZTEC) frac else 0.5f },124                        modifier = Modifier.fillMaxWidth().height(4.dp),125                        color = accent, trackColor = peachColors.S2,126                    )127                    if (s.step == CodexStep.AZTEC) {128                        Spacer(Modifier.height(6.dp))129                        Text("${s.current} / ${s.total} codes", color = peachColors.T3, fontFamily = PeachType.Mono, fontSize = 9.sp)130                    }131                }132133                is GenerateState.Done -> Column {134                    SummaryRow("verify code", s.manifest.verifyCode, accent)135                    SummaryRow("entries", s.manifest.entryCount.toString(), accent)136                    SummaryRow("codes", "${s.manifest.totalChunks} (${s.manifest.parityChunks} recovery)", accent)137                    SummaryRow("pages", s.manifest.pageCount.toString(), accent)138                    Spacer(Modifier.height(24.dp))139                    CodexButton("Save PDF…", accent, filled = true) { saveLauncher.launch(codexPdfFileName()) }140                    Spacer(Modifier.height(10.dp))141                    CodexButton("Share PDF…", peachColors.T2, filled = false) {142                        val uri = vm.sharePdfUri() ?: return@CodexButton143                        val intent = Intent(Intent.ACTION_SEND).apply {144                            type = "application/pdf"145                            putExtra(Intent.EXTRA_STREAM, uri)146                            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)147                        }148                        context.startActivity(Intent.createChooser(intent, "Share Codex PDF"))149                    }150                    Spacer(Modifier.height(10.dp))151                    CodexButton("Generate again", peachColors.T2, filled = false) { vm.startGenerate(System.currentTimeMillis()) }152                }153154                is GenerateState.Error -> Column {155                    Text("Couldn't generate", color = peachColors.Breach, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)156                    Spacer(Modifier.height(6.dp))157                    Text(s.message, color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)158                    Spacer(Modifier.height(20.dp))159                    CodexButton("Try again", accent, filled = true) { vm.startGenerate(System.currentTimeMillis()) }160                }161            }162        }163    }164}165166internal enum class CodexGenerateGate { PRO_REQUIRED, RECOVERY_REQUIRED, READY }167168internal fun codexGenerateGate(isPro: Boolean, recoveryWordsSet: Boolean): CodexGenerateGate = when {169    !isPro -> CodexGenerateGate.PRO_REQUIRED170    !recoveryWordsSet -> CodexGenerateGate.RECOVERY_REQUIRED171    else -> CodexGenerateGate.READY172}173174internal fun codexPdfFileName(date: LocalDate = LocalDate.now()): String =175    "peach-codex-$date.pdf"176177private fun stepLabel(step: CodexStep): String = when (step) {178    CodexStep.COMPRESS -> "compressing vault…"179    CodexStep.ERASURE -> "adding recovery codes…"180    CodexStep.AZTEC -> "drawing Aztec codes…"181    CodexStep.VERIFY -> "verifying it rebuilds…"182    CodexStep.PDF -> "laying out the page…"183}184185@Composable186internal fun CodexHeader(title: String, onBack: () -> Unit) {187    Row(188        modifier = Modifier.fillMaxWidth().padding(top = 12.dp).height(48.dp),189        verticalAlignment = Alignment.CenterVertically,190    ) {191        Text("‹", color = peachColors.T1, fontSize = 26.sp,192            modifier = Modifier.clip(CircleShape).clickable(onClick = onBack).padding(horizontal = 8.dp))193        Spacer(Modifier.size(8.dp))194        Text(title, color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)195    }196}197198@Composable199private fun SummaryRow(label: String, value: String, accent: Color) {200    Row(Modifier.fillMaxWidth().padding(vertical = 7.dp), horizontalArrangement = Arrangement.SpaceBetween) {201        Text(label, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)202        Text(value, color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)203    }204}205206@Composable207private fun GateField(label: String, value: String, accent: Color, onChange: (String) -> Unit) {208    Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {209        Text(label, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)210        BasicTextField(211            value = value,212            onValueChange = { onChange(it) },213            singleLine = true,214            textStyle = PeachType.bodyMono.copy(color = peachColors.T0),215            cursorBrush = SolidColor(accent),216            modifier = Modifier.fillMaxWidth().padding(top = 4.dp),217        )218        Box(Modifier.fillMaxWidth().padding(top = 4.dp).height(1.dp).background(peachColors.T3))219    }220}221222@Composable223internal fun CodexButton(label: String, color: Color, filled: Boolean, onClick: () -> Unit) {224    val accent = LocalPeachAccent.current225    Box(226        modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp))227            .background(if (filled) accent.copy(alpha = 0.14f) else Color.Transparent)228            .clickable(onClick = onClick).padding(vertical = 15.dp),229        contentAlignment = Alignment.Center,230    ) {231        Text(label, color = if (filled) accent else color, fontFamily = PeachType.Mono, fontSize = 13.sp)232    }233}