From 7f93efbd41ebcb30f126708091c5d3568bedc410 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:55:54 +0300 Subject: [PATCH] Polish player sidebar open/close and unify speed menu The sidebar previously entered with a fade plus a 44px nudge and disappeared instantly on close. It now slides the full distance from its edge (right for side sheets, bottom for bottom sheets) with the scrim fading in sync, and close requests play the exit animation before the panel is removed, including swipe-to-dismiss. Panel height changes between tabs are animated instead of snapping. The mobile speed menu drops its bordered chip grid in favor of the same checkmarked rows used by the audio, subtitle, and source lists. --- .../feature/player/PlayerSettingsSidebars.kt | 75 ++++--------------- .../feature/player/PlayerSidebarShell.kt | 38 +++++++--- 2 files changed, 40 insertions(+), 73 deletions(-) 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))