1package com.peachpasswords.ui.vault234567891011121314151617181920212223import androidx.compose.foundation.background24import androidx.compose.foundation.clickable25import androidx.compose.foundation.layout.Arrangement26import androidx.compose.foundation.layout.Box27import androidx.compose.foundation.layout.Column28import androidx.compose.foundation.layout.Row29import androidx.compose.foundation.layout.Spacer30import androidx.compose.foundation.layout.fillMaxWidth31import androidx.compose.foundation.layout.height32import androidx.compose.foundation.layout.padding33import androidx.compose.foundation.layout.size34import androidx.compose.foundation.layout.width35import androidx.compose.foundation.shape.RoundedCornerShape36import androidx.compose.foundation.text.BasicTextField37import androidx.compose.material.icons.Icons38import androidx.compose.material.icons.rounded.Check39import androidx.compose.material.icons.outlined.Visibility40import androidx.compose.material.icons.outlined.VisibilityOff41import androidx.compose.material.icons.rounded.Close42import androidx.compose.material3.DropdownMenu43import androidx.compose.material3.DropdownMenuItem44import androidx.compose.material3.Icon45import androidx.compose.material3.Text46import androidx.compose.runtime.Composable47import androidx.compose.runtime.getValue48import androidx.compose.runtime.mutableStateOf49import androidx.compose.runtime.remember50import androidx.compose.runtime.setValue51import androidx.compose.ui.Alignment52import androidx.compose.ui.Modifier53import androidx.compose.ui.draw.clip54import androidx.compose.ui.draw.drawBehind55import androidx.compose.ui.geometry.Offset56import androidx.compose.ui.graphics.SolidColor57import androidx.compose.ui.platform.LocalContext58import androidx.compose.ui.text.AnnotatedString59import androidx.compose.ui.text.input.PasswordVisualTransformation60import androidx.compose.ui.text.input.VisualTransformation61import androidx.compose.ui.unit.dp62import androidx.compose.ui.unit.em63import androidx.fragment.app.FragmentActivity64import com.peachpasswords.ui.components.CopyButton65import com.peachpasswords.ui.lock.Biometrics66import com.peachpasswords.ui.theme.LocalPeachAccent67import com.peachpasswords.ui.theme.peachColors68import com.peachpasswords.ui.theme.PeachType697071727374757677@param78@param7980@param81@param82@param8384@param858687@Composable88fun EditableFieldRow(89 label: String,90 field: String,91 value: String,92 copied: Boolean,93 onCopy: () -> Unit,94 onSave: (String) -> Unit,95 suggestions: List<String> = emptyList(),96 mono: Boolean = true,97 singleLine: Boolean = true,98 placeholder: String = "",99 displayText: AnnotatedString? = null,100 draftText: String? = null,101 onEditingChange: (Boolean) -> Unit = {},102 stacked: Boolean = false,103 divider: Boolean = false,104) {105 val editable = draftText ?: value106 var editing by remember(value) { mutableStateOf(false) }107 var draft by remember(value) { mutableStateOf(editable) }108109 FieldShell(label, stacked = stacked, divider = divider) {110 if (!editing) {111 val isEmpty = value.isBlank()112 Text(113 if (isEmpty) AnnotatedString(placeholder) else displayText ?: AnnotatedString(value),114 color = if (isEmpty) peachColors.T2 else peachColors.T0,115 style = if (mono) PeachType.bodyMono else PeachType.body,116 modifier = Modifier117 .weight(1f)118 .clickable { draft = editable; editing = true; onEditingChange(true) },119 )120 if (!isEmpty) CopyButton(copied, onCopy)121 } else {122 InlineEditor(123 draft = draft,124 onDraftChange = { draft = it },125 onSave = { onSave(draft.trim()); editing = false; onEditingChange(false) },126 onCancel = { draft = editable; editing = false; onEditingChange(false) },127 suggestions = suggestions,128 onPickSuggestion = { draft = it },129 singleLine = singleLine,130 modifier = Modifier.weight(1f),131 )132 }133 }134}135136137138139140141142143144@Composable145fun EditablePasswordRow(146 value: String,147 copied: Boolean,148 entryTitle: String,149 onCopy: () -> Unit,150 onSave: (String) -> Unit,151 onGenerate: () -> Unit,152 stacked: Boolean = false,153 divider: Boolean = false,154) {155 val accent = LocalPeachAccent.current156 val context = LocalContext.current157 var editing by remember(value) { mutableStateOf(false) }158 var draft by remember(value) { mutableStateOf(value) }159 var show by remember { mutableStateOf(false) }160 var reveal by remember(value) { mutableStateOf(false) }161162 fun beginEdit() {163 val activity = context as? FragmentActivity164 if (activity != null && Biometrics.isAvailable(context)) {165 Biometrics.authenticate(166 activity,167 title = "Unlock Peach",168 subtitle = entryTitle,169 onSuccess = { draft = value; show = false; editing = true },170 )171 } else {172 173 draft = value; show = false; editing = true174 }175 }176177 FieldShell("password", stacked = stacked, divider = divider) {178 if (!editing) {179 180 181 182 Text(183 text = if (reveal) value else "•".repeat(value.length.coerceIn(8, 16)),184 color = peachColors.T0,185 style = PeachType.bodyMono,186 modifier = Modifier.weight(1f).clickable { beginEdit() },187 )188 RevealButton(revealed = reveal, onClick = { reveal = !reveal })189 CopyButton(copied, onCopy)190 } else {191 Column(Modifier.weight(1f)) {192 Row(verticalAlignment = Alignment.CenterVertically) {193 UnderlinedField(194 draft = draft,195 onDraftChange = { draft = it },196 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),197 singleLine = true,198 modifier = Modifier.weight(1f),199 )200 Text(201 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,202 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),203 )204 205 206 207 WrenchButton(onClick = onGenerate)208 }209 Spacer(Modifier.height(8.dp))210 SaveCancelRow(211 onSave = { onSave(draft); editing = false },212 onCancel = { draft = value; editing = false },213 )214 }215 }216 }217}218219220@Composable221private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {222 Box(223 Modifier224 .size(36.dp)225 .clip(RoundedCornerShape(6.dp))226 .background(peachColors.S2)227 .clickable(onClick = onClick),228 contentAlignment = Alignment.Center,229 ) {230 Icon(231 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,232 contentDescription = if (revealed) "hide password" else "reveal password",233 tint = peachColors.T2,234 modifier = Modifier.size(15.dp),235 )236 }237}238239240@Composable241private fun InlineEditor(242 draft: String,243 onDraftChange: (String) -> Unit,244 onSave: () -> Unit,245 onCancel: () -> Unit,246 suggestions: List<String>,247 onPickSuggestion: (String) -> Unit,248 singleLine: Boolean,249 modifier: Modifier = Modifier,250) {251 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }252253 Column(modifier) {254 Box {255 UnderlinedField(256 draft = draft,257 onDraftChange = onDraftChange,258 singleLine = singleLine,259 modifier = Modifier.fillMaxWidth(),260 )261 if (suggestions.isNotEmpty()) {262 DropdownMenu(263 expanded = dropdownOpen,264 onDismissRequest = { dropdownOpen = false },265 ) {266 suggestions.take(4).forEach { suggestion ->267 DropdownMenuItem(268 text = {269 Text(270 suggestion,271 color = peachColors.T1,272 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),273 )274 },275 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },276 )277 }278 }279 }280 }281 Spacer(Modifier.height(8.dp))282 SaveCancelRow(onSave = onSave, onCancel = onCancel)283 }284}285286287@Composable288private fun UnderlinedField(289 draft: String,290 onDraftChange: (String) -> Unit,291 transformation: VisualTransformation = VisualTransformation.None,292 singleLine: Boolean = true,293 modifier: Modifier = Modifier,294) {295 val accent = LocalPeachAccent.current296 BasicTextField(297 value = draft,298 onValueChange = onDraftChange,299 singleLine = singleLine,300 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),301 cursorBrush = SolidColor(accent),302 visualTransformation = transformation,303 modifier = modifier304 .padding(bottom = 4.dp)305 .drawBehind {306 drawLine(307 color = accent,308 start = Offset(0f, size.height),309 end = Offset(size.width, size.height),310 strokeWidth = 1.dp.toPx(),311 )312 },313 )314}315316@Composable317private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {318 val accent = LocalPeachAccent.current319 320 321 322 323 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {324 Box(325 Modifier326 .size(36.dp)327 .clip(RoundedCornerShape(6.dp))328 .background(peachColors.S2)329 .clickable(onClick = onSave),330 contentAlignment = Alignment.Center,331 ) {332 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))333 }334 Box(335 Modifier336 .size(36.dp)337 .clip(RoundedCornerShape(6.dp))338 .background(peachColors.S2)339 .clickable(onClick = onCancel),340 contentAlignment = Alignment.Center,341 ) {342 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))343 }344 }345}