diff --git a/app/src/main/java/com/radiola/ui/components/FlipCover.kt b/app/src/main/java/com/radiola/ui/components/FlipCover.kt new file mode 100644 index 0000000..ebf40f6 --- /dev/null +++ b/app/src/main/java/com/radiola/ui/components/FlipCover.kt @@ -0,0 +1,78 @@ +package com.radiola.ui.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage + +/** + * Обложка с эффектом 3D-переворота при смене изображения — как будто + * перелистывается страница альбома / пластинка. Старая обложка «улетает» + * передней стороной (0–90°), новая «прилетает» задней (90–180°). + */ +@Composable +fun FlipCover( + model: String?, + contentDescription: String?, + modifier: Modifier = Modifier, + fallback: @Composable () -> Unit, +) { + var current by remember { mutableStateOf(model) } + var previous by remember { mutableStateOf(model) } + val rotation = remember { Animatable(0f) } + + LaunchedEffect(model) { + if (model != current) { + previous = current + current = model + rotation.snapTo(0f) + rotation.animateTo(180f, animationSpec = tween(620, easing = FastOutSlowInEasing)) + // Оседаем: новая обложка становится «лицом», угол 0 — без рывка. + previous = current + rotation.snapTo(0f) + } + } + + val angle = rotation.value + val showFront = angle <= 90f + val faceModel = if (showFront) previous else current + + Box( + modifier = modifier.graphicsLayer { + rotationY = angle + cameraDistance = 16f * density + }, + contentAlignment = Alignment.Center, + ) { + // Заднюю грань контр-вращаем, чтобы изображение не было зеркальным. + Box( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { rotationY = if (showFront) 0f else 180f }, + contentAlignment = Alignment.Center, + ) { + if (!faceModel.isNullOrBlank()) { + AsyncImage( + model = faceModel, + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } else { + fallback() + } + } + } +} 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 dc9c35e..1ab2771 100644 --- a/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt +++ b/app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt @@ -140,14 +140,11 @@ fun PlayerBottomSheet( contentAlignment = Alignment.Center ) { val coverModel = track?.coverUrl ?: station?.coverUrl - if (!coverModel.isNullOrBlank()) { - AsyncImage( - model = com.radiola.ui.components.crossfadeModel(coverModel), - contentDescription = station?.name, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } else { + com.radiola.ui.components.FlipCover( + model = coverModel, + contentDescription = station?.name, + modifier = Modifier.fillMaxSize() + ) { Icon( imageVector = Lucide.Radio, contentDescription = null,