1package com.peachpasswords.ui.codex23import androidx.activity.compose.rememberLauncherForActivityResult4import androidx.activity.result.contract.ActivityResultContracts5import androidx.compose.foundation.layout.Arrangement6import androidx.compose.foundation.layout.Column7import androidx.compose.foundation.layout.Row8import androidx.compose.foundation.layout.Spacer9import androidx.compose.foundation.layout.fillMaxSize10import androidx.compose.foundation.layout.fillMaxWidth11import androidx.compose.foundation.layout.height12import androidx.compose.foundation.layout.padding13import androidx.compose.foundation.layout.systemBarsPadding14import androidx.compose.material3.Text15import androidx.compose.runtime.Composable16import androidx.compose.runtime.getValue17import androidx.compose.runtime.mutableStateOf18import androidx.compose.runtime.remember19import androidx.compose.runtime.setValue20import androidx.compose.ui.Modifier21import androidx.compose.ui.graphics.Color22import androidx.compose.ui.text.font.FontStyle23import androidx.compose.ui.text.input.KeyboardType24import androidx.compose.ui.text.input.PasswordVisualTransformation25import androidx.compose.ui.unit.sp26import androidx.hilt.navigation.compose.hiltViewModel27import androidx.lifecycle.compose.collectAsStateWithLifecycle28import com.peachpasswords.ui.components.PeachEmptyOrEndState29import com.peachpasswords.ui.components.PeachField30import com.peachpasswords.ui.components.SecureFlagEffect31import com.peachpasswords.ui.theme.LocalPeachAccent32import com.peachpasswords.ui.theme.PeachSpacing33import com.peachpasswords.ui.theme.PeachType34import com.peachpasswords.ui.theme.ShellBackground35import com.peachpasswords.ui.theme.peachColors3637@Composable38fun CodexRestoreScreen(onBack: () -> Unit, onRestored: () -> Unit, vm: CodexViewModel = hiltViewModel()) {39 val state by vm.restore.collectAsStateWithLifecycle()40 val cameraState by vm.camera.collectAsStateWithLifecycle()41 val accent = LocalPeachAccent.current42 val pickLauncher = rememberLauncherForActivityResult(43 ActivityResultContracts.GetContent(),44 ) { uri -> uri?.let { vm.scan(it) } }45 fun launchPicker() {46 vm.expectTransientBackground()47 pickLauncher.launch("application/pdf")48 }4950 SecureFlagEffect()5152 ShellBackground {53 Column(Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = PeachSpacing.xl)) {54 Spacer(Modifier.height(PeachSpacing.md))55 CodexTopBar("Codex restore", onBack)56 Spacer(Modifier.height(PeachSpacing.sm))57 Text(58 "Choose a Codex PDF. Peach reads its Aztec codes — even if some pages are missing or smudged — and rebuilds your encrypted vault. You'll need the master password.",59 color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,60 )61 Spacer(Modifier.height(PeachSpacing.xxl))6263 when (val s = state) {64 is RestoreState.Idle -> when (val cs = cameraState) {65 is CameraScanState.Choosing -> CodexRestoreChoose(66 accent = accent,67 onScanCamera = { vm.startCameraScan() },68 onImportPdf = { launchPicker() },69 modifier = Modifier.weight(1f),70 )71 is CameraScanState.Scanning -> CodexCameraScan(72 accent = accent,73 timedOut = false,74 codesFound = cs.codesFound,75 onFrame = { vm.onCameraFrame(it) },76 onTimeout = { vm.cameraTimedOut() },77 onRetry = { vm.startCameraScan() },78 )79 is CameraScanState.Timeout -> CodexCameraScan(80 accent = accent,81 timedOut = true,82 codesFound = cs.codesFound,83 onFrame = { vm.onCameraFrame(it) },84 onTimeout = { },85 onRetry = { vm.startCameraScan() },86 )87 }8889 is RestoreState.Scanning -> Text(90 if (s.pages == 0) "Reading…" else "Reading page ${s.page} of ${s.pages}…",91 color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 12.sp,92 )9394 is RestoreState.Shortfall -> ShortfallBody(95 s = s,96 accent = accent,97 onRetryPdf = { vm.resetRestore(); launchPicker() },98 onUseCamera = { vm.startCameraScan() },99 )100101 is RestoreState.Decoded -> DecodedBody(s, accent, vm, onRestored)102103 is RestoreState.Restoring ->104 Text("Restoring vault…", color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 12.sp)105106 is RestoreState.Restored -> PeachEmptyOrEndState(107 message = "Vault restored.",108 detail = "Your restored vault is ready.",109 )110111 is RestoreState.Error -> PeachEmptyOrEndState(112 message = "Couldn't restore",113 detail = s.message,114 action = {115 CodexButton("Choose another file", accent, filled = true) {116 vm.resetRestore()117 launchPicker()118 }119 },120 )121 }122 }123 }124}125126@Composable127private fun ShortfallBody(128 s: RestoreState.Shortfall,129 accent: Color,130 onRetryPdf: () -> Unit,131 onUseCamera: () -> Unit,132) {133 Column {134 Text(135 "PDF import needs another pass.",136 color = peachColors.T1,137 fontFamily = PeachType.Display,138 fontStyle = FontStyle.Italic,139 fontSize = 19.sp,140 )141 Spacer(Modifier.height(PeachSpacing.xs))142 Text(143 "The PDF decoded ${s.found} codes, but ${s.missing} remained unreadable. " +144 (if (s.ready == s.total) {145 "All stripes are ready now."146 } else {147 "${s.ready} of ${s.total} stripes are ready."148 }),149 color = peachColors.T2,150 fontFamily = PeachType.Body,151 fontSize = 13.sp,152 )153 Spacer(Modifier.height(PeachSpacing.sm))154 Text(155 "Try importing the PDF again to fill the gaps, or switch to camera scanning if it keeps failing.",156 color = peachColors.T2,157 fontFamily = PeachType.Body,158 fontSize = 13.sp,159 )160 Spacer(Modifier.height(PeachSpacing.xl))161 CodexButton("Try PDF again", accent, filled = true, onClick = onRetryPdf)162 Spacer(Modifier.height(PeachSpacing.sm))163 CodexButton("Use camera", peachColors.T2, filled = false, onClick = onUseCamera)164 }165}166167@Composable168private fun DecodedBody(s: RestoreState.Decoded, accent: Color, vm: CodexViewModel, onRestored: () -> Unit) {169 var password by remember { mutableStateOf("") }170 Column {171 Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {172 Text("verify code", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)173 Text(s.payload.verifyCode, color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)174 }175 Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {176 Text("entries", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)177 Text(s.payload.entryCount.toString(), color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)178 }179 Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {180 Text("codes read", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)181 Text(s.codesFound.toString(), color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)182 }183 Spacer(Modifier.height(PeachSpacing.xl))184 PeachField(185 label = "Master password",186 value = password,187 onValueChange = { password = it },188 visualTransformation = PasswordVisualTransformation(),189 keyboardType = KeyboardType.Password,190 singleLine = true,191 )192 Spacer(Modifier.height(PeachSpacing.lg))193 CodexButton("Restore this vault", accent, filled = true) {194 if (password.isNotEmpty()) vm.confirmRestore(password, onRestored)195 }196 }197}