1package com.peachpasswords.ui.vault23import android.content.ActivityNotFoundException4import android.content.Context5import android.content.Intent6import android.net.Uri7import android.widget.Toast8import androidx.compose.animation.core.LinearEasing9import androidx.compose.animation.core.animateFloatAsState10import androidx.compose.animation.core.tween11import androidx.compose.foundation.background12import androidx.compose.foundation.clickable13import androidx.compose.foundation.layout.Arrangement14import androidx.compose.foundation.layout.Box15import androidx.compose.foundation.layout.Column16import androidx.compose.foundation.layout.Row17import androidx.compose.foundation.layout.Spacer18import androidx.compose.foundation.layout.fillMaxWidth19import androidx.compose.foundation.layout.height20import androidx.compose.foundation.layout.padding21import androidx.compose.foundation.layout.size22import androidx.compose.foundation.layout.width23import androidx.compose.foundation.shape.RoundedCornerShape24import androidx.compose.foundation.text.BasicTextField25import androidx.compose.material.icons.Icons26import androidx.compose.material.icons.automirrored.rounded.OpenInNew27import androidx.compose.material.icons.rounded.Check28import androidx.compose.material.icons.rounded.ContentCopy29import androidx.compose.material3.Icon30import androidx.compose.material3.Text31import androidx.compose.runtime.Composable32import androidx.compose.runtime.getValue33import androidx.compose.runtime.mutableStateOf34import androidx.compose.ui.Alignment35import androidx.compose.ui.Modifier36import androidx.compose.ui.draw.clip37import androidx.compose.ui.draw.drawBehind38import androidx.compose.ui.geometry.Offset39import androidx.compose.ui.geometry.Size40import androidx.compose.ui.graphics.Brush41import androidx.compose.ui.graphics.Color42import androidx.compose.ui.graphics.SolidColor43import androidx.compose.ui.platform.LocalContext44import androidx.compose.ui.text.TextStyle45import androidx.compose.ui.unit.dp46import androidx.compose.ui.unit.em47import androidx.compose.ui.unit.sp48import androidx.compose.runtime.remember49import androidx.compose.runtime.setValue50import com.peachpasswords.ui.theme.LocalPeachAccent51import com.peachpasswords.ui.theme.LocalReduceMotion52import com.peachpasswords.ui.theme.peachColors53import com.peachpasswords.ui.theme.PeachType5455565758596061626364@Composable65fun AdditionalUrlRows(66 urls: List<String>,67 copiedField: String?,68 onCopy: (key: String, value: String) -> Unit,69 onSave: (index: Int, value: String) -> Unit = { _, _ -> },70 onAdd: (value: String) -> Unit = {},71) {72 val context = LocalContext.current73 urls.forEachIndexed { index, url ->74 if (url.isNotBlank()) {75 val key = additionalUrlCopyKey(index)76 var editing by remember(url) { mutableStateOf(false) }77 var draft by remember(url) { mutableStateOf(url) }78 AdditionalUrlFieldShell(label = "URL ${index + 2}") {79 if (editing) {80 Column(Modifier.weight(1f)) {81 AdditionalUrlInlineField(82 draft = draft,83 onDraftChange = { draft = it },84 modifier = Modifier.fillMaxWidth(),85 )86 Spacer(Modifier.height(8.dp))87 AdditionalUrlSaveCancelRow(88 onSave = { onSave(index, draft); editing = false },89 onCancel = { draft = url; editing = false },90 )91 }92 } else {93 Text(94 url,95 color = peachColors.T0,96 style = PeachType.bodyMono,97 modifier = Modifier.weight(1f).clickable { draft = url; editing = true },98 )99 OpenInBrowserButton(onClick = { openUrlInBrowser(context, url) })100 Spacer(Modifier.width(4.dp))101 AdditionalUrlCopyButton(copied = copiedField == key, onCopy = { onCopy(key, url) })102 }103 }104 }105 }106 AddAdditionalUrlRow(onAdd = onAdd)107}108109@Composable110private fun AdditionalUrlInlineField(111 draft: String,112 onDraftChange: (String) -> Unit,113 modifier: Modifier = Modifier,114) {115 val accent = LocalPeachAccent.current116 BasicTextField(117 value = draft,118 onValueChange = onDraftChange,119 singleLine = true,120 textStyle = PeachType.bodyMono.copy(color = peachColors.T0),121 cursorBrush = SolidColor(accent),122 modifier = modifier123 .padding(bottom = 4.dp)124 .drawBehind {125 drawLine(126 color = accent,127 start = Offset(0f, size.height),128 end = Offset(size.width, size.height),129 strokeWidth = 1.dp.toPx(),130 )131 },132 )133}134135@Composable136private fun AdditionalUrlSaveCancelRow(onSave: () -> Unit, onCancel: () -> Unit) {137 val accent = LocalPeachAccent.current138 Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {139 Text(140 "✓ save", color = accent, style = PeachType.labelMono,141 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onSave).padding(vertical = 4.dp),142 )143 Text(144 "✗", color = peachColors.T2, style = PeachType.labelMono,145 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onCancel).padding(horizontal = 6.dp, vertical = 4.dp),146 )147 }148}149150@Composable151private fun AddAdditionalUrlRow(onAdd: (String) -> Unit) {152 val accent = LocalPeachAccent.current153 var editing by remember { mutableStateOf(false) }154 var draft by remember { mutableStateOf("") }155 if (!editing) {156 Text(157 "+ add another url",158 color = accent,159 style = PeachType.labelMono,160 modifier = Modifier161 .clip(RoundedCornerShape(6.dp))162 .clickable { editing = true }163 .padding(vertical = 6.dp),164 )165 return166 }167 AdditionalUrlFieldShell(label = "new url") {168 Column(Modifier.weight(1f)) {169 AdditionalUrlInlineField(170 draft = draft,171 onDraftChange = { draft = it },172 modifier = Modifier.fillMaxWidth(),173 )174 Spacer(Modifier.height(8.dp))175 AdditionalUrlSaveCancelRow(176 onSave = {177 onAdd(draft)178 draft = ""179 editing = false180 },181 onCancel = {182 draft = ""183 editing = false184 },185 )186 }187 }188}189190191fun additionalUrlCopyKey(index: Int): String = "additionalUrl:$index"192193194195196197198fun openUrlInBrowser(context: Context, raw: String) {199 val normalized = if (raw.matches(Regex("^[a-zA-Z][a-zA-Z0-9+.-]*://.*"))) raw else "https://$raw"200 try {201 context.startActivity(202 Intent(Intent.ACTION_VIEW, Uri.parse(normalized)).apply {203 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)204 },205 )206 } catch (_: ActivityNotFoundException) {207 Toast.makeText(context, "No app can open this link", Toast.LENGTH_SHORT).show()208 }209}210211212@Composable213private fun AdditionalUrlFieldShell(214 label: String,215 content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit,216) {217 val hairline = peachColors.T3218 Column(219 Modifier.fillMaxWidth().padding(vertical = 10.dp).drawBehind {220 drawRect(221 color = hairline,222 topLeft = Offset(0f, size.height - 1.dp.toPx()),223 size = Size(size.width, 1.dp.toPx()),224 )225 },226 ) {227 Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))228 Spacer(Modifier.height(6.dp))229 Row(verticalAlignment = Alignment.CenterVertically, content = content)230 }231}232233@Composable234private fun OpenInBrowserButton(onClick: () -> Unit) {235 Box(236 Modifier237 .size(36.dp)238 .clip(RoundedCornerShape(6.dp))239 .background(peachColors.S2)240 .clickable(onClick = onClick),241 contentAlignment = Alignment.Center,242 ) {243 Icon(244 Icons.AutoMirrored.Rounded.OpenInNew,245 contentDescription = "open in browser",246 tint = peachColors.T2,247 modifier = Modifier.size(15.dp),248 )249 }250}251252253@Composable254private fun AdditionalUrlCopyButton(copied: Boolean, onCopy: () -> Unit) {255 val accent = LocalPeachAccent.current256 val reduceMotion = LocalReduceMotion.current257 val animate = copied && !reduceMotion258 val drain by animateFloatAsState(259 targetValue = if (copied) 1f else 0f,260 animationSpec = if (animate) tween(1400, easing = LinearEasing) else tween(0),261 label = "url-copy-drain",262 )263 val sheen by animateFloatAsState(264 targetValue = if (copied) 1f else 0f,265 animationSpec = if (animate) tween(600, easing = LinearEasing) else tween(0),266 label = "url-copy-sheen",267 )268 Box(269 Modifier270 .size(36.dp)271 .clip(RoundedCornerShape(6.dp))272 .background(peachColors.S2)273 .clickable(onClick = onCopy)274 .drawBehind {275 if (animate && sheen < 1f) {276 val sweepX = (sheen * 2f - 0.5f) * size.width277 val band = size.width * 0.5f278 drawRect(279 brush = Brush.linearGradient(280 colors = listOf(Color.Transparent, accent.copy(alpha = 0.35f), Color.Transparent),281 start = Offset(sweepX - band, 0f),282 end = Offset(sweepX + band, size.height),283 ),284 )285 }286 if (drain > 0f) {287 val h = 2.dp.toPx()288 drawRect(289 color = accent,290 topLeft = Offset(0f, size.height - h),291 size = Size(size.width * drain, h),292 )293 }294 },295 contentAlignment = Alignment.Center,296 ) {297 Icon(298 if (copied) Icons.Rounded.Check else Icons.Rounded.ContentCopy,299 contentDescription = "copy",300 tint = if (copied) accent else peachColors.T1,301 modifier = Modifier.size(15.dp),302 )303 }304}