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.ShellBackground585960616263646566676869@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 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 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 108 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 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 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}193194195196197198199private 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}