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
↓ peach-android-8dffe253.tar.gzatlas at this save
files changed4 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 −0app/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 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()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 }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)