diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt index 83b062e..8641ac7 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSettingsSidebars.kt @@ -7,7 +7,6 @@ import com.fluxa.app.ui.catalog.FluxaIcons import androidx.compose.animation.Crossfade import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -205,42 +204,19 @@ fun UniversalSettingsSidebar( } else -> { val speeds = listOf(0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f) - if (deviceType == DeviceType.Mobile) { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.fillMaxWidth() - ) { - speeds.chunked(3).forEach { row -> - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.fillMaxWidth() - ) { - row.forEach { speed -> - SpeedChip( - label = formatSpeedLabel(speed), - isSelected = playbackSpeed == speed, - onClick = { onSpeedChange(speed) }, - modifier = Modifier.weight(1f) - ) - } - } - } - } - } else { - LazyColumn(verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.fillMaxWidth().heightIn(max = listMaxHeight)) { - items(speeds, key = { it }) { speed -> - TrackItem( - title = formatSpeedLabel(speed), - isSelected = playbackSpeed == speed, - onClick = { onSpeedChange(speed) }, - subtitle = when { - speed == 1.0f -> AppStrings.t(lang, "player.speed_standard") - speed < 1.0f -> AppStrings.t(lang, "player.speed_slower") - else -> AppStrings.t(lang, "player.speed_faster") - }, - deviceType = deviceType - ) - } + LazyColumn(verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.fillMaxWidth().heightIn(max = listMaxHeight)) { + items(speeds, key = { it }) { speed -> + TrackItem( + title = formatSpeedLabel(speed), + isSelected = playbackSpeed == speed, + onClick = { onSpeedChange(speed) }, + subtitle = when { + speed == 1.0f -> AppStrings.t(lang, "player.speed_standard") + speed < 1.0f -> AppStrings.t(lang, "player.speed_slower") + else -> AppStrings.t(lang, "player.speed_faster") + }, + deviceType = deviceType + ) } } } @@ -288,31 +264,6 @@ private fun formatSpeedLabel(speed: Float): String { return "${text}x" } -@Composable -private fun SpeedChip( - label: String, - isSelected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .height(46.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color.White.copy(alpha = if (isSelected) 0.10f else 0.04f)) - .border(1.dp, Color.White.copy(alpha = if (isSelected) 0.4f else 0.08f), RoundedCornerShape(10.dp)) - .clickable { onClick() }, - contentAlignment = Alignment.Center - ) { - Text( - text = label, - color = Color.White.copy(alpha = if (isSelected) 1f else 0.6f), - fontSize = 14.sp, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal - ) - } -} - @Composable private fun DelayAdjustmentItem( title: String, diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt index 6457e2b..97673e5 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/player/PlayerSidebarShell.kt @@ -4,12 +4,12 @@ import com.fluxa.app.common.AppStrings import com.fluxa.app.data.remote.Meta import com.fluxa.app.data.remote.Stream import com.fluxa.app.ui.catalog.DeviceType -import com.fluxa.app.ui.catalog.FluxaDimensions import com.fluxa.app.ui.catalog.FluxaIcons import com.fluxa.app.ui.catalog.LocalDeviceType import com.fluxa.app.ui.catalog.streamRawBody import com.fluxa.app.ui.catalog.streamSourceHeader +import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -45,6 +45,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex +import kotlinx.coroutines.delay @Composable fun SourceSidebar(streams: List, currentUrl: String, deviceType: DeviceType, lang: String = "en", onSelect: (String) -> Unit, onClose: (() -> Unit)? = null) { @@ -81,9 +82,23 @@ fun PlayerSidebarShell( content: @Composable ColumnScope.() -> Unit ) { var shown by remember { mutableStateOf(false) } + var closing by remember { mutableStateOf(false) } LaunchedEffect(Unit) { shown = true } - val panelAlpha by animateFloatAsState(if (shown) 1f else 0f, animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha), label = "sidebarAlpha") - val panelOffset by animateFloatAsState(if (shown) 0f else 44f, animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing), label = "sidebarOffset") + val visible = shown && !closing + val progress by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = if (visible) tween(300, easing = FastOutSlowInEasing) else tween(200, easing = FastOutSlowInEasing), + label = "sidebarProgress" + ) + fun requestClose() { + if (onClose != null && !closing) closing = true + } + LaunchedEffect(closing) { + if (closing) { + delay(210L) + onClose?.invoke() + } + } val isMobile = deviceType == DeviceType.Mobile BoxWithConstraints(modifier = Modifier.fillMaxSize().zIndex(100f)) { @@ -93,11 +108,11 @@ fun PlayerSidebarShell( Box( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = 0.48f)) + .background(Color.Black.copy(alpha = 0.48f * progress)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null - ) { onClose?.invoke() } + ) { requestClose() } ) val panelShape = if (isSideSheet) { @@ -129,10 +144,10 @@ fun PlayerSidebarShell( modifier = Modifier .align(if (isSideSheet) Alignment.CenterEnd else Alignment.BottomCenter) .then(panelSizeModifier) + .animateContentSize(tween(220, easing = FastOutSlowInEasing)) .graphicsLayer { - alpha = panelAlpha - translationY = if (!isSideSheet) panelOffset + dragOffsetPx else 0f - translationX = if (isSideSheet) panelOffset else 0f + translationY = if (!isSideSheet) (1f - progress) * size.height + dragOffsetPx else 0f + translationX = if (isSideSheet) (1f - progress) * size.width else 0f } .background(Color(0xFF10141A), shape = panelShape) .border(BorderStroke(1.dp, Color.White.copy(alpha = 0.08f)), shape = panelShape) @@ -153,9 +168,10 @@ fun PlayerSidebarShell( detectVerticalDragGestures( onDragEnd = { if (dragOffsetPx > dismissThresholdPx) { - onClose?.invoke() + requestClose() + } else { + dragOffsetPx = 0f } - dragOffsetPx = 0f }, onVerticalDrag = { change, dragAmount -> change.consume() @@ -207,7 +223,7 @@ fun PlayerSidebarShell( .size(if (deviceType == DeviceType.TV) 38.dp else 34.dp) .clip(CircleShape) .background(Color.White.copy(alpha = 0.08f)) - .clickable { onClose() }, + .clickable { requestClose() }, contentAlignment = Alignment.Center ) { Icon(FluxaIcons.Close, null, tint = Color.White, modifier = Modifier.size(if (deviceType == DeviceType.TV) 20.dp else 16.dp))