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.input.PasswordVisualTransformation59import androidx.compose.ui.text.input.VisualTransformation60import androidx.compose.ui.unit.dp61import androidx.compose.ui.unit.em62import androidx.fragment.app.FragmentActivity63import com.peachpasswords.ui.components.CopyButton64import com.peachpasswords.ui.lock.Biometrics65import com.peachpasswords.ui.theme.LocalPeachAccent66import com.peachpasswords.ui.theme.peachColors67import com.peachpasswords.ui.theme.PeachType686970717273747576@param77@param7879@param80@param8182@Composable83fun EditableFieldRow(84 label: String,85 field: String,86 value: String,87 copied: Boolean,88 onCopy: () -> Unit,89 onSave: (String) -> Unit,90 suggestions: List<String> = emptyList(),91 mono: Boolean = true,92 singleLine: Boolean = true,93 placeholder: String = "",94) {95 var editing by remember(value) { mutableStateOf(false) }96 var draft by remember(value) { mutableStateOf(value) }9798 FieldShell(label) {99 if (!editing) {100 val isEmpty = value.isBlank()101 Text(102 if (isEmpty) placeholder else value,103 color = if (isEmpty) peachColors.T2 else peachColors.T0,104 style = if (mono) PeachType.bodyMono else PeachType.body,105 modifier = Modifier106 .weight(1f)107 .clickable { draft = value; editing = true },108 )109 if (!isEmpty) CopyButton(copied, onCopy)110 } else {111 InlineEditor(112 draft = draft,113 onDraftChange = { draft = it },114 onSave = { onSave(draft.trim()); editing = false },115 onCancel = { draft = value; editing = false },116 suggestions = suggestions,117 onPickSuggestion = { draft = it },118 singleLine = singleLine,119 modifier = Modifier.weight(1f),120 )121 }122 }123}124125126127128129130131132133@Composable134fun EditablePasswordRow(135 value: String,136 copied: Boolean,137 entryTitle: String,138 onCopy: () -> Unit,139 onSave: (String) -> Unit,140 onGenerate: () -> Unit,141) {142 val accent = LocalPeachAccent.current143 val context = LocalContext.current144 var editing by remember(value) { mutableStateOf(false) }145 var draft by remember(value) { mutableStateOf(value) }146 var show by remember { mutableStateOf(false) }147 var reveal by remember(value) { mutableStateOf(false) }148149 fun beginEdit() {150 val activity = context as? FragmentActivity151 if (activity != null && Biometrics.isAvailable(context)) {152 Biometrics.authenticate(153 activity,154 title = "Unlock Peach",155 subtitle = entryTitle,156 onSuccess = { draft = value; show = false; editing = true },157 )158 } else {159 160 draft = value; show = false; editing = true161 }162 }163164 FieldShell("password") {165 if (!editing) {166 167 168 169 Text(170 text = if (reveal) value else "•".repeat(value.length.coerceIn(8, 16)),171 color = peachColors.T0,172 style = PeachType.bodyMono,173 modifier = Modifier.weight(1f).clickable { beginEdit() },174 )175 Text(176 "edit", color = accent, style = PeachType.labelMono,177 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),178 )179 RevealButton(revealed = reveal, onClick = { reveal = !reveal })180 CopyButton(copied, onCopy)181 } else {182 Column(Modifier.weight(1f)) {183 Row(verticalAlignment = Alignment.CenterVertically) {184 UnderlinedField(185 draft = draft,186 onDraftChange = { draft = it },187 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),188 singleLine = true,189 modifier = Modifier.weight(1f),190 )191 Text(192 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,193 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),194 )195 196 197 198 WrenchButton(onClick = onGenerate)199 }200 Spacer(Modifier.height(8.dp))201 SaveCancelRow(202 onSave = { onSave(draft); editing = false },203 onCancel = { draft = value; editing = false },204 )205 }206 }207 }208}209210211@Composable212private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {213 Box(214 Modifier215 .size(36.dp)216 .clip(RoundedCornerShape(6.dp))217 .background(peachColors.S2)218 .clickable(onClick = onClick),219 contentAlignment = Alignment.Center,220 ) {221 Icon(222 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,223 contentDescription = if (revealed) "hide password" else "reveal password",224 tint = peachColors.T2,225 modifier = Modifier.size(15.dp),226 )227 }228}229230231@Composable232private fun InlineEditor(233 draft: String,234 onDraftChange: (String) -> Unit,235 onSave: () -> Unit,236 onCancel: () -> Unit,237 suggestions: List<String>,238 onPickSuggestion: (String) -> Unit,239 singleLine: Boolean,240 modifier: Modifier = Modifier,241) {242 var dropdownOpen by remember { mutableStateOf(suggestions.isNotEmpty()) }243244 Column(modifier) {245 Box {246 UnderlinedField(247 draft = draft,248 onDraftChange = onDraftChange,249 singleLine = singleLine,250 modifier = Modifier.fillMaxWidth(),251 )252 if (suggestions.isNotEmpty()) {253 DropdownMenu(254 expanded = dropdownOpen,255 onDismissRequest = { dropdownOpen = false },256 ) {257 suggestions.take(4).forEach { suggestion ->258 DropdownMenuItem(259 text = {260 Text(261 suggestion,262 color = peachColors.T1,263 style = PeachType.labelMono.copy(letterSpacing = 0.06.em),264 )265 },266 onClick = { onPickSuggestion(suggestion); dropdownOpen = false },267 )268 }269 }270 }271 }272 Spacer(Modifier.height(8.dp))273 SaveCancelRow(onSave = onSave, onCancel = onCancel)274 }275}276277278@Composable279private fun UnderlinedField(280 draft: String,281 onDraftChange: (String) -> Unit,282 transformation: VisualTransformation = VisualTransformation.None,283 singleLine: Boolean = true,284 modifier: Modifier = Modifier,285) {286 val accent = LocalPeachAccent.current287 BasicTextField(288 value = draft,289 onValueChange = onDraftChange,290 singleLine = singleLine,291 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),292 cursorBrush = SolidColor(accent),293 visualTransformation = transformation,294 modifier = modifier295 .padding(bottom = 4.dp)296 .drawBehind {297 drawLine(298 color = accent,299 start = Offset(0f, size.height),300 end = Offset(size.width, size.height),301 strokeWidth = 1.dp.toPx(),302 )303 },304 )305}306307@Composable308private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {309 val accent = LocalPeachAccent.current310 311 312 313 314 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {315 Box(316 Modifier317 .size(36.dp)318 .clip(RoundedCornerShape(6.dp))319 .background(peachColors.S2)320 .clickable(onClick = onSave),321 contentAlignment = Alignment.Center,322 ) {323 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))324 }325 Box(326 Modifier327 .size(36.dp)328 .clip(RoundedCornerShape(6.dp))329 .background(peachColors.S2)330 .clickable(onClick = onCancel),331 contentAlignment = Alignment.Center,332 ) {333 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))334 }335 }336}