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.rounded.Close40import androidx.compose.material3.DropdownMenu41import androidx.compose.material3.DropdownMenuItem42import androidx.compose.material3.Icon43import androidx.compose.material3.Text44import androidx.compose.runtime.Composable45import androidx.compose.runtime.getValue46import androidx.compose.runtime.mutableStateOf47import androidx.compose.runtime.remember48import androidx.compose.runtime.setValue49import androidx.compose.ui.Alignment50import androidx.compose.ui.Modifier51import androidx.compose.ui.draw.clip52import androidx.compose.ui.draw.drawBehind53import androidx.compose.ui.geometry.Offset54import androidx.compose.ui.graphics.SolidColor55import androidx.compose.ui.platform.LocalContext56import androidx.compose.ui.text.input.PasswordVisualTransformation57import androidx.compose.ui.text.input.VisualTransformation58import androidx.compose.ui.unit.dp59import androidx.compose.ui.unit.em60import androidx.fragment.app.FragmentActivity61import com.peachpasswords.ui.components.CopyButton62import com.peachpasswords.ui.lock.Biometrics63import com.peachpasswords.ui.theme.LocalPeachAccent64import com.peachpasswords.ui.theme.peachColors65import com.peachpasswords.ui.theme.PeachType666768697071727374@param75@param7677@param78@param7980@Composable81fun EditableFieldRow(82 label: String,83 field: String,84 value: String,85 copied: Boolean,86 onCopy: () -> Unit,87 onSave: (String) -> Unit,88 suggestions: List<String> = emptyList(),89 mono: Boolean = true,90 singleLine: Boolean = true,91 placeholder: String = "",92) {93 var editing by remember(value) { mutableStateOf(false) }94 var draft by remember(value) { mutableStateOf(value) }9596 FieldShell(label) {97 if (!editing) {98 val isEmpty = value.isBlank()99 Text(100 if (isEmpty) placeholder else value,101 color = if (isEmpty) peachColors.T2 else peachColors.T0,102 style = if (mono) PeachType.bodyMono else PeachType.body,103 modifier = Modifier104 .weight(1f)105 .clickable { draft = value; editing = true },106 )107 if (!isEmpty) CopyButton(copied, onCopy)108 } else {109 InlineEditor(110 draft = draft,111 onDraftChange = { draft = it },112 onSave = { onSave(draft.trim()); editing = false },113 onCancel = { draft = value; editing = false },114 suggestions = suggestions,115 onPickSuggestion = { draft = it },116 singleLine = singleLine,117 modifier = Modifier.weight(1f),118 )119 }120 }121}122123124125126127128129130131@Composable132fun EditablePasswordRow(133 value: String,134 copied: Boolean,135 entryTitle: String,136 onCopy: () -> Unit,137 onSave: (String) -> Unit,138 onGenerate: () -> Unit,139) {140 val accent = LocalPeachAccent.current141 val context = LocalContext.current142 var editing by remember(value) { mutableStateOf(false) }143 var draft by remember(value) { mutableStateOf(value) }144 var show by remember { mutableStateOf(false) }145146 fun beginEdit() {147 val activity = context as? FragmentActivity148 if (activity != null && Biometrics.isAvailable(context)) {149 Biometrics.authenticate(150 activity,151 title = "Unlock Peach",152 subtitle = entryTitle,153 onSuccess = { draft = value; show = false; editing = true },154 )155 } else {156 157 draft = value; show = false; editing = true158 }159 }160161 FieldShell("password") {162 if (!editing) {163 Text(164 text = "•".repeat(value.length.coerceIn(8, 16)),165 color = peachColors.T0,166 style = PeachType.bodyMono,167 modifier = Modifier.weight(1f).clickable { beginEdit() },168 )169 Text(170 "edit", color = accent, style = PeachType.labelMono,171 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),172 )173 CopyButton(copied, onCopy)174 WrenchButton(onClick = onGenerate)175 } else {176 Column(Modifier.weight(1f)) {177 Row(verticalAlignment = Alignment.CenterVertically) {178 UnderlinedField(179 draft = draft,180 onDraftChange = { draft = it },181 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),182 singleLine = true,183 modifier = Modifier.weight(1f),184 )185 Text(186 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,187 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),188 )189 }190 Spacer(Modifier.height(8.dp))191 SaveCancelRow(192 onSave = { onSave(draft); editing = false },193 onCancel = { draft = value; editing = false },194 )195 }196 }197 }198}199200201@Composable202private fun InlineEditor(203 draft: String,204 onDraftChange: (String) -> Unit,205 onSave: () -> Unit,206 onCancel: () -> Unit,207 suggestions: List<String>,208 onPickSuggestion: (String) -> Unit,209 singleLine: Boolean,210 modifier: Modifier = Modifier,211) {212 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }213214 Column(modifier) {215 Box {216 UnderlinedField(217 draft = draft,218 onDraftChange = onDraftChange,219 singleLine = singleLine,220 modifier = Modifier.fillMaxWidth(),221 )222 if (suggestions.isNotEmpty()) {223 DropdownMenu(224 expanded = dropdownOpen,225 onDismissRequest = { dropdownOpen = false },226 ) {227 suggestions.take(4).forEach { suggestion ->228 DropdownMenuItem(229 text = {230 Text(231 suggestion,232 color = peachColors.T1,233 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),234 )235 },236 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },237 )238 }239 }240 }241 }242 Spacer(Modifier.height(8.dp))243 SaveCancelRow(onSave = onSave, onCancel = onCancel)244 }245}246247248@Composable249private fun UnderlinedField(250 draft: String,251 onDraftChange: (String) -> Unit,252 transformation: VisualTransformation = VisualTransformation.None,253 singleLine: Boolean = true,254 modifier: Modifier = Modifier,255) {256 val accent = LocalPeachAccent.current257 BasicTextField(258 value = draft,259 onValueChange = onDraftChange,260 singleLine = singleLine,261 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),262 cursorBrush = SolidColor(accent),263 visualTransformation = transformation,264 modifier = modifier265 .padding(bottom = 4.dp)266 .drawBehind {267 drawLine(268 color = accent,269 start = Offset(0f, size.height),270 end = Offset(size.width, size.height),271 strokeWidth = 1.dp.toPx(),272 )273 },274 )275}276277@Composable278private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {279 val accent = LocalPeachAccent.current280 281 282 283 284 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {285 Box(286 Modifier287 .size(36.dp)288 .clip(RoundedCornerShape(6.dp))289 .background(peachColors.S2)290 .clickable(onClick = onSave),291 contentAlignment = Alignment.Center,292 ) {293 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))294 }295 Box(296 Modifier297 .size(36.dp)298 .clip(RoundedCornerShape(6.dp))299 .background(peachColors.S2)300 .clickable(onClick = onCancel),301 contentAlignment = Alignment.Center,302 ) {303 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))304 }305 }306}