1package com.peachpasswords.ui.components23import androidx.compose.foundation.Image4import androidx.compose.foundation.layout.Box5import androidx.compose.foundation.layout.size6import androidx.compose.foundation.shape.RoundedCornerShape7import androidx.compose.material3.Text8import androidx.compose.runtime.Composable9import androidx.compose.runtime.LaunchedEffect10import androidx.compose.runtime.getValue11import androidx.compose.runtime.mutableIntStateOf12import androidx.compose.runtime.remember13import androidx.compose.runtime.setValue14import androidx.compose.runtime.staticCompositionLocalOf15import androidx.compose.ui.Alignment16import androidx.compose.ui.Modifier17import androidx.compose.ui.draw.clip18import androidx.compose.ui.graphics.Color19import androidx.compose.ui.draw.drawBehind20import androidx.compose.ui.graphics.asImageBitmap21import androidx.compose.ui.layout.ContentScale22import androidx.compose.ui.platform.LocalDensity23import androidx.compose.ui.text.font.FontWeight24import androidx.compose.ui.unit.Dp25import androidx.compose.ui.unit.dp26import androidx.compose.ui.unit.em27import androidx.compose.ui.unit.sp28import com.peachpasswords.shared.extractDomain29import com.peachpasswords.ui.theme.peachColors30import com.peachpasswords.ui.theme.PeachType313233val LocalFavicons = staticCompositionLocalOf<Favicons?> { null }343536373839404142@Composable43fun FaviconInitials(44 source: String,45 modifier: Modifier = Modifier,46 size: Dp = 26.dp,47 cornerRadius: Dp = (size.value * 0.23f).dp,48) {49 val favicons = LocalFavicons.current50 val domain = remember(source) { extractDomain(source) }51 var revision by remember { mutableIntStateOf(0) }52 LaunchedEffect(favicons) {53 favicons?.revision?.collect { revision = it }54 }55 val sizePx = with(LocalDensity.current) { size.roundToPx() }56 val bitmap = remember(domain, revision, sizePx) {57 if (domain != null && favicons != null) favicons.bitmap(domain, sizePx) else null58 }59 if (bitmap != null) {60 Image(61 bitmap = bitmap.asImageBitmap(),62 contentDescription = null,63 contentScale = ContentScale.Crop,64 modifier = modifier65 .size(size)66 .clip(RoundedCornerShape(cornerRadius)),67 )68 return69 }70 val bg = titleBackground(source)71 Box(72 modifier = modifier73 .size(size)74 .clip(RoundedCornerShape(cornerRadius))75 .drawBehind { drawRect(bg) },76 contentAlignment = Alignment.Center,77 ) {78 Text(79 text = titleInitials(source),80 color = peachColors.T0.copy(alpha = 0.92f),81 fontFamily = PeachType.Mono,82 fontWeight = FontWeight.SemiBold,83 letterSpacing = 0.04.em,84 fontSize = (size.value * 0.34f).sp,85 )86 }87}8889fun titleInitials(source: String): String {90 val cleaned = source.replace(Regex("^https?://"), "").removePrefix("www.").trim()91 if (cleaned.isEmpty()) return "·"92 val words = cleaned.split(Regex("[\\s.\\-_/]+")).filter { it.isNotEmpty() }93 if (words.size >= 2 && words[0].isNotEmpty() && words[1].isNotEmpty()) {94 return (words[0][0].toString() + words[1][0].toString()).uppercase()95 }96 val single = words.firstOrNull() ?: cleaned97 return single.take(2).uppercase()98}99100101fun titleBackground(source: String): Color {102 var hash = 0103 for (ch in source) hash = ((hash shl 5) - hash + ch.code)104 val hue = (((hash % 360) + 360) % 360).toFloat()105 return hsl(hue, 0.32f, 0.26f)106}107108private fun hsl(h: Float, s: Float, l: Float): Color {109 val c = (1f - kotlin.math.abs(2 * l - 1f)) * s110 val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f))111 val m = l - c / 2f112 val (r, g, b) = when {113 h < 60 -> Triple(c, x, 0f)114 h < 120 -> Triple(x, c, 0f)115 h < 180 -> Triple(0f, c, x)116 h < 240 -> Triple(0f, x, c)117 h < 300 -> Triple(x, 0f, c)118 else -> Triple(c, 0f, x)119 }120 return Color(r + m, g + m, b + m)121}