package com.radiola.ui.components import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.BlurredEdgeTreatment import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.composables.icons.lucide.Heart import com.composables.icons.lucide.Lucide import com.radiola.domain.model.Station import com.radiola.domain.model.Track import com.radiola.ui.theme.Motion import com.radiola.ui.theme.RadiolaTheme import com.radiola.ui.theme.pressScale @Composable fun StationCard( station: Station, isFavorite: Boolean, onClick: () -> Unit, onFavoriteClick: () -> Unit, modifier: Modifier = Modifier, nowTrack: Track? = null, isCurrent: Boolean = false, isPlaying: Boolean = false ) { val colors = RadiolaTheme.colors val haptics = LocalHapticFeedback.current val interaction = remember { MutableInteractionSource() } val heartTint by animateColorAsState( targetValue = if (isFavorite) colors.accent else colors.textPrimary, animationSpec = tween(Motion.Medium), label = "heartTint" ) Column( modifier = modifier .pressScale(interactionSource = interaction) .clickable(interactionSource = interaction, indication = null, onClick = onClick) ) { Box( modifier = Modifier .fillMaxWidth() .aspectRatio(1f), contentAlignment = Alignment.Center ) { // Свечение активной станции — позади обложки, мягко вылезает из-под краёв. if (isCurrent) { PlayingGlow( modifier = Modifier.matchParentSize(), color = colors.accent, playing = isPlaying ) } Box( modifier = Modifier .matchParentSize() .clip(RoundedCornerShape(16.dp)) .background(colors.surface2) ) { val trackCover = nowTrack?.coverUrl?.takeIf { it.isNotBlank() } // Фон карточки: обложка трека → логотип станции → фирменная плитка. when { trackCover != null -> { AsyncImage( model = crossfadeModel(trackCover), contentDescription = nowTrack.song, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) } !station.coverUrl.isNullOrBlank() -> { AsyncImage( model = crossfadeModel(station.coverUrl), contentDescription = station.name, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) } else -> { Box( modifier = Modifier .fillMaxSize() .background(stationTileBrush(station.name)), contentAlignment = Alignment.Center ) { Text( text = stationInitials(station.name), color = Color.White, fontWeight = FontWeight.Black, style = androidx.compose.material3.MaterialTheme.typography.headlineMedium ) } } } // Подпись играющего трека — поверх любого фона, если трек известен. if (nowTrack != null) { Box( modifier = Modifier .fillMaxSize() .background( Brush.verticalGradient( 0f to Color.Transparent, 0.5f to Color.Transparent, 1f to Color.Black.copy(alpha = 0.8f) ) ) ) Column( modifier = Modifier .align(Alignment.BottomStart) .padding(10.dp) ) { Text( text = nowTrack.song, color = Color.White, fontWeight = FontWeight.Bold, style = androidx.compose.material3.MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis ) Text( text = nowTrack.artist, color = Color.White.copy(alpha = 0.8f), style = androidx.compose.material3.MaterialTheme.typography.labelMedium, maxLines = 1, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis ) } } // Бейдж активной станции: эквалайзер в углу обложки. if (isCurrent) { Box( modifier = Modifier .align(Alignment.TopStart) .padding(10.dp) .clip(RoundedCornerShape(8.dp)) .background(Color.Black.copy(alpha = 0.45f)) .padding(horizontal = 7.dp, vertical = 6.dp), contentAlignment = Alignment.Center ) { com.radiola.ui.theme.LiveEqualizer( modifier = Modifier.size(width = 16.dp, height = 12.dp), barCount = 4, color = colors.accent, playing = isPlaying ) } } // Кнопка сердечка — поверх всего, top-end. Box( modifier = Modifier .align(Alignment.TopEnd) .padding(10.dp) .size(32.dp) .clip(RoundedCornerShape(16.dp)) .background(Color.Black.copy(alpha = 0.4f)) .clickable { haptics.performHapticFeedback(HapticFeedbackType.LongPress) onFavoriteClick() }, contentAlignment = Alignment.Center ) { Icon( imageVector = Lucide.Heart, contentDescription = if (isFavorite) "В избранном" else "В избранное", tint = heartTint, modifier = Modifier.size(17.dp) ) } } } Spacer(Modifier.height(10.dp)) Text( text = station.name, style = androidx.compose.material3.MaterialTheme.typography.titleMedium, color = colors.textPrimary, maxLines = 1, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis ) if (station.genre.isNotBlank()) { Text( text = station.genre, style = androidx.compose.material3.MaterialTheme.typography.labelMedium, color = colors.textSecondary, maxLines = 1, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, fontWeight = FontWeight.Normal ) } } } /** * Мягкое радиальное свечение играющей станции — рисуется ПОЗАДИ обложки и * вылезает из-под её краёв. Центр градиента «гуляет», размер дышит. */ @Composable private fun PlayingGlow( modifier: Modifier = Modifier, color: Color, playing: Boolean ) { val transition = rememberInfiniteTransition(label = "glow") val t by transition.animateFloat( initialValue = 0f, targetValue = (2f * Math.PI).toFloat(), animationSpec = infiniteRepeatable( animation = tween(4200, easing = LinearEasing), repeatMode = RepeatMode.Restart ), label = "glowT" ) val pulse by transition.animateFloat( initialValue = if (playing) 1.05f else 1.0f, targetValue = if (playing) 1.20f else 1.07f, animationSpec = infiniteRepeatable( animation = tween(2200, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "glowPulse" ) val cx = 0.5f + 0.22f * kotlin.math.cos(t) val cy = 0.5f + 0.22f * kotlin.math.sin(t * 1.3f) Box( modifier = modifier .scale(pulse) .blur(28.dp, BlurredEdgeTreatment.Unbounded) .drawBehind { val brush = Brush.radialGradient( colors = listOf( color.copy(alpha = 0.85f), color.copy(alpha = 0.35f), Color.Transparent ), center = Offset(size.width * cx, size.height * cy), radius = size.minDimension * 0.72f ) drawRoundRect( brush = brush, cornerRadius = CornerRadius(22.dp.toPx(), 22.dp.toPx()) ) } ) } /** Инициалы станции для плитки-плейсхолдера (1–2 символа). */ private fun stationInitials(name: String): String { val words = name.trim().split(Regex("\\s+")).filter { it.isNotBlank() } return when { words.isEmpty() -> "?" words.size == 1 -> words[0].take(2).uppercase() else -> (words[0].take(1) + words[1].take(1)).uppercase() } } /** Детерминированный фирменный градиент плитки по названию станции. */ private fun stationTileBrush(name: String): Brush { val h = (name.hashCode().toLong() and 0xFFFFFFFFL) val hue = (h % 360L).toFloat() val c1 = Color.hsv(hue, 0.55f, 0.45f) val c2 = Color.hsv((hue + 28f) % 360f, 0.6f, 0.30f) return Brush.linearGradient(listOf(c1, c2)) }