package com.peachpasswords.ui.vault import android.view.WindowManager import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.runtime.collectAsState import androidx.fragment.app.FragmentActivity import androidx.hilt.navigation.compose.hiltViewModel import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.ui.theme.ShellBackground /** * E2 — full-screen editor for NOTE-type entries. Editable inline title, a B/I/U * formatting toolbar that applies [SpanStyle] to the current selection, a * full-width Inter body field, and an 800ms-debounced auto-save with a "saved." * pulse. FLAG_SECURE blocks screenshots while open (same as EntryDetail). * * Span support is intentionally basic (no markdown/persistence of styles): the * toolbar styles the live [AnnotatedString] selection; only the plain text is * persisted via the ViewModel. */ @Composable fun NotesModeScreen( onBack: () -> Unit, vm: NotesModeViewModel = hiltViewModel(), ) { val entry by vm.entry.collectAsState() val savedPulse by vm.savedPulse.collectAsState() val accent = LocalPeachAccent.current val context = LocalContext.current // FLAG_SECURE only while shown. androidx.compose.runtime.DisposableEffect(Unit) { val window = (context as? FragmentActivity)?.window window?.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } val e = entry // "saved." indicator fades in on each save pulse, holds, then fades out. var showSaved by remember { mutableStateOf(false) } LaunchedEffect(savedPulse) { if (savedPulse == 0L) return@LaunchedEffect showSaved = true kotlinx.coroutines.delay(1300) showSaved = false } val savedAlpha by animateFloatAsState( targetValue = if (showSaved) 1f else 0f, animationSpec = tween(300), label = "saved-fade", ) var title by remember(e?.id) { mutableStateOf(e?.title ?: "") } var body by remember(e?.id) { mutableStateOf(TextFieldValue(e?.notes ?: "")) } ShellBackground { Column(Modifier.fillMaxSize().statusBarsPadding()) { // Top bar: back · editable title · "saved." indicator Row( Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0, modifier = Modifier.size(40.dp).clip(CircleShape) .clickable { vm.saveTitleNow(title); onBack() }.padding(8.dp), ) Box(Modifier.weight(1f).padding(horizontal = 4.dp)) { if (title.isEmpty()) { Text( "untitled note", color = peachColors.T2, style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp), ) } BasicTextField( value = title, onValueChange = { title = it; vm.onTitleChanged(it) }, singleLine = true, textStyle = TextStyle( color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp, ), cursorBrush = SolidColor(accent), modifier = Modifier.fillMaxWidth(), ) } Text( "saved.", color = peachColors.T2, style = TextStyle(fontFamily = PeachType.Mono, fontSize = 9.sp), modifier = Modifier.padding(end = 8.dp).alpha(savedAlpha), ) } if (e == null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("note not found", color = peachColors.T1, style = PeachType.bodyMono) } return@Column } // Formatting toolbar: B / I / U — apply span to current selection. Row( Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, ) { FormatButton("B", FontWeight.Bold) { body = applySpan(body, SpanStyle(fontWeight = FontWeight.Bold)) } FormatButton("I", fontStyle = FontStyle.Italic) { body = applySpan(body, SpanStyle(fontStyle = FontStyle.Italic)) } FormatButton("U", underline = true) { body = applySpan(body, SpanStyle(textDecoration = TextDecoration.Underline)) } } // Content area. Box(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 12.dp)) { if (body.text.isEmpty()) { Text( "Start writing…", color = peachColors.T2, style = TextStyle(fontFamily = PeachType.Body, fontStyle = FontStyle.Italic, fontSize = 14.sp), ) } BasicTextField( value = body, onValueChange = { body = it; vm.onContentChanged(it.text) }, textStyle = TextStyle(color = peachColors.T0, fontFamily = PeachType.Body, fontSize = 14.sp, lineHeight = 22.sp), cursorBrush = SolidColor(accent), modifier = Modifier.fillMaxSize(), ) } } } } /** * Apply [style] to the current selection of [value], preserving the text and * existing spans. If the selection is collapsed, the value is returned unchanged * (basic span support — styling needs a non-empty selection). */ private fun applySpan(value: TextFieldValue, style: SpanStyle): TextFieldValue { val sel = value.selection if (sel.collapsed) return value val start = sel.min val end = sel.max val existing = value.annotatedString val merged: AnnotatedString = buildAnnotatedString { append(existing) addStyle(style, start, end) } return value.copy(annotatedString = merged) } @Composable private fun FormatButton( label: String, fontWeight: FontWeight? = null, fontStyle: FontStyle? = null, underline: Boolean = false, onClick: () -> Unit, ) { Box( Modifier .size(30.dp) .clip(RoundedCornerShape(6.dp)) .background(peachColors.S2) .clickable(onClick = onClick), contentAlignment = Alignment.Center, ) { Text( label, color = peachColors.T0, style = TextStyle( fontFamily = PeachType.Body, fontSize = 14.sp, fontWeight = fontWeight, fontStyle = fontStyle, textDecoration = if (underline) TextDecoration.Underline else null, ), ) } }