package com.peachpasswords.ui.codex import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.peachpasswords.ui.components.PeachEmptyOrEndState import com.peachpasswords.ui.components.PeachField import com.peachpasswords.ui.components.SecureFlagEffect import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.PeachSpacing import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.ui.theme.ShellBackground import com.peachpasswords.ui.theme.peachColors @Composable fun CodexRestoreScreen(onBack: () -> Unit, onRestored: () -> Unit, vm: CodexViewModel = hiltViewModel()) { val state by vm.restore.collectAsStateWithLifecycle() val cameraState by vm.camera.collectAsStateWithLifecycle() val accent = LocalPeachAccent.current val pickLauncher = rememberLauncherForActivityResult( ActivityResultContracts.GetContent(), ) { uri -> uri?.let { vm.scan(it) } } fun launchPicker() { vm.expectTransientBackground() pickLauncher.launch("application/pdf") } SecureFlagEffect() ShellBackground { Column(Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = PeachSpacing.xl)) { Spacer(Modifier.height(PeachSpacing.md)) CodexTopBar("Codex restore", onBack) Spacer(Modifier.height(PeachSpacing.sm)) Text( "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.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp, ) Spacer(Modifier.height(PeachSpacing.xxl)) when (val s = state) { is RestoreState.Idle -> when (val cs = cameraState) { is CameraScanState.Choosing -> CodexRestoreChoose( accent = accent, onScanCamera = { vm.startCameraScan() }, onImportPdf = { launchPicker() }, modifier = Modifier.weight(1f), ) is CameraScanState.Scanning -> CodexCameraScan( accent = accent, timedOut = false, codesFound = cs.codesFound, onFrame = { vm.onCameraFrame(it) }, onTimeout = { vm.cameraTimedOut() }, onRetry = { vm.startCameraScan() }, ) is CameraScanState.Timeout -> CodexCameraScan( accent = accent, timedOut = true, codesFound = cs.codesFound, onFrame = { vm.onCameraFrame(it) }, onTimeout = { }, onRetry = { vm.startCameraScan() }, ) } is RestoreState.Scanning -> Text( if (s.pages == 0) "Reading…" else "Reading page ${s.page} of ${s.pages}…", color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 12.sp, ) is RestoreState.Shortfall -> ShortfallBody( s = s, accent = accent, onRetryPdf = { vm.resetRestore(); launchPicker() }, onUseCamera = { vm.startCameraScan() }, ) is RestoreState.Decoded -> DecodedBody(s, accent, vm, onRestored) is RestoreState.Restoring -> Text("Restoring vault…", color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 12.sp) is RestoreState.Restored -> PeachEmptyOrEndState( message = "Vault restored.", detail = "Your restored vault is ready.", ) is RestoreState.Error -> PeachEmptyOrEndState( message = "Couldn't restore", detail = s.message, action = { CodexButton("Choose another file", accent, filled = true) { vm.resetRestore() launchPicker() } }, ) } } } } @Composable private fun ShortfallBody( s: RestoreState.Shortfall, accent: Color, onRetryPdf: () -> Unit, onUseCamera: () -> Unit, ) { Column { Text( "PDF import needs another pass.", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp, ) Spacer(Modifier.height(PeachSpacing.xs)) Text( "The PDF decoded ${s.found} codes, but ${s.missing} remained unreadable. " + (if (s.ready == s.total) { "All stripes are ready now." } else { "${s.ready} of ${s.total} stripes are ready." }), color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp, ) Spacer(Modifier.height(PeachSpacing.sm)) Text( "Try importing the PDF again to fill the gaps, or switch to camera scanning if it keeps failing.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp, ) Spacer(Modifier.height(PeachSpacing.xl)) CodexButton("Try PDF again", accent, filled = true, onClick = onRetryPdf) Spacer(Modifier.height(PeachSpacing.sm)) CodexButton("Use camera", peachColors.T2, filled = false, onClick = onUseCamera) } } @Composable private fun DecodedBody(s: RestoreState.Decoded, accent: Color, vm: CodexViewModel, onRestored: () -> Unit) { var password by remember { mutableStateOf("") } Column { Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) { Text("verify code", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp) Text(s.payload.verifyCode, color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp) } Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) { Text("entries", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp) Text(s.payload.entryCount.toString(), color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp) } Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) { Text("codes read", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp) Text(s.codesFound.toString(), color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp) } Spacer(Modifier.height(PeachSpacing.xl)) PeachField( label = "Master password", value = password, onValueChange = { password = it }, visualTransformation = PasswordVisualTransformation(), keyboardType = KeyboardType.Password, singleLine = true, ) Spacer(Modifier.height(PeachSpacing.lg)) CodexButton("Restore this vault", accent, filled = true) { if (password.isNotEmpty()) vm.confirmRestore(password, onRestored) } } }