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) {103 val editable = draftText ?: value104 var editing by remember(value) { mutableStateOf(false) }105 var draft by remember(value) { mutableStateOf(editable) }106107 FieldShell(label) {108 if (!editing) {109 val isEmpty = value.isBlank()110 Text(111 if (isEmpty) AnnotatedString(placeholder) else displayText ?: AnnotatedString(value),112 color = if (isEmpty) peachColors.T2 else peachColors.T0,113 style = if (mono) PeachType.bodyMono else PeachType.body,114 modifier = Modifier115 .weight(1f)116 .clickable { draft = editable; editing = true; onEditingChange(true) },117 )118 if (!isEmpty) CopyButton(copied, onCopy)119 } else {120 InlineEditor(121 draft = draft,122 onDraftChange = { draft = it },123 onSave = { onSave(draft.trim()); editing = false; onEditingChange(false) },124 onCancel = { draft = editable; editing = false; onEditingChange(false) },125 suggestions = suggestions,126 onPickSuggestion = { draft = it },127 singleLine = singleLine,128 modifier = Modifier.weight(1f),129 )130 }131 }132}133134135136137138139140141142@Composable143fun EditablePasswordRow(144 value: String,145 copied: Boolean,146 entryTitle: String,147 onCopy: () -> Unit,148 onSave: (String) -> Unit,149 onGenerate: () -> Unit,150) {151 val accent = LocalPeachAccent.current152 val context = LocalContext.current153 var editing by remember(value) { mutableStateOf(false) }154 var draft by remember(value) { mutableStateOf(value) }155 var show by remember { mutableStateOf(false) }156 var reveal by remember(value) { mutableStateOf(false) }157158 fun beginEdit() {159 val activity = context as? FragmentActivity160 if (activity != null && Biometrics.isAvailable(context)) {161 Biometrics.authenticate(162 activity,163 title = "Unlock Peach",164 subtitle = entryTitle,165 onSuccess = { draft = value; show = false; editing = true },166 )167 } else {168 169 draft = value; show = false; editing = true170 }171 }172173 FieldShell("password") {174 if (!editing) {175 176 177 178 Text(179 text = if (reveal) value else "•".repeat(value.length.coerceIn(8, 16)),180 color = peachColors.T0,181 style = PeachType.bodyMono,182 modifier = Modifier.weight(1f).clickable { beginEdit() },183 )184 RevealButton(revealed = reveal, onClick = { reveal = !reveal })185 CopyButton(copied, onCopy)186 } else {187 Column(Modifier.weight(1f)) {188 Row(verticalAlignment = Alignment.CenterVertically) {189 UnderlinedField(190 draft = draft,191 onDraftChange = { draft = it },192 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),193 singleLine = true,194 modifier = Modifier.weight(1f),195 )196 Text(197 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,198 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),199 )200 201 202 203 WrenchButton(onClick = onGenerate)204 }205 Spacer(Modifier.height(8.dp))206 SaveCancelRow(207 onSave = { onSave(draft); editing = false },208 onCancel = { draft = value; editing = false },209 )210 }211 }212 }213}214215216@Composable217private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {218 Box(219 Modifier220 .size(36.dp)221 .clip(RoundedCornerShape(6.dp))222 .background(peachColors.S2)223 .clickable(onClick = onClick),224 contentAlignment = Alignment.Center,225 ) {226 Icon(227 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,228 contentDescription = if (revealed) "hide password" else "reveal password",229 tint = peachColors.T2,230 modifier = Modifier.size(15.dp),231 )232 }233}234235236@Composable237private fun InlineEditor(238 draft: String,239 onDraftChange: (String) -> Unit,240 onSave: () -> Unit,241 onCancel: () -> Unit,242 suggestions: List<String>,243 onPickSuggestion: (String) -> Unit,244 singleLine: Boolean,245 modifier: Modifier = Modifier,246) {247 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }248249 Column(modifier) {250 Box {251 UnderlinedField(252 draft = draft,253 onDraftChange = onDraftChange,254 singleLine = singleLine,255 modifier = Modifier.fillMaxWidth(),256 )257 if (suggestions.isNotEmpty()) {258 DropdownMenu(259 expanded = dropdownOpen,260 onDismissRequest = { dropdownOpen = false },261 ) {262 suggestions.take(4).forEach { suggestion ->263 DropdownMenuItem(264 text = {265 Text(266 suggestion,267 color = peachColors.T1,268 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),269 )270 },271 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },272 )273 }274 }275 }276 }277 Spacer(Modifier.height(8.dp))278 SaveCancelRow(onSave = onSave, onCancel = onCancel)279 }280}281282283@Composable284private fun UnderlinedField(285 draft: String,286 onDraftChange: (String) -> Unit,287 transformation: VisualTransformation = VisualTransformation.None,288 singleLine: Boolean = true,289 modifier: Modifier = Modifier,290) {291 val accent = LocalPeachAccent.current292 BasicTextField(293 value = draft,294 onValueChange = onDraftChange,295 singleLine = singleLine,296 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),297 cursorBrush = SolidColor(accent),298 visualTransformation = transformation,299 modifier = modifier300 .padding(bottom = 4.dp)301 .drawBehind {302 drawLine(303 color = accent,304 start = Offset(0f, size.height),305 end = Offset(size.width, size.height),306 strokeWidth = 1.dp.toPx(),307 )308 },309 )310}311312@Composable313private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {314 val accent = LocalPeachAccent.current315 316 317 318 319 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {320 Box(321 Modifier322 .size(36.dp)323 .clip(RoundedCornerShape(6.dp))324 .background(peachColors.S2)325 .clickable(onClick = onSave),326 contentAlignment = Alignment.Center,327 ) {328 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))329 }330 Box(331 Modifier332 .size(36.dp)333 .clip(RoundedCornerShape(6.dp))334 .background(peachColors.S2)335 .clickable(onClick = onCancel),336 contentAlignment = Alignment.Center,337 ) {338 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))339 }340 }341}