diff --git a/app/src/main/java/com/radiola/ui/components/EmptyState.kt b/app/src/main/java/com/radiola/ui/components/EmptyState.kt index b7fe9fd..5a614e5 100644 --- a/app/src/main/java/com/radiola/ui/components/EmptyState.kt +++ b/app/src/main/java/com/radiola/ui/components/EmptyState.kt @@ -1,27 +1,49 @@ package com.radiola.ui.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.Radio +import com.radiola.ui.theme.RadiolaTheme @Composable fun EmptyState( message: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + icon: ImageVector = Lucide.Radio ) { + val colors = RadiolaTheme.colors Box( - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center ) { - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - color = Color(0xFF888888) - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(72.dp) + .clip(RoundedCornerShape(24.dp)) + .background(colors.surface2), + contentAlignment = Alignment.Center + ) { + Icon(icon, contentDescription = null, tint = colors.textMuted, modifier = Modifier.size(32.dp)) + } + Spacer(Modifier.height(16.dp)) + Text( + text = message, + style = androidx.compose.material3.MaterialTheme.typography.bodyLarge, + color = colors.textSecondary, + textAlign = TextAlign.Center + ) + } } } diff --git a/app/src/main/java/com/radiola/ui/components/FilterChips.kt b/app/src/main/java/com/radiola/ui/components/FilterChips.kt index f3fa805..eb74f4f 100644 --- a/app/src/main/java/com/radiola/ui/components/FilterChips.kt +++ b/app/src/main/java/com/radiola/ui/components/FilterChips.kt @@ -1,13 +1,28 @@ package com.radiola.ui.components +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.radiola.ui.theme.Motion +import com.radiola.ui.theme.RadiolaTheme @Composable fun FilterChips( @@ -18,22 +33,49 @@ fun FilterChips( ) { LazyRow( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(9.dp), contentPadding = PaddingValues(horizontal = 16.dp) ) { item { - FilterChip( - selected = selectedTag == null, - onClick = { onTagSelected(null) }, - label = { Text("Все") } - ) + Chip(label = "Все", selected = selectedTag == null) { onTagSelected(null) } } items(tags) { tag -> - FilterChip( - selected = selectedTag == tag, - onClick = { onTagSelected(tag) }, - label = { Text(tag) } - ) + Chip(label = tag, selected = selectedTag == tag) { onTagSelected(tag) } } } } + +@Composable +private fun Chip(label: String, selected: Boolean, onClick: () -> Unit) { + val colors = RadiolaTheme.colors + val bg by animateColorAsState( + targetValue = if (selected) colors.accent else colors.surface2, + animationSpec = tween(Motion.Medium), + label = "chipBg" + ) + val fg by animateColorAsState( + targetValue = if (selected) colors.bgBase else colors.textSecondary, + animationSpec = tween(Motion.Medium), + label = "chipFg" + ) + Text( + text = label, + color = fg, + fontWeight = FontWeight.SemiBold, + style = androidx.compose.material3.MaterialTheme.typography.labelLarge, + modifier = Modifier + .clip(RoundedCornerShape(18.dp)) + .background(bg) + .border( + width = if (selected) 0.dp else 1.dp, + color = if (selected) Color.Transparent else colors.border, + shape = RoundedCornerShape(18.dp) + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ) + .padding(horizontal = 16.dp, vertical = 9.dp) + ) +} diff --git a/app/src/main/java/com/radiola/ui/components/MiniPlayer.kt b/app/src/main/java/com/radiola/ui/components/MiniPlayer.kt index 9dd282d..61d4a7d 100644 --- a/app/src/main/java/com/radiola/ui/components/MiniPlayer.kt +++ b/app/src/main/java/com/radiola/ui/components/MiniPlayer.kt @@ -1,24 +1,33 @@ package com.radiola.ui.components +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border 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.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember 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.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Pause import com.composables.icons.lucide.Play +import com.composables.icons.lucide.Radio 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 MiniPlayer( @@ -29,42 +38,76 @@ fun MiniPlayer( onPlayPause: () -> Unit, modifier: Modifier = Modifier ) { + val colors = RadiolaTheme.colors Row( modifier = modifier .fillMaxWidth() - .height(64.dp) - .background(Color(0xFF1E1E1E)) + .padding(horizontal = 16.dp) + .clip(RoundedCornerShape(18.dp)) + .background(colors.elevated) + .border(1.dp, colors.border, RoundedCornerShape(18.dp)) .clickable(onClick = onClick) - .padding(horizontal = 16.dp), + .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { - AsyncImage( - model = track?.coverUrl, - contentDescription = null, + Box( modifier = Modifier .size(48.dp) - .clip(RoundedCornerShape(6.dp)) - ) - Spacer(modifier = Modifier.width(12.dp)) + .clip(RoundedCornerShape(12.dp)) + .background(colors.surface2), + contentAlignment = Alignment.Center + ) { + if (track?.coverUrl != null) { + AsyncImage( + model = track.coverUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + Icon(Lucide.Radio, null, tint = colors.textSecondary, modifier = Modifier.size(20.dp)) + } + } + Spacer(Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = stationName, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1 + text = "СЕЙЧАС ИГРАЕТ", + color = colors.accent, + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp ) + Spacer(Modifier.height(2.dp)) Text( - text = track?.let { "${it.artist} — ${it.song}" } ?: "", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - maxLines = 1 + text = track?.let { "${it.artist} — ${it.song}" } ?: stationName, + style = androidx.compose.material3.MaterialTheme.typography.titleMedium, + color = colors.textPrimary, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis ) } - IconButton(onClick = onPlayPause) { - Icon( - imageVector = if (isPlaying) Lucide.Pause else Lucide.Play, - contentDescription = if (isPlaying) "Pause" else "Play", - tint = Color.White - ) + Spacer(Modifier.width(8.dp)) + Box( + modifier = Modifier + .pressScale() + .size(44.dp) + .clip(RoundedCornerShape(22.dp)) + .background(colors.accent) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onPlayPause + ), + contentAlignment = Alignment.Center + ) { + Crossfade(targetState = isPlaying, animationSpec = tween(Motion.Fast), label = "miniPlay") { playing -> + Icon( + imageVector = if (playing) Lucide.Pause else Lucide.Play, + contentDescription = if (playing) "Пауза" else "Играть", + tint = colors.bgBase, + modifier = Modifier.size(20.dp) + ) + } } } } diff --git a/app/src/main/java/com/radiola/ui/components/SearchBar.kt b/app/src/main/java/com/radiola/ui/components/SearchBar.kt index f038698..d9ba97d 100644 --- a/app/src/main/java/com/radiola/ui/components/SearchBar.kt +++ b/app/src/main/java/com/radiola/ui/components/SearchBar.kt @@ -1,10 +1,8 @@ package com.radiola.ui.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults @@ -14,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Search +import com.radiola.ui.theme.RadiolaTheme @Composable fun SearchBar( @@ -22,22 +21,23 @@ fun SearchBar( placeholder: String = "Поиск станции...", modifier: Modifier = Modifier ) { + val colors = RadiolaTheme.colors TextField( value = query, onValueChange = onQueryChange, - modifier = modifier - .fillMaxWidth() - .background(Color(0xFF2A2A2A), RoundedCornerShape(8.dp)), - placeholder = { Text(placeholder, color = Color(0xFF888888)) }, - leadingIcon = { Icon(Lucide.Search, contentDescription = null, tint = Color(0xFF888888)) }, + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + placeholder = { Text(placeholder, color = colors.textMuted) }, + leadingIcon = { Icon(Lucide.Search, contentDescription = null, tint = colors.textMuted) }, singleLine = true, colors = TextFieldDefaults.colors( - focusedContainerColor = Color(0xFF2A2A2A), - unfocusedContainerColor = Color(0xFF2A2A2A), + focusedContainerColor = colors.surface, + unfocusedContainerColor = colors.surface, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, - focusedTextColor = Color.White, - unfocusedTextColor = Color.White + cursorColor = colors.accent, + focusedTextColor = colors.textPrimary, + unfocusedTextColor = colors.textPrimary ) ) } diff --git a/app/src/main/java/com/radiola/ui/components/StationCard.kt b/app/src/main/java/com/radiola/ui/components/StationCard.kt index 985c2d0..bb68ed0 100644 --- a/app/src/main/java/com/radiola/ui/components/StationCard.kt +++ b/app/src/main/java/com/radiola/ui/components/StationCard.kt @@ -1,27 +1,31 @@ package com.radiola.ui.components +import androidx.compose.animation.animateColorAsState +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.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme 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.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +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.composables.icons.lucide.Radio import com.radiola.domain.model.Station +import com.radiola.ui.theme.Motion +import com.radiola.ui.theme.RadiolaTheme +import com.radiola.ui.theme.pressScale @Composable fun StationCard( @@ -31,61 +35,76 @@ fun StationCard( onFavoriteClick: () -> Unit, modifier: Modifier = Modifier ) { - Card( + val colors = RadiolaTheme.colors + 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 - .aspectRatio(1f) - .clickable(onClick = onClick), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)) + .pressScale(interactionSource = interaction) + .clickable(interactionSource = interaction, indication = null, onClick = onClick) ) { - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - .background( - Brush.linearGradient( - colors = listOf( - Color(0xFF667eea), - Color(0xFF764ba2) - ) - ) - ) - ) { - AsyncImage( - model = station.coverUrl, - contentDescription = station.name, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - Text( - text = station.name, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(12.dp), - maxLines = 1 + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(16.dp)) + .background(colors.surface2) + ) { + if (!station.coverUrl.isNullOrBlank()) { + AsyncImage( + model = station.coverUrl, + contentDescription = station.name, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + Icon( + Lucide.Radio, + contentDescription = null, + tint = colors.textMuted, + modifier = Modifier.align(Alignment.Center).size(34.dp) ) } - IconButton( - onClick = onFavoriteClick, + Box( modifier = Modifier .align(Alignment.TopEnd) - .padding(4.dp) + .padding(10.dp) .size(32.dp) - .background( - color = Color.Black.copy(alpha = 0.4f), - shape = RoundedCornerShape(8.dp) - ) + .clip(RoundedCornerShape(16.dp)) + .background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.4f)) + .clickable(onClick = onFavoriteClick), + contentAlignment = Alignment.Center ) { Icon( imageVector = Lucide.Heart, - contentDescription = if (isFavorite) "В избранном" else "Добавить в избранное", - tint = if (isFavorite) Color(0xFFFF4081) else Color.White, - modifier = Modifier.size(18.dp) + 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 + ) + } } } diff --git a/app/src/main/java/com/radiola/ui/components/TrackListItem.kt b/app/src/main/java/com/radiola/ui/components/TrackListItem.kt index 9e177b9..e143a0d 100644 --- a/app/src/main/java/com/radiola/ui/components/TrackListItem.kt +++ b/app/src/main/java/com/radiola/ui/components/TrackListItem.kt @@ -1,17 +1,26 @@ package com.radiola.ui.components +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.MaterialTheme +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.Music import com.radiola.domain.model.Track +import com.radiola.ui.theme.RadiolaTheme +import com.radiola.ui.theme.pressScale import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -23,38 +32,57 @@ fun TrackListItem( onClick: () -> Unit = {}, modifier: Modifier = Modifier ) { + val colors = RadiolaTheme.colors + val interaction = remember { MutableInteractionSource() } Row( modifier = modifier .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 10.dp), + .pressScale(interactionSource = interaction) + .clickable(interactionSource = interaction, indication = null, onClick = onClick) + .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - AsyncImage( - model = track.coverUrl, - contentDescription = null, + Box( modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(8.dp)) - ) + .size(44.dp) + .clip(RoundedCornerShape(10.dp)) + .background(colors.surface2), + contentAlignment = Alignment.Center + ) { + if (!track.coverUrl.isNullOrBlank()) { + AsyncImage( + model = track.coverUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + Icon(Lucide.Music, null, tint = colors.textSecondary, modifier = Modifier.size(20.dp)) + } + } Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = "${track.artist} — ${track.song}", - style = MaterialTheme.typography.bodyMedium, - maxLines = 1 + style = androidx.compose.material3.MaterialTheme.typography.titleMedium, + color = colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Text( text = track.stationName, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + style = androidx.compose.material3.MaterialTheme.typography.labelMedium, + color = colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } timestamp?.let { + Spacer(Modifier.width(8.dp)) Text( text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(it)), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + style = androidx.compose.material3.MaterialTheme.typography.labelMedium, + color = colors.textMuted ) } }