package com.peachpasswords.ui.components import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.peachpasswords.shared.extractDomain import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.ui.theme.PeachType /** Favicons repository for this window; provided by MainActivity and the picker. */ val LocalFavicons = staticCompositionLocalOf { null } /** * Site favicon with initials fallback (offline-first). Shows the cached real * favicon (U1, [Favicons]) once fetched; until then the initials chip — the * browser's titleInitials + titleBackground port (deterministic HSL 32%/26%). * The corner radius tracks the chip size (≈6dp at 26dp, ≈3dp at 16dp) per * spec §4.6. */ @Composable fun FaviconInitials( source: String, modifier: Modifier = Modifier, size: Dp = 26.dp, cornerRadius: Dp = (size.value * 0.23f).dp, ) { val favicons = LocalFavicons.current val domain = remember(source) { extractDomain(source) } var revision by remember { mutableIntStateOf(0) } LaunchedEffect(favicons) { favicons?.revision?.collect { revision = it } } val sizePx = with(LocalDensity.current) { size.roundToPx() } val bitmap = remember(domain, revision, sizePx) { if (domain != null && favicons != null) favicons.bitmap(domain, sizePx) else null } if (bitmap != null) { Image( bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = ContentScale.Crop, modifier = modifier .size(size) .clip(RoundedCornerShape(cornerRadius)), ) return } val bg = titleBackground(source) Box( modifier = modifier .size(size) .clip(RoundedCornerShape(cornerRadius)) .drawBehind { drawRect(bg) }, contentAlignment = Alignment.Center, ) { Text( text = titleInitials(source), color = peachColors.T0.copy(alpha = 0.92f), fontFamily = PeachType.Mono, fontWeight = FontWeight.SemiBold, letterSpacing = 0.04.em, fontSize = (size.value * 0.34f).sp, ) } } fun titleInitials(source: String): String { val cleaned = source.replace(Regex("^https?://"), "").removePrefix("www.").trim() if (cleaned.isEmpty()) return "·" val words = cleaned.split(Regex("[\\s.\\-_/]+")).filter { it.isNotEmpty() } if (words.size >= 2 && words[0].isNotEmpty() && words[1].isNotEmpty()) { return (words[0][0].toString() + words[1][0].toString()).uppercase() } val single = words.firstOrNull() ?: cleaned return single.take(2).uppercase() } /** Deterministic muted HSL(hue, 32%, 26%) background, matching the browser. */ fun titleBackground(source: String): Color { var hash = 0 for (ch in source) hash = ((hash shl 5) - hash + ch.code) val hue = (((hash % 360) + 360) % 360).toFloat() return hsl(hue, 0.32f, 0.26f) } private fun hsl(h: Float, s: Float, l: Float): Color { val c = (1f - kotlin.math.abs(2 * l - 1f)) * s val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f)) val m = l - c / 2f val (r, g, b) = when { h < 60 -> Triple(c, x, 0f) h < 120 -> Triple(x, c, 0f) h < 180 -> Triple(0f, c, x) h < 240 -> Triple(0f, x, c) h < 300 -> Triple(x, 0f, c) else -> Triple(c, 0f, x) } return Color(r + m, g + m, b + m) }