From a46e437351a9779326f29cbd8f35be13ac1b1b15 Mon Sep 17 00:00:00 2001 From: nk Date: Thu, 4 Jun 2026 17:19:10 +0300 Subject: [PATCH] =?UTF-8?q?feat(player):=203D-=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=D1=80=D0=BE=D1=82=20=D0=BE=D0=B1=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=20=D1=82=D1=80=D0=B5=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вместо простой смены — эффект переворота (как страница альбома/пластинка): старая обложка улетает передней гранью (0–90°), новая прилетает задней (90–180°, контр-вращение чтобы не зеркалилась). Компонент FlipCover, подключён к обложке в плеере; срабатывает при смене coverUrl трека. Co-Authored-By: Claude Opus 4.8 --- .../com/radiola/ui/components/FlipCover.kt | 78 +++++++++++++++++++ .../radiola/ui/player/PlayerBottomSheet.kt | 13 ++-- 2 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/radiola/ui/components/FlipCover.kt 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,