From b4751da22e9731fac34674c8e0546a2ce08c49ec Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:36:23 +0300 Subject: [PATCH] Move adjust entry to category headers and keep subtitles visible while adjusting The adjust entry no longer scrolls with the track lists: each category header (column label in the two-column card, the tab row in portrait) carries a small tune icon. Audio's icon opens the inline delay pill directly; the subtitles icon opens the adjust panel. The adjust panel now anchors to the top of the screen with a near-zero scrim so the bottom-center subtitle area stays visible, and while a slider is being dragged the whole card fades to 15% opacity. A sample cue styled with the live text/background/outline opacity values renders at the subtitle position whenever the panel is open, so there is always something to judge adjustments against. --- .../commonMain/resources/i18n/english_us.json | 1 + core/src/commonMain/resources/i18n/tr_tr.json | 1 + .../feature/player/PlayerSettingsSidebars.kt | 191 ++++++++++++------ .../feature/player/PlayerSidebarShell.kt | 26 ++- .../com/fluxa/app/ui/catalog/FluxaIcons.kt | 1 + 5 files changed, 152 insertions(+), 68 deletions(-) diff --git a/core/src/commonMain/resources/i18n/english_us.json b/core/src/commonMain/resources/i18n/english_us.json index 7ddab38..073c239 100644 --- a/core/src/commonMain/resources/i18n/english_us.json +++ b/core/src/commonMain/resources/i18n/english_us.json @@ -778,6 +778,7 @@ "player.hide_subtitles": "Hide subtitles", "player.embedded_subtitle": "Embedded subtitle", "player.speed_standard": "Normal", + "player.subtitle_sample": "This is what subtitles will look like", "player.speed_slower": "Slower pace", "player.speed_faster": "Faster playback", "player.choose_preferred_source": "Choose your preferred source.", diff --git a/core/src/commonMain/resources/i18n/tr_tr.json b/core/src/commonMain/resources/i18n/tr_tr.json index d07c7f0..dbdc8cc 100644 --- a/core/src/commonMain/resources/i18n/tr_tr.json +++ b/core/src/commonMain/resources/i18n/tr_tr.json @@ -778,6 +778,7 @@ "player.hide_subtitles": "Altyazıyı gizle", "player.embedded_subtitle": "Gömülü altyazı", "player.speed_standard": "Normal", + "player.subtitle_sample": "Altyazılar böyle görünecek", "player.speed_slower": "Daha sakin tempo", "player.speed_faster": "Daha hızlı oynatım", "player.choose_preferred_source": "Tercih ettiğiniz kaynağı seçin.", 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 b36656d..171aa3a 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 @@ -11,6 +11,8 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -30,9 +32,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -78,6 +83,7 @@ fun UniversalSettingsSidebar( val twoColumn = deviceType != DeviceType.Mobile || maxWidth > maxHeight var tab by remember(activeTab) { mutableStateOf(activeTab.coerceIn(0, 1)) } var showAdjust by remember(activeTab) { mutableStateOf(false) } + var sliderActive by remember { mutableStateOf(false) } val listMaxHeight = if (deviceType == DeviceType.TV) 400.dp else 440.dp val audioList: @Composable () -> Unit = { @@ -102,16 +108,6 @@ fun UniversalSettingsSidebar( deviceType = deviceType ) } - item { - TrackItem( - title = AppStrings.t(lang, "player.audio_delay"), - isSelected = false, - onClick = { onInlineDelayAdjust(0) }, - subtitle = formatDelayMs(audioDelayMs), - deviceType = deviceType, - trailingIcon = FluxaIcons.ChevronRight - ) - } } } val subtitleList: @Composable () -> Unit = { @@ -133,15 +129,6 @@ fun UniversalSettingsSidebar( deviceType = deviceType ) } - item { - TrackItem( - title = AppStrings.t(lang, "player.adjust"), - isSelected = false, - onClick = { showAdjust = true }, - deviceType = deviceType, - trailingIcon = FluxaIcons.ChevronRight - ) - } } } val subtitleAdjust: @Composable () -> Unit = { @@ -158,67 +145,86 @@ fun UniversalSettingsSidebar( title = AppStrings.t(lang, "settings.subtitle_text"), value = subtitleTextOpacity, deviceType = deviceType, - onChange = onSubtitleTextOpacityChange + onChange = onSubtitleTextOpacityChange, + onDragActiveChange = { sliderActive = it } ) OpacityAdjustmentItem( title = AppStrings.t(lang, "settings.subtitle_background"), value = subtitleBackgroundOpacity, deviceType = deviceType, - onChange = onSubtitleBackgroundOpacityChange + onChange = onSubtitleBackgroundOpacityChange, + onDragActiveChange = { sliderActive = it } ) OpacityAdjustmentItem( title = AppStrings.t(lang, "settings.subtitle_outline"), value = subtitleOutlineOpacity, deviceType = deviceType, - onChange = onSubtitleOutlineOpacityChange + onChange = onSubtitleOutlineOpacityChange, + onDragActiveChange = { sliderActive = it } ) } } - if (twoColumn) { + if (showAdjust) { + SubtitlePreviewCue( + lang = lang, + textOpacity = subtitleTextOpacity, + backgroundOpacity = subtitleBackgroundOpacity, + outlineOpacity = subtitleOutlineOpacity + ) PlayerSidebarShell( - title = if (showAdjust) AppStrings.t(lang, "player.adjust") else "", + title = AppStrings.t(lang, "player.adjust"), deviceType = deviceType, onClose = onClose, - onBack = if (showAdjust) { { showAdjust = false } } else null, - cardWidth = if (showAdjust) 380.dp else 580.dp + onBack = { showAdjust = false }, + cardWidth = 380.dp, + anchorTop = true, + scrimAlpha = 0.06f, + dimmed = sliderActive ) { - if (showAdjust) { - subtitleAdjust() - } else { - Row(horizontalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.weight(1f)) { - ColumnLabel(AppStrings.t(lang, "player.audio_title")) - audioList() - } - Column(modifier = Modifier.weight(1f)) { - ColumnLabel(AppStrings.t(lang, "player.subtitles_title")) - subtitleList() - } + subtitleAdjust() + } + } else if (twoColumn) { + PlayerSidebarShell( + title = "", + deviceType = deviceType, + onClose = onClose, + cardWidth = 580.dp + ) { + Row(horizontalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f)) { + ColumnHeader(AppStrings.t(lang, "player.audio_title")) { onInlineDelayAdjust(0) } + audioList() + } + Column(modifier = Modifier.weight(1f)) { + ColumnHeader(AppStrings.t(lang, "player.subtitles_title")) { showAdjust = true } + subtitleList() } } } } else { PlayerSidebarShell( - title = if (showAdjust) AppStrings.t(lang, "player.adjust") else "", + title = "", deviceType = deviceType, - onClose = onClose, - onBack = if (showAdjust) { { showAdjust = false } } else null + onClose = onClose ) { - if (showAdjust) { - subtitleAdjust() - } else { - SegmentedTabRow( - options = listOf( - AppStrings.t(lang, "player.audio_title") to 0, - AppStrings.t(lang, "player.subtitles_title") to 1 - ), - selected = tab, - onSelect = { tab = it } - ) - Spacer(Modifier.height(14.dp)) - if (tab == 0) audioList() else subtitleList() + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Box(modifier = Modifier.weight(1f)) { + SegmentedTabRow( + options = listOf( + AppStrings.t(lang, "player.audio_title") to 0, + AppStrings.t(lang, "player.subtitles_title") to 1 + ), + selected = tab, + onSelect = { tab = it } + ) + } + AdjustIconButton { + if (tab == 0) onInlineDelayAdjust(0) else showAdjust = true + } } + Spacer(Modifier.height(14.dp)) + if (tab == 0) audioList() else subtitleList() } } } @@ -353,14 +359,67 @@ private fun PillIconButton(icon: ImageVector, onClick: () -> Unit, emphasized: B } @Composable -private fun ColumnLabel(text: String) { - Text( - text = text, - color = Color.White.copy(alpha = 0.55f), - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(start = 12.dp, bottom = 6.dp) - ) +private fun ColumnHeader(text: String, onAdjust: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 4.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + color = Color.White.copy(alpha = 0.55f), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f) + ) + AdjustIconButton(onAdjust) + } +} + +@Composable +private fun AdjustIconButton(onClick: () -> Unit) { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.06f)) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Icon(FluxaIcons.Tune, null, tint = Color.White.copy(alpha = 0.8f), modifier = Modifier.size(14.dp)) + } +} + +@Composable +private fun SubtitlePreviewCue( + lang: String, + textOpacity: Float, + backgroundOpacity: Float, + outlineOpacity: Float +) { + Box(modifier = Modifier.fillMaxSize().zIndex(60f)) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 56.dp) + .clip(RoundedCornerShape(6.dp)) + .background(Color.Black.copy(alpha = backgroundOpacity.coerceIn(0f, 1f))) + .padding(horizontal = 12.dp, vertical = 6.dp) + ) { + Text( + text = AppStrings.t(lang, "player.subtitle_sample"), + color = Color.White.copy(alpha = textOpacity.coerceIn(0f, 1f)), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + style = TextStyle( + shadow = Shadow( + color = Color.Black.copy(alpha = outlineOpacity.coerceIn(0f, 1f)), + offset = Offset.Zero, + blurRadius = 4f + ) + ) + ) + } + } } @Composable @@ -413,8 +472,13 @@ private fun OpacityAdjustmentItem( title: String, value: Float, deviceType: DeviceType, - onChange: (Float) -> Unit + onChange: (Float) -> Unit, + onDragActiveChange: (Boolean) -> Unit = {} ) { + val sliderInteraction = remember { MutableInteractionSource() } + val dragged by sliderInteraction.collectIsDraggedAsState() + val pressed by sliderInteraction.collectIsPressedAsState() + LaunchedEffect(dragged, pressed) { onDragActiveChange(dragged || pressed) } Box( modifier = Modifier .fillMaxWidth() @@ -445,6 +509,7 @@ private fun OpacityAdjustmentItem( value = value.coerceIn(0f, 1f), onValueChange = { onChange(it.coerceIn(0f, 1f)) }, valueRange = 0f..1f, + interactionSource = sliderInteraction, colors = SliderDefaults.colors( thumbColor = Color.White, activeTrackColor = Color.White, 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 6c8c35d..40e407e 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 @@ -78,6 +78,9 @@ fun PlayerSidebarShell( onClose: (() -> Unit)? = null, onBack: (() -> Unit)? = null, cardWidth: Dp? = null, + anchorTop: Boolean = false, + scrimAlpha: Float = 0.42f, + dimmed: Boolean = false, content: @Composable ColumnScope.() -> Unit ) { var shown by remember { mutableStateOf(false) } @@ -100,14 +103,20 @@ fun PlayerSidebarShell( } val isMobile = deviceType == DeviceType.Mobile + val dim by animateFloatAsState( + targetValue = if (dimmed) 0.15f else 1f, + animationSpec = tween(150), + label = "sidebarDim" + ) + BoxWithConstraints(modifier = Modifier.fillMaxSize().zIndex(100f)) { val isLandscape = maxWidth > maxHeight - val isCard = !isMobile || isLandscape + val isCard = !isMobile || isLandscape || anchorTop Box( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = 0.42f * progress)) + .background(Color.Black.copy(alpha = scrimAlpha * progress * dim)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null @@ -121,6 +130,7 @@ fun PlayerSidebarShell( } val panelSizeModifier = if (isCard) { Modifier + .padding(top = if (anchorTop) 16.dp else 0.dp) .width(minOf(cardWidth ?: if (isMobile) 340.dp else 360.dp, maxWidth - 48.dp)) .wrapContentHeight() .heightIn(max = maxHeight - 48.dp) @@ -138,13 +148,19 @@ fun PlayerSidebarShell( Column( modifier = Modifier - .align(if (isCard) Alignment.Center else Alignment.BottomCenter) + .align( + when { + anchorTop -> Alignment.TopCenter + isCard -> Alignment.Center + else -> Alignment.BottomCenter + } + ) .then(panelSizeModifier) .animateContentSize(tween(220, easing = FastOutSlowInEasing)) .graphicsLayer { - alpha = if (isCard) progress else 1f + alpha = (if (isCard) progress else 1f) * dim translationY = if (isCard) { - (1f - progress) * 16.dp.toPx() + (1f - progress) * (if (anchorTop) (-16).dp.toPx() else 16.dp.toPx()) } else { (1f - progress) * size.height + dragOffsetPx } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt index d6fd830..4ace2ce 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/FluxaIcons.kt @@ -98,6 +98,7 @@ object FluxaIcons { val Forward10: ImageVector = Icons.Filled.Forward10 val Search: ImageVector = Icons.Filled.Search val Settings: ImageVector = Icons.Filled.Settings + val Tune: ImageVector = Icons.Filled.Tune val Share: ImageVector = Icons.Filled.Share val SkipNext: ImageVector = Icons.Filled.SkipNext val SkipPrevious: ImageVector = Icons.Filled.SkipPrevious