diff --git a/app/src/main/java/com/radiola/ui/components/Images.kt b/app/src/main/java/com/radiola/ui/components/Images.kt new file mode 100644 index 0000000..d2e729d --- /dev/null +++ b/app/src/main/java/com/radiola/ui/components/Images.kt @@ -0,0 +1,16 @@ +package com.radiola.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import coil.request.ImageRequest + +/** + * Модель изображения с плавным проявлением (crossfade). + * Используется во всех обложках, чтобы загрузка не «моргала». + */ +@Composable +fun crossfadeModel(url: String?): ImageRequest = + ImageRequest.Builder(LocalContext.current) + .data(url) + .crossfade(220) + .build() 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 61d4a7d..700e1d8 100644 --- a/app/src/main/java/com/radiola/ui/components/MiniPlayer.kt +++ b/app/src/main/java/com/radiola/ui/components/MiniPlayer.kt @@ -2,6 +2,10 @@ package com.radiola.ui.components import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.basicMarquee +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -29,6 +33,7 @@ import com.radiola.ui.theme.Motion import com.radiola.ui.theme.RadiolaTheme import com.radiola.ui.theme.pressScale +@OptIn(ExperimentalFoundationApi::class) @Composable fun MiniPlayer( stationName: String, @@ -39,6 +44,7 @@ fun MiniPlayer( modifier: Modifier = Modifier ) { val colors = RadiolaTheme.colors + val haptics = LocalHapticFeedback.current Row( modifier = modifier .fillMaxWidth() @@ -59,7 +65,7 @@ fun MiniPlayer( ) { if (track?.coverUrl != null) { AsyncImage( - model = track.coverUrl, + model = crossfadeModel(track.coverUrl), contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop @@ -83,7 +89,7 @@ fun MiniPlayer( style = androidx.compose.material3.MaterialTheme.typography.titleMedium, color = colors.textPrimary, maxLines = 1, - overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis + modifier = Modifier.basicMarquee() ) } Spacer(Modifier.width(8.dp)) @@ -95,9 +101,11 @@ fun MiniPlayer( .background(colors.accent) .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onPlayPause - ), + indication = null + ) { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onPlayPause() + }, contentAlignment = Alignment.Center ) { Crossfade(targetState = isPlaying, animationSpec = tween(Motion.Fast), label = "miniPlay") { playing -> 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 bb68ed0..784fb07 100644 --- a/app/src/main/java/com/radiola/ui/components/StationCard.kt +++ b/app/src/main/java/com/radiola/ui/components/StationCard.kt @@ -15,7 +15,9 @@ 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.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 @@ -36,6 +38,7 @@ fun StationCard( modifier: Modifier = Modifier ) { val colors = RadiolaTheme.colors + val haptics = LocalHapticFeedback.current val interaction = remember { MutableInteractionSource() } val heartTint by animateColorAsState( targetValue = if (isFavorite) colors.accent else colors.textPrimary, @@ -57,7 +60,7 @@ fun StationCard( ) { if (!station.coverUrl.isNullOrBlank()) { AsyncImage( - model = station.coverUrl, + model = crossfadeModel(station.coverUrl), contentDescription = station.name, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop @@ -77,7 +80,10 @@ fun StationCard( .size(32.dp) .clip(RoundedCornerShape(16.dp)) .background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.4f)) - .clickable(onClick = onFavoriteClick), + .clickable { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onFavoriteClick() + }, contentAlignment = Alignment.Center ) { Icon( 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 e143a0d..dee1d91 100644 --- a/app/src/main/java/com/radiola/ui/components/TrackListItem.kt +++ b/app/src/main/java/com/radiola/ui/components/TrackListItem.kt @@ -51,7 +51,7 @@ fun TrackListItem( ) { if (!track.coverUrl.isNullOrBlank()) { AsyncImage( - model = track.coverUrl, + model = crossfadeModel(track.coverUrl), contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop diff --git a/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt b/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt index 59a1ecd..a345287 100644 --- a/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt +++ b/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt @@ -1,6 +1,8 @@ package com.radiola.ui.player import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.basicMarquee import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween @@ -20,7 +22,9 @@ 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.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -47,6 +51,7 @@ import com.radiola.ui.theme.Motion import com.radiola.ui.theme.RadiolaTheme import com.radiola.ui.theme.pressScale +@OptIn(ExperimentalFoundationApi::class) @Composable fun PlayerBottomSheet( station: Station?, @@ -65,6 +70,7 @@ fun PlayerBottomSheet( val context = LocalContext.current val enabledServices by viewModel.enabledServices.collectAsState() val colors = RadiolaTheme.colors + val haptics = LocalHapticFeedback.current Column( modifier = modifier @@ -95,7 +101,7 @@ fun PlayerBottomSheet( val coverModel = track?.coverUrl ?: station?.coverUrl if (!coverModel.isNullOrBlank()) { AsyncImage( - model = coverModel, + model = com.radiola.ui.components.crossfadeModel(coverModel), contentDescription = station?.name, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop @@ -123,8 +129,8 @@ fun PlayerBottomSheet( style = MaterialTheme.typography.headlineLarge, color = colors.textPrimary, maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = androidx.compose.ui.text.style.TextAlign.Center + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + modifier = Modifier.basicMarquee() ) Spacer(Modifier.height(4.dp)) Text( @@ -161,7 +167,13 @@ fun PlayerBottomSheet( label = "heartTint" ) PlayerIconBtn(size = 44.dp) { - IconButton(onClick = onToggleFavorite, modifier = Modifier.size(44.dp)) { + IconButton( + onClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onToggleFavorite() + }, + modifier = Modifier.size(44.dp) + ) { Icon(Lucide.Heart, "Избранное", tint = heartTint, modifier = Modifier.size(22.dp)) } } @@ -181,7 +193,10 @@ fun PlayerBottomSheet( .clip(CircleShape) .background(colors.accent) .pressScale(interactionSource = playInteraction) - .clickable(interactionSource = playInteraction, indication = null, onClick = onPlayPause), + .clickable(interactionSource = playInteraction, indication = null) { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + onPlayPause() + }, contentAlignment = Alignment.Center ) { Crossfade(