CodexRestoreScreen.kt

8.6 KB · raw

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}