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.heightIn33import androidx.compose.foundation.layout.padding34import androidx.compose.foundation.layout.size35import androidx.compose.foundation.layout.width36import androidx.compose.foundation.lazy.LazyColumn37import androidx.compose.foundation.lazy.items38import androidx.compose.foundation.shape.RoundedCornerShape39import androidx.compose.foundation.text.BasicTextField40import androidx.compose.material.icons.Icons41import androidx.compose.material.icons.rounded.Check42import androidx.compose.material.icons.outlined.Visibility43import androidx.compose.material.icons.outlined.VisibilityOff44import androidx.compose.material.icons.rounded.Close45import androidx.compose.material3.Icon46import androidx.compose.material3.Text47import androidx.compose.runtime.Composable48import androidx.compose.runtime.getValue49import androidx.compose.runtime.mutableStateOf50import androidx.compose.runtime.remember51import androidx.compose.runtime.setValue52import androidx.compose.ui.Alignment53import androidx.compose.ui.Modifier54import androidx.compose.ui.draw.clip55import androidx.compose.ui.draw.drawBehind56import androidx.compose.ui.geometry.Offset57import androidx.compose.ui.graphics.SolidColor58import androidx.compose.ui.platform.LocalContext59import androidx.compose.ui.text.AnnotatedString60import androidx.compose.ui.text.input.PasswordVisualTransformation61import androidx.compose.ui.text.input.VisualTransformation62import androidx.compose.ui.unit.dp63import 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<IdentitySuggestion> = 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 stacked: Boolean = false,103 divider: Boolean = false,104) {105 val editable = draftText ?: value106 var editing by remember(value) { mutableStateOf(false) }107 var draft by remember(value) { mutableStateOf(editable) }108109 FieldShell(label, stacked = stacked, divider = divider) {110 if (!editing) {111 val isEmpty = value.isBlank()112 Text(113 if (isEmpty) AnnotatedString(placeholder) else displayText ?: AnnotatedString(value),114 color = if (isEmpty) peachColors.T2 else peachColors.T0,115 style = if (mono) PeachType.bodyMono else PeachType.body,116 modifier = Modifier117 .weight(1f)118 .clickable { draft = editable; editing = true; onEditingChange(true) },119 )120 if (!isEmpty) CopyButton(copied, onCopy)121 } else {122 InlineEditor(123 draft = draft,124 onDraftChange = { draft = it },125 onSave = { onSave(draft.trim()); editing = false; onEditingChange(false) },126 onCancel = { draft = editable; editing = false; onEditingChange(false) },127 suggestions = suggestions,128 onPickSuggestion = { draft = it },129 singleLine = singleLine,130 modifier = Modifier.weight(1f),131 )132 }133 }134}135136137138139140141142143144@Composable145fun EditablePasswordRow(146 value: String,147 copied: Boolean,148 entryTitle: String,149 onCopy: () -> Unit,150 onSave: (String) -> Unit,151 onGenerate: () -> Unit,152 stacked: Boolean = false,153 divider: Boolean = false,154) {155 val accent = LocalPeachAccent.current156 val context = LocalContext.current157 var editing by remember(value) { mutableStateOf(false) }158 var draft by remember(value) { mutableStateOf(value) }159 var show by remember { mutableStateOf(false) }160 var reveal by remember(value) { mutableStateOf(false) }161162 fun beginEdit() {163 val activity = context as? FragmentActivity164 if (activity != null && Biometrics.isAvailable(context)) {165 Biometrics.authenticate(166 activity,167 title = "Unlock Peach",168 subtitle = entryTitle,169 onSuccess = { draft = value; show = false; editing = true },170 )171 } else {172 173 draft = value; show = false; editing = true174 }175 }176177 FieldShell("password", stacked = stacked, divider = divider) {178 if (!editing) {179 180 181 182 Text(183 text = if (reveal) value else "•".repeat(value.length.coerceIn(8, 16)),184 color = peachColors.T0,185 style = PeachType.bodyMono,186 modifier = Modifier.weight(1f).clickable { beginEdit() },187 )188 RevealButton(revealed = reveal, onClick = { reveal = !reveal })189 CopyButton(copied, onCopy)190 } else {191 Column(Modifier.weight(1f)) {192 Row(verticalAlignment = Alignment.CenterVertically) {193 UnderlinedField(194 draft = draft,195 onDraftChange = { draft = it },196 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),197 singleLine = true,198 modifier = Modifier.weight(1f),199 )200 Text(201 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,202 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),203 )204 205 206 207 WrenchButton(onClick = onGenerate)208 }209 Spacer(Modifier.height(8.dp))210 SaveCancelRow(211 onSave = { onSave(draft); editing = false },212 onCancel = { draft = value; editing = false },213 )214 }215 }216 }217}218219220@Composable221private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {222 Box(223 Modifier224 .size(36.dp)225 .clip(RoundedCornerShape(6.dp))226 .background(peachColors.S2)227 .clickable(onClick = onClick),228 contentAlignment = Alignment.Center,229 ) {230 Icon(231 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,232 contentDescription = if (revealed) "hide password" else "reveal password",233 tint = peachColors.T2,234 modifier = Modifier.size(15.dp),235 )236 }237}238239240@Composable241private fun InlineEditor(242 draft: String,243 onDraftChange: (String) -> Unit,244 onSave: () -> Unit,245 onCancel: () -> Unit,246 suggestions: List<IdentitySuggestion>,247 onPickSuggestion: (String) -> Unit,248 singleLine: Boolean,249 modifier: Modifier = Modifier,250) {251 Column(modifier) {252 UnderlinedField(253 draft = draft,254 onDraftChange = onDraftChange,255 singleLine = singleLine,256 modifier = Modifier.fillMaxWidth(),257 )258 if (suggestions.isNotEmpty()) {259 Spacer(Modifier.height(6.dp))260 LazyColumn(261 modifier = Modifier262 .fillMaxWidth()263 .heightIn(max = 236.dp)264 .clip(RoundedCornerShape(8.dp))265 .background(peachColors.SurfaceCard),266 ) {267 items(suggestions, key = { it.value }) { suggestion ->268 Row(269 Modifier270 .fillMaxWidth()271 .clickable { onPickSuggestion(suggestion.value) }272 .padding(horizontal = 12.dp, vertical = 9.dp),273 verticalAlignment = Alignment.CenterVertically,274 ) {275 Text(276 suggestion.value,277 color = peachColors.T1,278 style = PeachType.bodyMono,279 modifier = Modifier.weight(1f),280 )281 Text(282 "${suggestion.count}×",283 color = peachColors.T2,284 style = PeachType.labelMono,285 )286 }287 }288 }289 }290 Spacer(Modifier.height(8.dp))291 SaveCancelRow(onSave = onSave, onCancel = onCancel)292 }293}294295296@Composable297private fun UnderlinedField(298 draft: String,299 onDraftChange: (String) -> Unit,300 transformation: VisualTransformation = VisualTransformation.None,301 singleLine: Boolean = true,302 modifier: Modifier = Modifier,303) {304 val accent = LocalPeachAccent.current305 BasicTextField(306 value = draft,307 onValueChange = onDraftChange,308 singleLine = singleLine,309 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),310 cursorBrush = SolidColor(accent),311 visualTransformation = transformation,312 modifier = modifier313 .padding(bottom = 4.dp)314 .drawBehind {315 drawLine(316 color = accent,317 start = Offset(0f, size.height),318 end = Offset(size.width, size.height),319 strokeWidth = 1.dp.toPx(),320 )321 },322 )323}324325@Composable326private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {327 val accent = LocalPeachAccent.current328 329 330 331 332 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {333 Box(334 Modifier335 .size(36.dp)336 .clip(RoundedCornerShape(6.dp))337 .background(peachColors.S2)338 .clickable(onClick = onSave),339 contentAlignment = Alignment.Center,340 ) {341 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))342 }343 Box(344 Modifier345 .size(36.dp)346 .clip(RoundedCornerShape(6.dp))347 .background(peachColors.S2)348 .clickable(onClick = onCancel),349 contentAlignment = Alignment.Center,350 ) {351 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))352 }353 }354}