1package com.peachpasswords.ui.vault234567891011121314151617181920212223import androidx.compose.foundation.clickable24import androidx.compose.foundation.layout.Arrangement25import androidx.compose.foundation.layout.Box26import androidx.compose.foundation.layout.Column27import androidx.compose.foundation.layout.Row28import androidx.compose.foundation.layout.Spacer29import androidx.compose.foundation.layout.fillMaxWidth30import androidx.compose.foundation.layout.height31import androidx.compose.foundation.layout.padding32import androidx.compose.foundation.layout.width33import androidx.compose.foundation.shape.RoundedCornerShape34import androidx.compose.foundation.text.BasicTextField35import androidx.compose.material3.DropdownMenu36import androidx.compose.material3.DropdownMenuItem37import androidx.compose.material3.Text38import androidx.compose.runtime.Composable39import androidx.compose.runtime.getValue40import androidx.compose.runtime.mutableStateOf41import androidx.compose.runtime.remember42import androidx.compose.runtime.setValue43import androidx.compose.ui.Alignment44import androidx.compose.ui.Modifier45import androidx.compose.ui.draw.clip46import androidx.compose.ui.draw.drawBehind47import androidx.compose.ui.geometry.Offset48import androidx.compose.ui.graphics.SolidColor49import androidx.compose.ui.platform.LocalContext50import androidx.compose.ui.text.input.PasswordVisualTransformation51import androidx.compose.ui.text.input.VisualTransformation52import androidx.compose.ui.unit.dp53import androidx.compose.ui.unit.em54import androidx.fragment.app.FragmentActivity55import com.peachpasswords.ui.components.CopyButton56import com.peachpasswords.ui.lock.Biometrics57import com.peachpasswords.ui.theme.LocalPeachAccent58import com.peachpasswords.ui.theme.peachColors59import com.peachpasswords.ui.theme.PeachType606162636465666768@param69@param7071@param72@param7374@Composable75fun EditableFieldRow(76 label: String,77 field: String,78 value: String,79 copied: Boolean,80 onCopy: () -> Unit,81 onSave: (String) -> Unit,82 suggestions: List<String> = emptyList(),83 mono: Boolean = true,84 singleLine: Boolean = true,85 placeholder: String = "",86) {87 var editing by remember(value) { mutableStateOf(false) }88 var draft by remember(value) { mutableStateOf(value) }8990 FieldShell(label) {91 if (!editing) {92 val isEmpty = value.isBlank()93 Text(94 if (isEmpty) placeholder else value,95 color = if (isEmpty) peachColors.T2 else peachColors.T0,96 style = if (mono) PeachType.bodyMono else PeachType.body,97 modifier = Modifier98 .weight(1f)99 .clickable { draft = value; editing = true },100 )101 if (!isEmpty) CopyButton(copied, onCopy)102 } else {103 InlineEditor(104 draft = draft,105 onDraftChange = { draft = it },106 onSave = { onSave(draft.trim()); editing = false },107 onCancel = { draft = value; editing = false },108 suggestions = suggestions,109 onPickSuggestion = { draft = it },110 singleLine = singleLine,111 modifier = Modifier.weight(1f),112 )113 }114 }115}116117118119120121122123124125@Composable126fun EditablePasswordRow(127 value: String,128 copied: Boolean,129 entryTitle: String,130 onCopy: () -> Unit,131 onSave: (String) -> Unit,132 onGenerate: () -> Unit,133) {134 val accent = LocalPeachAccent.current135 val context = LocalContext.current136 var editing by remember(value) { mutableStateOf(false) }137 var draft by remember(value) { mutableStateOf(value) }138 var show by remember { mutableStateOf(false) }139140 fun beginEdit() {141 val activity = context as? FragmentActivity142 if (activity != null && Biometrics.isAvailable(context)) {143 Biometrics.authenticate(144 activity,145 title = "Unlock Peach",146 subtitle = entryTitle,147 onSuccess = { draft = value; show = false; editing = true },148 )149 } else {150 151 draft = value; show = false; editing = true152 }153 }154155 FieldShell("password") {156 if (!editing) {157 Text(158 text = "•".repeat(value.length.coerceIn(8, 16)),159 color = peachColors.T0,160 style = PeachType.bodyMono,161 modifier = Modifier.weight(1f).clickable { beginEdit() },162 )163 Text(164 "edit", color = accent, style = PeachType.labelMono,165 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),166 )167 CopyButton(copied, onCopy)168 WrenchButton(onClick = onGenerate)169 } else {170 Column(Modifier.weight(1f)) {171 Row(verticalAlignment = Alignment.CenterVertically) {172 UnderlinedField(173 draft = draft,174 onDraftChange = { draft = it },175 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),176 singleLine = true,177 modifier = Modifier.weight(1f),178 )179 Text(180 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,181 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),182 )183 }184 Spacer(Modifier.height(8.dp))185 SaveCancelRow(186 onSave = { onSave(draft); editing = false },187 onCancel = { draft = value; editing = false },188 )189 }190 }191 }192}193194195@Composable196private fun InlineEditor(197 draft: String,198 onDraftChange: (String) -> Unit,199 onSave: () -> Unit,200 onCancel: () -> Unit,201 suggestions: List<String>,202 onPickSuggestion: (String) -> Unit,203 singleLine: Boolean,204 modifier: Modifier = Modifier,205) {206 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }207208 Column(modifier) {209 Box {210 UnderlinedField(211 draft = draft,212 onDraftChange = onDraftChange,213 singleLine = singleLine,214 modifier = Modifier.fillMaxWidth(),215 )216 if (suggestions.isNotEmpty()) {217 DropdownMenu(218 expanded = dropdownOpen,219 onDismissRequest = { dropdownOpen = false },220 ) {221 suggestions.take(4).forEach { suggestion ->222 DropdownMenuItem(223 text = {224 Text(225 suggestion,226 color = peachColors.T1,227 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),228 )229 },230 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },231 )232 }233 }234 }235 }236 Spacer(Modifier.height(8.dp))237 SaveCancelRow(onSave = onSave, onCancel = onCancel)238 }239}240241242@Composable243private fun UnderlinedField(244 draft: String,245 onDraftChange: (String) -> Unit,246 transformation: VisualTransformation = VisualTransformation.None,247 singleLine: Boolean = true,248 modifier: Modifier = Modifier,249) {250 val accent = LocalPeachAccent.current251 BasicTextField(252 value = draft,253 onValueChange = onDraftChange,254 singleLine = singleLine,255 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),256 cursorBrush = SolidColor(accent),257 visualTransformation = transformation,258 modifier = modifier259 .padding(bottom = 4.dp)260 .drawBehind {261 drawLine(262 color = accent,263 start = Offset(0f, size.height),264 end = Offset(size.width, size.height),265 strokeWidth = 1.dp.toPx(),266 )267 },268 )269}270271@Composable272private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {273 val accent = LocalPeachAccent.current274 Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {275 Text(276 "✓ save", color = accent, style = PeachType.labelMono,277 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onSave).padding(vertical = 4.dp),278 )279 Text(280 "✗", color = peachColors.T2, style = PeachType.labelMono,281 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onCancel).padding(horizontal = 6.dp, vertical = 4.dp),282 )283 Spacer(Modifier.width(1.dp))284 }285}