NotesModeScreen.kt

9.9 KB · raw

1package com.peachpasswords.ui.vault23import android.view.WindowManager4import androidx.compose.animation.core.animateFloatAsState5import androidx.compose.animation.core.tween6import androidx.compose.foundation.background7import androidx.compose.foundation.clickable8import androidx.compose.foundation.layout.Arrangement9import androidx.compose.foundation.layout.Box10import androidx.compose.foundation.layout.Column11import androidx.compose.foundation.layout.Row12import androidx.compose.foundation.layout.Spacer13import androidx.compose.foundation.layout.fillMaxSize14import androidx.compose.foundation.layout.fillMaxWidth15import androidx.compose.foundation.layout.height16import androidx.compose.foundation.layout.padding17import androidx.compose.foundation.layout.size18import androidx.compose.foundation.layout.statusBarsPadding19import androidx.compose.foundation.layout.width20import androidx.compose.foundation.shape.CircleShape21import androidx.compose.foundation.shape.RoundedCornerShape22import androidx.compose.foundation.text.BasicTextField23import androidx.compose.material.icons.Icons24import androidx.compose.material.icons.automirrored.rounded.ArrowBack25import androidx.compose.material.icons.outlined.DeleteOutline26import androidx.compose.material3.Icon27import androidx.compose.material3.Text28import androidx.compose.runtime.Composable29import androidx.compose.runtime.LaunchedEffect30import androidx.compose.runtime.getValue31import androidx.compose.runtime.mutableStateOf32import androidx.compose.runtime.remember33import androidx.compose.runtime.setValue34import androidx.compose.ui.Alignment35import androidx.compose.ui.Modifier36import androidx.compose.ui.draw.alpha37import androidx.compose.ui.draw.clip38import androidx.compose.ui.graphics.SolidColor39import androidx.compose.ui.platform.LocalContext40import androidx.compose.ui.text.AnnotatedString41import androidx.compose.ui.text.SpanStyle42import androidx.compose.ui.text.TextStyle43import androidx.compose.ui.text.buildAnnotatedString44import androidx.compose.ui.text.font.FontStyle45import androidx.compose.ui.text.font.FontWeight46import androidx.compose.ui.text.input.TextFieldValue47import androidx.compose.ui.text.style.TextDecoration48import androidx.compose.ui.text.withStyle49import androidx.compose.ui.unit.dp50import androidx.compose.ui.unit.sp51import androidx.compose.runtime.collectAsState52import androidx.fragment.app.FragmentActivity53import androidx.hilt.navigation.compose.hiltViewModel54import com.peachpasswords.ui.theme.LocalPeachAccent55import com.peachpasswords.ui.theme.peachColors56import com.peachpasswords.ui.theme.PeachType57import com.peachpasswords.ui.theme.ShellBackground5859/**60 * E2 — full-screen editor for NOTE-type entries. Editable inline title, a B/I/U61 * formatting toolbar that applies [SpanStyle] to the current selection, a62 * full-width Inter body field, and an 800ms-debounced auto-save with a "saved."63 * pulse. FLAG_SECURE blocks screenshots while open (same as EntryDetail).64 *65 * Span support is intentionally basic (no markdown/persistence of styles): the66 * toolbar styles the live [AnnotatedString] selection; only the plain text is67 * persisted via the ViewModel.68 */69@Composable70fun NotesModeScreen(71    onBack: () -> Unit,72    vm: NotesModeViewModel = hiltViewModel(),73) {74    val entry by vm.entry.collectAsState()75    val savedPulse by vm.savedPulse.collectAsState()76    val accent = LocalPeachAccent.current77    val context = LocalContext.current7879    // FLAG_SECURE only while shown.80    androidx.compose.runtime.DisposableEffect(Unit) {81        val window = (context as? FragmentActivity)?.window82        window?.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)83        onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }84    }8586    val e = entry8788    // "saved." indicator fades in on each save pulse, holds, then fades out.89    var showSaved by remember { mutableStateOf(false) }90    LaunchedEffect(savedPulse) {91        if (savedPulse == 0L) return@LaunchedEffect92        showSaved = true93        kotlinx.coroutines.delay(1300)94        showSaved = false95    }96    val savedAlpha by animateFloatAsState(97        targetValue = if (showSaved) 1f else 0f,98        animationSpec = tween(300),99        label = "saved-fade",100    )101102    var title by remember(e?.id) { mutableStateOf(e?.title ?: "") }103    var body by remember(e?.id) { mutableStateOf(TextFieldValue(e?.notes ?: "")) }104105    ShellBackground {106        Column(Modifier.fillMaxSize().statusBarsPadding()) {107            // Top bar: back · editable title · "saved." indicator108            Row(109                Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 8.dp),110                verticalAlignment = Alignment.CenterVertically,111            ) {112                Icon(113                    Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0,114                    modifier = Modifier.size(40.dp).clip(CircleShape)115                        .clickable { vm.saveNow(title, body.text); onBack() }.padding(8.dp),116                )117                Box(Modifier.weight(1f).padding(horizontal = 4.dp)) {118                    if (title.isEmpty()) {119                        Text(120                            "untitled note",121                            color = peachColors.T2,122                            style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp),123                        )124                    }125                    BasicTextField(126                        value = title,127                        onValueChange = { title = it; vm.onTitleChanged(it) },128                        singleLine = true,129                        textStyle = TextStyle(130                            color = peachColors.T0,131                            fontFamily = PeachType.Display,132                            fontStyle = FontStyle.Italic,133                            fontSize = 22.sp,134                        ),135                        cursorBrush = SolidColor(accent),136                        modifier = Modifier.fillMaxWidth(),137                    )138                }139                Text(140                    "saved.",141                    color = peachColors.T2,142                    style = TextStyle(fontFamily = PeachType.Mono, fontSize = 9.sp),143                    modifier = Modifier.padding(end = 8.dp).alpha(savedAlpha),144                )145                Icon(146                    Icons.Outlined.DeleteOutline,147                    contentDescription = "delete note",148                    tint = peachColors.T2,149                    modifier = Modifier.size(36.dp).clip(CircleShape)150                        .clickable { vm.delete(onBack) }151                        .padding(8.dp),152                )153            }154155            if (e == null) {156                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {157                    Text("note not found", color = peachColors.T1, style = PeachType.bodyMono)158                }159                return@Column160            }161162            // Formatting toolbar: B / I / U — apply span to current selection.163            Row(164                Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),165                horizontalArrangement = Arrangement.spacedBy(6.dp),166                verticalAlignment = Alignment.CenterVertically,167            ) {168                FormatButton("B", FontWeight.Bold) { body = applySpan(body, SpanStyle(fontWeight = FontWeight.Bold)) }169                FormatButton("I", fontStyle = FontStyle.Italic) { body = applySpan(body, SpanStyle(fontStyle = FontStyle.Italic)) }170                FormatButton("U", underline = true) { body = applySpan(body, SpanStyle(textDecoration = TextDecoration.Underline)) }171            }172173            // Content area.174            Box(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 12.dp)) {175                if (body.text.isEmpty()) {176                    Text(177                        "Start writing…",178                        color = peachColors.T2,179                        style = TextStyle(fontFamily = PeachType.Body, fontStyle = FontStyle.Italic, fontSize = 14.sp),180                    )181                }182                BasicTextField(183                    value = body,184                    onValueChange = { body = it; vm.onContentChanged(it.text) },185                    textStyle = TextStyle(color = peachColors.T0, fontFamily = PeachType.Body, fontSize = 14.sp, lineHeight = 22.sp),186                    cursorBrush = SolidColor(accent),187                    modifier = Modifier.fillMaxSize(),188                )189            }190        }191    }192}193194/**195 * Apply [style] to the current selection of [value], preserving the text and196 * existing spans. If the selection is collapsed, the value is returned unchanged197 * (basic span support — styling needs a non-empty selection).198 */199private fun applySpan(value: TextFieldValue, style: SpanStyle): TextFieldValue {200    val sel = value.selection201    if (sel.collapsed) return value202    val start = sel.min203    val end = sel.max204    val existing = value.annotatedString205    val merged: AnnotatedString = buildAnnotatedString {206        append(existing)207        addStyle(style, start, end)208    }209    return value.copy(annotatedString = merged)210}211212@Composable213private fun FormatButton(214    label: String,215    fontWeight: FontWeight? = null,216    fontStyle: FontStyle? = null,217    underline: Boolean = false,218    onClick: () -> Unit,219) {220    Box(221        Modifier222            .size(30.dp)223            .clip(RoundedCornerShape(6.dp))224            .background(peachColors.S2)225            .clickable(onClick = onClick),226        contentAlignment = Alignment.Center,227    ) {228        Text(229            label,230            color = peachColors.T0,231            style = TextStyle(232                fontFamily = PeachType.Body,233                fontSize = 14.sp,234                fontWeight = fontWeight,235                fontStyle = fontStyle,236                textDecoration = if (underline) TextDecoration.Underline else null,237            ),238        )239    }240}