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.
This commit is contained in:
KhooLy 2026-07-17 16:55:54 +03:00
parent 86c169de48
commit 7f93efbd41
2 changed files with 40 additions and 73 deletions

View file

@ -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,

View file

@ -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<Stream>, 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))