8dffe25307ef

restore onboarding after vault destroy, add rune glows to panic step 3, add lock FAB above the + FAB

time
2026-07-24 22:25 UTC · 3 months ago
machine
koh2
agent
claude-opus-5
session
panic chaos and lock fab
parent
099f17d7211c
4 files app/src/main/java/com/peachpasswords/ui/components/RuneGridBackground.kt+47 −3app/src/main/java/com/peachpasswords/ui/settings/PanicScreen.kt+30 −2app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+24 −5app/src/main/java/com/peachpasswords/ui/vault/VaultListViewModel.kt+8 −0
app/src/main/java/com/peachpasswords/ui/components/RuneGridBackground.kt+47 −3
@@ -18,10 +18,15 @@1818 import com.peachpasswords.ui.theme.LocalReduceMotion1919 import com.peachpasswords.ui.theme.PeachType2020 import com.peachpasswords.ui.theme.peachColors21+import com.peachpasswords.ui.theme.LocalPeachAccent22+import kotlin.math.PI23+import kotlin.math.sin24+import kotlin.random.Random2125 import kotlin.math.ceil2226 2327 private const val RUNE_COUNT = 2562428 private const val PATTERN_COLUMNS = 1629+private const val GLOW_COUNT = 82530 2631 /**2732  * Continuous PeachScript rune grid for onboarding only. This is deliberately@@ -33,10 +38,17 @@3338  * matching that means not adding one here either (ONBOARDING_ADDENDUM.md §2).3439  */3540 @Composable36-fun RuneGridBackground(modifier: Modifier = Modifier) {41+fun RuneGridBackground(42+    modifier: Modifier = Modifier,43+    alpha: Float = 0.07f,44+    // Browser parity: the panic screen's final step runs the same drifting45+    // field with a few runes flaring and dying at random (RuneCanvas's glows).46+    glowing: Boolean = false,47+) {3748     val reduceMotion = LocalReduceMotion.current49+    val accent = LocalPeachAccent.current3850     val layer = rememberRuneLayer(39-        color = peachColors.T0.copy(alpha = 0.07f),51+        color = peachColors.T0.copy(alpha = alpha),4052         fontSize = 24,4153         tileWidth = 54.dp,4254         rowHeight = 42.dp,@@ -60,7 +72,39 @@6072     }6173 6274     Canvas(modifier) {63-        drawRuneLayer(layer, elapsedSeconds.floatValue)75+        val t = elapsedSeconds.floatValue76+        drawRuneLayer(layer, t)77+        if (glowing) drawRuneGlows(layer, t, accent)78+    }79+}80+81+/** A few runes flare and fade at random positions — the panic screen's chaos. */82+private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawRuneGlows(83+    layer: RuneLayer,84+    elapsedSeconds: Float,85+    accent: Color,86+) {87+    val tileWidth = layer.tileWidth.toPx()88+    val rowHeight = layer.rowHeight.toPx()89+    val cols = ceil(size.width / tileWidth).toInt() + 190+    val rows = ceil(size.height / rowHeight).toInt() + 191+    val rng = Random(layer.seed)92+    repeat(GLOW_COUNT) {93+        // Each glow keeps a fixed cell and its own period/offset, so the field94+        // reads as random flares without tracking mutable per-glow state.95+        val col = rng.nextInt(cols.coerceAtLeast(1))96+        val row = rng.nextInt(rows.coerceAtLeast(1))97+        val period = 2f + rng.nextFloat() * 3f98+        val offset = rng.nextFloat() * period99+        val phase = ((elapsedSeconds + offset) % period) / period100+        val a = sin(phase * PI.toFloat()) * 0.85f101+        if (a <= 0.02f) return@repeat102+        val glyph = layer.glyphs[positiveMod(col * 31 + row * 17 + layer.seed, RUNE_COUNT)]103+        drawText(104+            glyph,105+            topLeft = Offset(col * tileWidth, row * rowHeight),106+            color = accent.copy(alpha = a),107+        )64108     }65109 }66110 

view file

app/src/main/java/com/peachpasswords/ui/settings/PanicScreen.kt+30 −2
@@ -36,6 +36,7 @@3636 import androidx.lifecycle.ViewModel3737 import androidx.lifecycle.viewModelScope3838 import com.peachpasswords.ui.components.PeachField39+import com.peachpasswords.ui.components.RuneGridBackground3940 import com.peachpasswords.ui.components.PeachTopBar4041 import com.peachpasswords.ui.theme.LocalPeachAccent4142 import com.peachpasswords.ui.theme.LocalPeachColors@@ -43,15 +44,32 @@4344 import com.peachpasswords.ui.theme.peachColors4445 import com.peachpasswords.ui.theme.peachDark4546 import com.peachpasswords.ui.theme.PeachType47+import android.content.Context48+import dagger.hilt.android.qualifiers.ApplicationContext49+import com.peachpasswords.ui.onboarding.ONBOARD_KEY50+import com.peachpasswords.ui.onboarding.ONBOARD_PREFS4651 import com.peachpasswords.vault.VaultRepository4752 import dagger.hilt.android.lifecycle.HiltViewModel4853 import kotlinx.coroutines.launch4954 import javax.inject.Inject5055 5156 @HiltViewModel52-class PanicViewModel @Inject constructor(private val vault: VaultRepository) : ViewModel() {57+class PanicViewModel @Inject constructor(58+    private val vault: VaultRepository,59+    @ApplicationContext private val appContext: Context,60+) : ViewModel() {5361     val entryCount: Int get() = vault.getEntries().size54-    fun destroy(onDone: () -> Unit) = viewModelScope.launch { vault.destroyVault(); onDone() }62+63+    fun destroy(onDone: () -> Unit) = viewModelScope.launch {64+        vault.destroyVault()65+        // Destroying the vault puts the app back to factory state, so clear the66+        // "already onboarded" flag too — otherwise the next launch skipped the67+        // intro and dropped straight onto create-vault, which is not what a68+        // fresh start should look like.69+        appContext.getSharedPreferences(ONBOARD_PREFS, Context.MODE_PRIVATE)70+            .edit().remove(ONBOARD_KEY).apply()71+        onDone()72+    }5573 }5674 5775 @Composable@@ -72,6 +90,16 @@7290     // The panic flow is intentionally cold: no bloom, a flat near-black field so7391     // it reads as a dead-end rather than another lit-up screen in the app.7492     Box(modifier = Modifier.fillMaxSize().background(Color(0xFF0C0A06))) {93+        // Browser parity: the final step runs a drifting rune field with runes94+        // flaring and dying behind it. The earlier steps stay deliberately cold95+        // and flat — the chaos is what marks this as the point of no return.96+        if (step == 3) {97+            RuneGridBackground(98+                modifier = Modifier.fillMaxSize(),99+                alpha = 0.22f,100+                glowing = true,101+            )102+        }75103         Column(76104             modifier = Modifier77105                 .fillMaxSize()

view file

app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+24 −5
@@ -43,6 +43,8 @@4343 import androidx.compose.material.icons.rounded.Close4444 import androidx.compose.material.icons.rounded.Search4545 import androidx.compose.material.icons.rounded.Settings46+import androidx.compose.material3.SmallFloatingActionButton47+import androidx.compose.material.icons.outlined.Lock4648 import androidx.compose.material3.FloatingActionButton4749 import androidx.compose.material3.Icon4850 import androidx.compose.material3.TextButton@@ -248,16 +250,33 @@248250                 // Standard Android list+FAB convention, replacing the old249251                 // "+ new entry" row that floated mid-list (centered in250252                 // whatever dead space a short list left below its rows).251-                FloatingActionButton(252-                    onClick = { onNavigate(PeachNav.EntryEdit(null)) },253+                Column(253254                     modifier = Modifier254255                         .align(Alignment.BottomEnd)255256                         .padding(PeachSpacing.lg)256257                         .navigationBarsPadding(),257-                    containerColor = LocalPeachAccent.current,258-                    contentColor = peachColors.Bg,258+                    horizontalAlignment = Alignment.CenterHorizontally,259+                    verticalArrangement = Arrangement.spacedBy(PeachSpacing.md),259260                 ) {260-                    Icon(Icons.Rounded.Add, contentDescription = "new entry")261+                    // Small and quiet above the accent FAB: locking is a262+                    // frequent action but not the screen's primary one, and263+                    // from an empty vault there was previously no way to lock264+                    // at all (the lock lives on the entry-detail top bar,265+                    // which does not exist when there are no entries).266+                    SmallFloatingActionButton(267+                        onClick = vm::lock,268+                        containerColor = peachColors.S1,269+                        contentColor = peachColors.T1,270+                    ) {271+                        Icon(Icons.Outlined.Lock, contentDescription = "Lock vault")272+                    }273+                    FloatingActionButton(274+                        onClick = { onNavigate(PeachNav.EntryEdit(null)) },275+                        containerColor = LocalPeachAccent.current,276+                        contentColor = peachColors.Bg,277+                    ) {278+                        Icon(Icons.Rounded.Add, contentDescription = "new entry")279+                    }261280                 }262281             }263282         }

view file

app/src/main/java/com/peachpasswords/ui/vault/VaultListViewModel.kt+8 −0
@@ -11,6 +11,7 @@1111 import com.peachpasswords.vault.TOTPGenerator1212 import com.peachpasswords.vault.PasskeyData1313 import com.peachpasswords.vault.VaultEntry14+import com.peachpasswords.vault.VaultLockTimer1415 import com.peachpasswords.vault.VaultRepository1516 import com.peachpasswords.vault.isLogin1617 import com.peachpasswords.vault.isNote@@ -38,6 +39,7 @@3839     private val syncAlertStore: SyncAlertStore,3940     private val proNoticeStore: ProNoticeStore,4041     @ApplicationContext private val appContext: Context,42+    private val lockTimer: VaultLockTimer,4143 ) : ViewModel() {4244 4345     private val _sort = MutableStateFlow(SortOrder.SMART)@@ -79,6 +81,12 @@7981     fun setSearch(q: String) { _search.value = q }8082     fun setTagFilter(tag: String?) { _tagFilter.value = tag }8183     fun toggleNoteFilter() { _noteOnly.value = !_noteOnly.value }84+85+    /** Lock the vault from the list's lock button; the nav host drops to Lock. */86+    fun lock() {87+        vault.lock()88+        lockTimer.cancel()89+    }8290 8391     fun dismissSyncAlert(alert: SyncAlert) {8492         syncAlertStore.dismiss(alert)

view file