PanicScreen.kt

8.0 KB · raw

1package com.peachpasswords.ui.settings23import androidx.compose.foundation.background4import androidx.compose.foundation.border5import androidx.compose.foundation.clickable6import androidx.compose.foundation.layout.Arrangement7import androidx.compose.foundation.layout.Box8import androidx.compose.foundation.layout.Column9import androidx.compose.foundation.layout.Spacer10import androidx.compose.foundation.layout.fillMaxSize11import androidx.compose.foundation.layout.fillMaxWidth12import androidx.compose.foundation.layout.height13import androidx.compose.foundation.layout.padding14import androidx.compose.foundation.layout.size15import androidx.compose.foundation.layout.systemBarsPadding16import androidx.compose.foundation.shape.RoundedCornerShape17import androidx.compose.material.icons.Icons18import androidx.compose.material.icons.automirrored.rounded.ArrowBack19import androidx.compose.material3.Icon20import androidx.compose.material3.Text21import androidx.compose.runtime.Composable22import androidx.compose.runtime.CompositionLocalProvider23import 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.text.TextStyle32import androidx.compose.ui.text.font.FontStyle33import androidx.compose.ui.unit.sp34import androidx.compose.ui.unit.dp35import androidx.hilt.navigation.compose.hiltViewModel36import androidx.lifecycle.ViewModel37import androidx.lifecycle.viewModelScope38import com.peachpasswords.ui.components.PeachField39import com.peachpasswords.ui.components.PeachTopBar40import com.peachpasswords.ui.theme.LocalPeachAccent41import com.peachpasswords.ui.theme.LocalPeachColors42import com.peachpasswords.ui.theme.PeachSpacing43import com.peachpasswords.ui.theme.peachColors44import com.peachpasswords.ui.theme.peachDark45import com.peachpasswords.ui.theme.PeachType46import com.peachpasswords.vault.VaultRepository47import dagger.hilt.android.lifecycle.HiltViewModel48import kotlinx.coroutines.launch49import javax.inject.Inject5051@HiltViewModel52class PanicViewModel @Inject constructor(private val vault: VaultRepository) : ViewModel() {53    val entryCount: Int get() = vault.getEntries().size54    fun destroy(onDone: () -> Unit) = viewModelScope.launch { vault.destroyVault(); onDone() }55}5657@Composable58fun PanicScreen(59    onBack: () -> Unit,60    onDestroyed: () -> Unit,61    vm: PanicViewModel = hiltViewModel(),62) {63    var step by remember { mutableStateOf(1) }64    val entryCount = remember { vm.entryCount }6566    // Panic always renders dark regardless of app theme: its tokens must stay67    // readable on the intentional near-black field below (keep the user's accent).68    val accent = LocalPeachAccent.current69    val darkPalette = remember(accent) { peachDark(accent) }7071    CompositionLocalProvider(LocalPeachColors provides darkPalette) {72    // The panic flow is intentionally cold: no bloom, a flat near-black field so73    // it reads as a dead-end rather than another lit-up screen in the app.74    Box(modifier = Modifier.fillMaxSize().background(Color(0xFF0C0A06))) {75        Column(76            modifier = Modifier77                .fillMaxSize()78                .systemBarsPadding()79                .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),80        ) {81            PeachTopBar(82                title = "Panic",83                leading = {84                    Icon(85                        Icons.AutoMirrored.Rounded.ArrowBack,86                        contentDescription = "back",87                        tint = peachColors.T0,88                        modifier = Modifier89                            .size(22.dp)90                            .clip(RoundedCornerShape(PeachSpacing.sm))91                            .clickable(onClick = onBack),92                    )93                },94            )95            Box(96                modifier = Modifier.fillMaxSize().padding(horizontal = PeachSpacing.sm),97                contentAlignment = Alignment.Center,98            ) {99                Column(verticalArrangement = Arrangement.Center) {100                    when (step) {101                        1 -> Step1(onCancel = onBack, onContinue = { step = 2 })102                        2 -> Step2(entryCount = entryCount, onBack = { step = 1 }, onContinue = { step = 3 })103                        else -> Step3(onDestroy = { vm.destroy(onDestroyed) })104                    }105                }106            }107        }108    }109    }110}111112@Composable113private fun Step1(onCancel: () -> Unit, onContinue: () -> Unit) {114    Headline("destroy vault", 44.sp, peachColors.Breach)115    Spacer(Modifier.height(PeachSpacing.lg))116    Text(117        "This will permanently delete your vault and all saved passwords from this device. This cannot be undone.",118        color = peachColors.T1,119        style = PeachType.body,120    )121    Spacer(Modifier.height(PeachSpacing.xl))122    Column(123        modifier = Modifier.fillMaxWidth(),124        verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),125    ) {126        GhostButton("Cancel", peachColors.T1, onCancel)127        DangerButton("I understand, continue", enabled = true, onClick = onContinue)128    }129}130131@Composable132private fun Step2(entryCount: Int, onBack: () -> Unit, onContinue: () -> Unit) {133    Headline("are you sure?", 32.sp, peachColors.Breach)134    Spacer(Modifier.height(PeachSpacing.lg))135    Text(136        "You will lose access to all $entryCount saved passwords.",137        color = peachColors.T1,138        style = PeachType.body,139    )140    Spacer(Modifier.height(PeachSpacing.xl))141    Column(142        modifier = Modifier.fillMaxWidth(),143        verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),144    ) {145        GhostButton("Go back", peachColors.T1, onBack)146        DangerButton("Yes, destroy it", enabled = true, onClick = onContinue)147    }148}149150@Composable151private fun Step3(onDestroy: () -> Unit) {152    var typed by remember { mutableStateOf("") }153    val confirmed = typed == "DESTROY"154155    Headline("type DESTROY to confirm", 28.sp, peachColors.T0)156    Spacer(Modifier.height(PeachSpacing.lg))157    PeachField(158        label = "confirmation",159        value = typed,160        onValueChange = { typed = it },161    )162    Spacer(Modifier.height(PeachSpacing.xl))163    DangerButton("Destroy vault", enabled = confirmed, onClick = onDestroy)164}165166@Composable167private fun Headline(text: String, fontSize: androidx.compose.ui.unit.TextUnit, color: Color) {168    Text(169        text,170        color = color,171        style = TextStyle(172            fontFamily = PeachType.Display,173            fontStyle = FontStyle.Italic,174            fontSize = fontSize,175        ),176    )177}178179@Composable180private fun GhostButton(label: String, color: Color, onClick: () -> Unit) {181    Box(182        modifier = Modifier183            .fillMaxWidth()184            .clip(RoundedCornerShape(999.dp))185            .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))186            .clickable(onClick = onClick)187            .padding(horizontal = PeachSpacing.lg, vertical = 11.dp),188        contentAlignment = Alignment.Center,189    ) {190        Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 11.sp))191    }192}193194@Composable195private fun DangerButton(label: String, enabled: Boolean, onClick: () -> Unit) {196    val alpha = if (enabled) 1f else 0.4f197    Box(198        modifier = Modifier199            .fillMaxWidth()200            .clip(RoundedCornerShape(999.dp))201            .background(peachColors.BreachBg.copy(alpha = peachColors.BreachBg.alpha * alpha))202            .border(1.dp, peachColors.Breach.copy(alpha = 0.55f * alpha), RoundedCornerShape(999.dp))203            .clickable(enabled = enabled, onClick = onClick)204            .padding(horizontal = PeachSpacing.lg, vertical = 11.dp),205        contentAlignment = Alignment.Center,206    ) {207        Text(208            label,209            color = peachColors.Breach.copy(alpha = alpha),210            style = PeachType.sortPill.copy(fontSize = 11.sp),211        )212    }213}