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 Spacer(Modifier.width(6.dp))190 CopyButton(copied, onCopy)191 } else {192 Column(Modifier.weight(1f)) {193 Row(verticalAlignment = Alignment.CenterVertically) {194 UnderlinedField(195 draft = draft,196 onDraftChange = { draft = it },197 transformation = if (show) VisualTransformation.None else PasswordVisualTransformation(),198 singleLine = true,199 modifier = Modifier.weight(1f),200 )201 Text(202 if (show) "hide" else "show", color = accent, style = PeachType.labelMono,203 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { show = !show }.padding(6.dp),204 )205 Spacer(Modifier.width(6.dp))206 207 208 209 WrenchButton(onClick = onGenerate)210 }211 Spacer(Modifier.height(8.dp))212 SaveCancelRow(213 onSave = { onSave(draft); editing = false },214 onCancel = { draft = value; editing = false },215 )216 }217 }218 }219}220221222@Composable223private fun RevealButton(revealed: Boolean, onClick: () -> Unit) {224 Box(225 Modifier226 .size(36.dp)227 .clip(RoundedCornerShape(6.dp))228 .background(peachColors.S2)229 .clickable(onClick = onClick),230 contentAlignment = Alignment.Center,231 ) {232 Icon(233 if (revealed) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,234 contentDescription = if (revealed) "hide password" else "reveal password",235 tint = peachColors.T2,236 modifier = Modifier.size(15.dp),237 )238 }239}240241242@Composable243private fun InlineEditor(244 draft: String,245 onDraftChange: (String) -> Unit,246 onSave: () -> Unit,247 onCancel: () -> Unit,248 suggestions: List<IdentitySuggestion>,249 onPickSuggestion: (String) -> Unit,250 singleLine: Boolean,251 modifier: Modifier = Modifier,252) {253 Column(modifier) {254 UnderlinedField(255 draft = draft,256 onDraftChange = onDraftChange,257 singleLine = singleLine,258 modifier = Modifier.fillMaxWidth(),259 )260 if (suggestions.isNotEmpty()) {261 Spacer(Modifier.height(6.dp))262 LazyColumn(263 modifier = Modifier264 .fillMaxWidth()265 .heightIn(max = 236.dp)266 .clip(RoundedCornerShape(8.dp))267 .background(peachColors.SurfaceCard),268 ) {269 items(suggestions, key = { it.value }) { suggestion ->270 Row(271 Modifier272 .fillMaxWidth()273 .clickable { onPickSuggestion(suggestion.value) }274 .padding(horizontal = 12.dp, vertical = 9.dp),275 verticalAlignment = Alignment.CenterVertically,276 ) {277 Text(278 suggestion.value,279 color = peachColors.T1,280 style = PeachType.bodyMono,281 modifier = Modifier.weight(1f),282 )283 Text(284 "${suggestion.count}×",285 color = peachColors.T2,286 style = PeachType.labelMono,287 )288 }289 }290 }291 }292 Spacer(Modifier.height(8.dp))293 SaveCancelRow(onSave = onSave, onCancel = onCancel)294 }295}296297298@Composable299private fun UnderlinedField(300 draft: String,301 onDraftChange: (String) -> Unit,302 transformation: VisualTransformation = VisualTransformation.None,303 singleLine: Boolean = true,304 modifier: Modifier = Modifier,305) {306 val accent = LocalPeachAccent.current307 BasicTextField(308 value = draft,309 onValueChange = onDraftChange,310 singleLine = singleLine,311 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),312 cursorBrush = SolidColor(accent),313 visualTransformation = transformation,314 modifier = modifier315 .padding(bottom = 4.dp)316 .drawBehind {317 drawLine(318 color = accent,319 start = Offset(0f, size.height),320 end = Offset(size.width, size.height),321 strokeWidth = 1.dp.toPx(),322 )323 },324 )325}326327@Composable328private fun SaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {329 val accent = LocalPeachAccent.current330 331 332 333 334 Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {335 Box(336 Modifier337 .size(36.dp)338 .clip(RoundedCornerShape(6.dp))339 .background(peachColors.S2)340 .clickable(onClick = onSave),341 contentAlignment = Alignment.Center,342 ) {343 Icon(Icons.Rounded.Check, contentDescription = "save", tint = accent, modifier = Modifier.size(16.dp))344 }345 Box(346 Modifier347 .size(36.dp)348 .clip(RoundedCornerShape(6.dp))349 .background(peachColors.S2)350 .clickable(onClick = onCancel),351 contentAlignment = Alignment.Center,352 ) {353 Icon(Icons.Rounded.Close, contentDescription = "cancel", tint = peachColors.T2, modifier = Modifier.size(16.dp))354 }355 }356}