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) {102 val editable = draftText ?: value103 var editing by remember(value) { mutableStateOf(false) }104 var draft by remember(value) { mutableStateOf(editable) }105106 FieldShell(label) {107 if (!editing) {108 val isEmpty = value.isBlank()109 Text(110 if (isEmpty) AnnotatedString(placeholder) else displayText ?: AnnotatedString(value),111 color = if (isEmpty) peachColors.T2 else peachColors.T0,112 style = if (mono) PeachType.bodyMono else PeachType.body,113 modifier = Modifier114 .weight(1f)115 .clickable { draft = editable; editing = true },116 )117 if (!isEmpty) CopyButton(copied, onCopy)118 } else {119 InlineEditor(120 draft = draft,121 onDraftChange = { draft = it },122 onSave = { onSave(draft.trim()); editing = false },123 onCancel = { draft = editable; editing = false },124 suggestions = suggestions,125 onPickSuggestion = { draft = it },126 singleLine = singleLine,127 modifier = Modifier.weight(1f),128 )129 }130 }131}132133134135136137138139140141@Composable142fun EditablePasswordRow(143 value: String,144 copied: Boolean,145 entryTitle: String,146 onCopy: () -> Unit,147 onSave: (String) -> Unit,148 onGenerate: () -> Unit,149) {150 val accent = LocalPeachAccent.current151 val context = LocalContext.current152 var editing by remember(value) { mutableStateOf(false) }153 var draft by remember(value) { mutableStateOf(value) }154 var show by remember { mutableStateOf(false) }155 var reveal by remember(value) { mutableStateOf(false) }156157 fun beginEdit() {158 val activity = context as? FragmentActivity159 if (activity != null && Biometrics.isAvailable(context)) {160 Biometrics.authenticate(161 activity,162 title = "Unlock Peach",163 subtitle = entryTitle,164 onSuccess = { draft = value; show = false; editing = true },165 )166 } else {167 168 draft = value; show = false; editing = true169 }170 }171172 FieldShell("password") {173 if (!editing) {174 175 176 177 Text(178 text = if (reveal) value else "•".repeat(value.length.coerceIn(8, 16)),179 color = peachColors.T0,180 style = PeachType.bodyMono,181 modifier = Modifier.weight(1f).clickable { beginEdit() },182 )183 RevealButton(revealed = reveal, onClick = { reveal = !reveal })184 CopyButton(copied, onCopy)185 } else {186 Column(Modifier.weight(1f)) {187 Row(verticalAlignment = Alignment.CenterVertically) {188 UnderlinedField(189 draft = draft,190 onDraftChange = { draft = it },191 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),192 singleLine = true,193 modifier = Modifier.weight(1f),194 )195 Text(196 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,197 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),198 )199 200 201 202 WrenchButton(onClick = onGenerate)203 }204 Spacer(Modifier.height(8.dp))205 SaveCancelRow(206 onSave = { onSave(draft); editing = false },207 onCancel = { draft = value; editing = false },208 )209 }210 }211 }212}213214215@Composable216private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {217 Box(218 Modifier219 .size(36.dp)220 .clip(RoundedCornerShape(6.dp))221 .background(peachColors.S2)222 .clickable(onClick = onClick),223 contentAlignment = Alignment.Center,224 ) {225 Icon(226 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,227 contentDescription = if (revealed) "hide password" else "reveal password",228 tint = peachColors.T2,229 modifier = Modifier.size(15.dp),230 )231 }232}233234235@Composable236private fun InlineEditor(237 draft: String,238 onDraftChange: (String) -> Unit,239 onSave: () -> Unit,240 onCancel: () -> Unit,241 suggestions: List<String>,242 onPickSuggestion: (String) -> Unit,243 singleLine: Boolean,244 modifier: Modifier = Modifier,245) {246 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }247248 Column(modifier) {249 Box {250 UnderlinedField(251 draft = draft,252 onDraftChange = onDraftChange,253 singleLine = singleLine,254 modifier = Modifier.fillMaxWidth(),255 )256 if (suggestions.isNotEmpty()) {257 DropdownMenu(258 expanded = dropdownOpen,259 onDismissRequest = { dropdownOpen = false },260 ) {261 suggestions.take(4).forEach { suggestion ->262 DropdownMenuItem(263 text = {264 Text(265 suggestion,266 color = peachColors.T1,267 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),268 )269 },270 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },271 )272 }273 }274 }275 }276 Spacer(Modifier.height(8.dp))277 SaveCancelRow(onSave = onSave, onCancel = onCancel)278 }279}280281282@Composable283private fun UnderlinedField(284 draft: String,285 onDraftChange: (String) -> Unit,286 transformation: VisualTransformation = VisualTransformation.None,287 singleLine: Boolean = true,288 modifier: Modifier = Modifier,289) {290 val accent = LocalPeachAccent.current291 BasicTextField(292 value = draft,293 onValueChange = onDraftChange,294 singleLine = singleLine,295 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),296 cursorBrush = SolidColor(accent),297 visualTransformation = transformation,298 modifier = modifier299 .padding(bottom = 4.dp)300 .drawBehind {301 drawLine(302 color = accent,303 start = Offset(0f, size.height),304 end = Offset(size.width, size.height),305 strokeWidth = 1.dp.toPx(),306 )307 },308 )309}310311@Composable312private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {313 val accent = LocalPeachAccent.current314 315 316 317 318 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {319 Box(320 Modifier321 .size(36.dp)322 .clip(RoundedCornerShape(6.dp))323 .background(peachColors.S2)324 .clickable(onClick = onSave),325 contentAlignment = Alignment.Center,326 ) {327 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))328 }329 Box(330 Modifier331 .size(36.dp)332 .clip(RoundedCornerShape(6.dp))333 .background(peachColors.S2)334 .clickable(onClick = onCancel),335 contentAlignment = Alignment.Center,336 ) {337 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))338 }339 }340}