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 }343536val LocalFaviconsEnabled = staticCompositionLocalOf { false }373839404142434445@Composable46fun FaviconInitials(47 source: String,48 modifier: Modifier = Modifier,49 size: Dp = 26.dp,50 cornerRadius: Dp = (size.value * 0.23f).dp,51) {52 val favicons = if (LocalFaviconsEnabled.current) LocalFavicons.current else null53 val domain = remember(source) { extractDomain(source) }54 var revision by remember { mutableIntStateOf(0) }55 LaunchedEffect(favicons) {56 favicons?.revision?.collect { revision = it }57 }58 val sizePx = with(LocalDensity.current) { size.roundToPx() }59 val bitmap = remember(domain, revision, sizePx) {60 if (domain != null && favicons != null) favicons.bitmap(domain, sizePx) else null61 }62 if (bitmap != null) {63 Image(64 bitmap = bitmap.asImageBitmap(),65 contentDescription = null,66 contentScale = ContentScale.Crop,67 modifier = modifier68 .size(size)69 .clip(RoundedCornerShape(cornerRadius)),70 )71 return72 }73 val bg = titleBackground(source)74 Box(75 modifier = modifier76 .size(size)77 .clip(RoundedCornerShape(cornerRadius))78 .drawBehind { drawRect(bg) },79 contentAlignment = Alignment.Center,80 ) {81 Text(82 text = titleInitials(source),83 color = peachColors.T0.copy(alpha = 0.92f),84 fontFamily = PeachType.Mono,85 fontWeight = FontWeight.SemiBold,86 letterSpacing = 0.04.em,87 fontSize = (size.value * 0.34f).sp,88 )89 }90}9192fun titleInitials(source: String): String {93 val cleaned = source.replace(Regex("^https?://"), "").removePrefix("www.").trim()94 if (cleaned.isEmpty()) return "·"95 val words = cleaned.split(Regex("[\\s.\\-_/]+")).filter { it.isNotEmpty() }96 if (words.size >= 2 && words[0].isNotEmpty() && words[1].isNotEmpty()) {97 return (words[0][0].toString() + words[1][0].toString()).uppercase()98 }99 val single = words.firstOrNull() ?: cleaned100 return single.take(2).uppercase()101}102103104fun titleBackground(source: String): Color {105 var hash = 0106 for (ch in source) hash = ((hash shl 5) - hash + ch.code)107 val hue = (((hash % 360) + 360) % 360).toFloat()108 return hsl(hue, 0.32f, 0.26f)109}110111private fun hsl(h: Float, s: Float, l: Float): Color {112 val c = (1f - kotlin.math.abs(2 * l - 1f)) * s113 val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f))114 val m = l - c / 2f115 val (r, g, b) = when {116 h < 60 -> Triple(c, x, 0f)117 h < 120 -> Triple(x, c, 0f)118 h < 180 -> Triple(0f, c, x)119 h < 240 -> Triple(0f, x, c)120 h < 300 -> Triple(x, 0f, c)121 else -> Triple(c, 0f, x)122 }123 return Color(r + m, g + m, b + m)124}