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 Text(184 "edit", color = accent, style = PeachType.labelMono,185 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),186 )187 RevealButton(revealed = reveal, onClick = { reveal = !reveal })188 CopyButton(copied, onCopy)189 } else {190 Column(Modifier.weight(1f)) {191 Row(verticalAlignment = Alignment.CenterVertically) {192 UnderlinedField(193 draft = draft,194 onDraftChange = { draft = it },195 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),196 singleLine = true,197 modifier = Modifier.weight(1f),198 )199 Text(200 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,201 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),202 )203 204 205 206 WrenchButton(onClick = onGenerate)207 }208 Spacer(Modifier.height(8.dp))209 SaveCancelRow(210 onSave = { onSave(draft); editing = false },211 onCancel = { draft = value; editing = false },212 )213 }214 }215 }216}217218219@Composable220private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {221 Box(222 Modifier223 .size(36.dp)224 .clip(RoundedCornerShape(6.dp))225 .background(peachColors.S2)226 .clickable(onClick = onClick),227 contentAlignment = Alignment.Center,228 ) {229 Icon(230 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,231 contentDescription = if (revealed) "hide password" else "reveal password",232 tint = peachColors.T2,233 modifier = Modifier.size(15.dp),234 )235 }236}237238239@Composable240private fun InlineEditor(241 draft: String,242 onDraftChange: (String) -> Unit,243 onSave: () -> Unit,244 onCancel: () -> Unit,245 suggestions: List<String>,246 onPickSuggestion: (String) -> Unit,247 singleLine: Boolean,248 modifier: Modifier = Modifier,249) {250 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }251252 Column(modifier) {253 Box {254 UnderlinedField(255 draft = draft,256 onDraftChange = onDraftChange,257 singleLine = singleLine,258 modifier = Modifier.fillMaxWidth(),259 )260 if (suggestions.isNotEmpty()) {261 DropdownMenu(262 expanded = dropdownOpen,263 onDismissRequest = { dropdownOpen = false },264 ) {265 suggestions.take(4).forEach { suggestion ->266 DropdownMenuItem(267 text = {268 Text(269 suggestion,270 color = peachColors.T1,271 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),272 )273 },274 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },275 )276 }277 }278 }279 }280 Spacer(Modifier.height(8.dp))281 SaveCancelRow(onSave = onSave, onCancel = onCancel)282 }283}284285286@Composable287private fun UnderlinedField(288 draft: String,289 onDraftChange: (String) -> Unit,290 transformation: VisualTransformation = VisualTransformation.None,291 singleLine: Boolean = true,292 modifier: Modifier = Modifier,293) {294 val accent = LocalPeachAccent.current295 BasicTextField(296 value = draft,297 onValueChange = onDraftChange,298 singleLine = singleLine,299 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),300 cursorBrush = SolidColor(accent),301 visualTransformation = transformation,302 modifier = modifier303 .padding(bottom = 4.dp)304 .drawBehind {305 drawLine(306 color = accent,307 start = Offset(0f, size.height),308 end = Offset(size.width, size.height),309 strokeWidth = 1.dp.toPx(),310 )311 },312 )313}314315@Composable316private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {317 val accent = LocalPeachAccent.current318 319 320 321 322 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {323 Box(324 Modifier325 .size(36.dp)326 .clip(RoundedCornerShape(6.dp))327 .background(peachColors.S2)328 .clickable(onClick = onSave),329 contentAlignment = Alignment.Center,330 ) {331 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))332 }333 Box(334 Modifier335 .size(36.dp)336 .clip(RoundedCornerShape(6.dp))337 .background(peachColors.S2)338 .clickable(onClick = onCancel),339 contentAlignment = Alignment.Center,340 ) {341 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))342 }343 }344}