PanicScreen.kt

7.3 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.Row10import androidx.compose.foundation.layout.Spacer11import androidx.compose.foundation.layout.fillMaxSize12import androidx.compose.foundation.layout.fillMaxWidth13import androidx.compose.foundation.layout.padding14import androidx.compose.foundation.layout.size15import androidx.compose.foundation.layout.systemBarsPadding16import androidx.compose.foundation.layout.width17import androidx.compose.foundation.shape.RoundedCornerShape18import androidx.compose.foundation.text.BasicTextField19import androidx.compose.material3.Text20import androidx.compose.runtime.Composable21import androidx.compose.runtime.CompositionLocalProvider22import androidx.compose.runtime.getValue23import androidx.compose.runtime.mutableStateOf24import androidx.compose.runtime.remember25import androidx.compose.runtime.setValue26import androidx.compose.ui.Alignment27import androidx.compose.ui.Modifier28import androidx.compose.ui.draw.clip29import androidx.compose.ui.graphics.Color30import androidx.compose.ui.graphics.SolidColor31import androidx.compose.ui.text.TextStyle32import androidx.compose.ui.text.font.FontStyle33import androidx.compose.ui.text.input.TextFieldValue34import androidx.compose.ui.unit.em35import androidx.compose.ui.unit.sp36import androidx.compose.ui.unit.dp37import androidx.hilt.navigation.compose.hiltViewModel38import androidx.lifecycle.ViewModel39import androidx.lifecycle.viewModelScope40import com.peachpasswords.ui.theme.LocalPeachAccent41import com.peachpasswords.ui.theme.LocalPeachColors42import com.peachpasswords.ui.theme.peachColors43import com.peachpasswords.ui.theme.peachDark44import com.peachpasswords.ui.theme.PeachType45import com.peachpasswords.vault.VaultRepository46import dagger.hilt.android.lifecycle.HiltViewModel47import kotlinx.coroutines.launch48import javax.inject.Inject4950@HiltViewModel51class PanicViewModel @Inject constructor(private val vault: VaultRepository) : ViewModel() {52    val entryCount: Int get() = vault.getEntries().size53    fun destroy(onDone: () -> Unit) = viewModelScope.launch { vault.destroyVault(); onDone() }54}5556@Composable57fun PanicScreen(58    onBack: () -> Unit,59    onDestroyed: () -> Unit,60    vm: PanicViewModel = hiltViewModel(),61) {62    var step by remember { mutableStateOf(1) }63    val entryCount = remember { vm.entryCount }6465    // Panic always renders dark regardless of app theme: its tokens must stay66    // readable on the intentional near-black field below (keep the user's accent).67    val accent = LocalPeachAccent.current68    val darkPalette = remember(accent) { peachDark(accent) }6970    CompositionLocalProvider(LocalPeachColors provides darkPalette) {71    // The panic flow is intentionally cold: no bloom, a flat near-black field so72    // it reads as a dead-end rather than another lit-up screen in the app.73    Box(modifier = Modifier.fillMaxSize().background(Color(0xFF0C0A06))) {74        Column(75            modifier = Modifier76                .fillMaxSize()77                .systemBarsPadding()78                .padding(horizontal = 28.dp, vertical = 32.dp),79            verticalArrangement = Arrangement.Center,80        ) {81            when (step) {82                1 -> Step1(onCancel = onBack, onContinue = { step = 2 })83                2 -> Step2(entryCount = entryCount, onBack = { step = 1 }, onContinue = { step = 3 })84                else -> Step3(onDestroy = { vm.destroy(onDestroyed) })85            }86        }87    }88    }89}9091@Composable92private fun Step1(onCancel: () -> Unit, onContinue: () -> Unit) {93    Headline("destroy vault", 44.sp, peachColors.Breach)94    Spacer(Modifier.size(20.dp))95    Text(96        "This will permanently delete your vault and all saved passwords from this device. This cannot be undone.",97        color = peachColors.T1,98        style = PeachType.body,99    )100    Spacer(Modifier.size(28.dp))101    Row(modifier = Modifier.fillMaxWidth()) {102        GhostButton("Cancel", peachColors.T1, onCancel)103        Spacer(Modifier.width(12.dp))104        DangerButton("I understand, continue", enabled = true, onClick = onContinue)105    }106}107108@Composable109private fun Step2(entryCount: Int, onBack: () -> Unit, onContinue: () -> Unit) {110    Headline("are you sure?", 32.sp, peachColors.Breach)111    Spacer(Modifier.size(18.dp))112    Text(113        "You will lose access to all $entryCount saved passwords.",114        color = peachColors.T1,115        style = PeachType.body,116    )117    Spacer(Modifier.size(28.dp))118    Row(modifier = Modifier.fillMaxWidth()) {119        GhostButton("Go back", peachColors.T1, onBack)120        Spacer(Modifier.width(12.dp))121        DangerButton("Yes, destroy it", enabled = true, onClick = onContinue)122    }123}124125@Composable126private fun Step3(onDestroy: () -> Unit) {127    var typed by remember { mutableStateOf(TextFieldValue("")) }128    val accent = LocalPeachAccent.current129    val confirmed = typed.text == "DESTROY"130131    Headline("type DESTROY to confirm", 28.sp, peachColors.T0)132    Spacer(Modifier.size(22.dp))133    BasicTextField(134        value = typed,135        onValueChange = { typed = it },136        singleLine = true,137        textStyle = TextStyle(138            fontFamily = PeachType.Mono,139            fontSize = 18.sp,140            letterSpacing = 0.18.em,141            color = peachColors.T0,142        ),143        cursorBrush = SolidColor(accent),144        modifier = Modifier.fillMaxWidth(),145    )146    Spacer(Modifier.size(6.dp))147    Box(148        modifier = Modifier149            .fillMaxWidth()150            .size(1.dp)151            .background(peachColors.T3),152    )153    Spacer(Modifier.size(28.dp))154    DangerButton("Destroy vault", enabled = confirmed, onClick = onDestroy)155}156157@Composable158private fun Headline(text: String, fontSize: androidx.compose.ui.unit.TextUnit, color: Color) {159    Text(160        text,161        color = color,162        style = TextStyle(163            fontFamily = PeachType.Display,164            fontStyle = FontStyle.Italic,165            fontSize = fontSize,166        ),167    )168}169170@Composable171private fun GhostButton(label: String, color: Color, onClick: () -> Unit) {172    Box(173        modifier = Modifier174            .clip(RoundedCornerShape(999.dp))175            .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))176            .clickable(onClick = onClick)177            .padding(horizontal = 18.dp, vertical = 11.dp),178    ) {179        Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 11.sp))180    }181}182183@Composable184private fun DangerButton(label: String, enabled: Boolean, onClick: () -> Unit) {185    val alpha = if (enabled) 1f else 0.4f186    Box(187        modifier = Modifier188            .clip(RoundedCornerShape(999.dp))189            .background(peachColors.BreachBg.copy(alpha = peachColors.BreachBg.alpha * alpha))190            .border(1.dp, peachColors.Breach.copy(alpha = 0.55f * alpha), RoundedCornerShape(999.dp))191            .clickable(enabled = enabled, onClick = onClick)192            .padding(horizontal = 18.dp, vertical = 11.dp),193    ) {194        Text(195            label,196            color = peachColors.Breach.copy(alpha = alpha),197            style = PeachType.sortPill.copy(fontSize = 11.sp),198        )199    }200}