mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-11 01:27:13 +00:00
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.
This commit is contained in:
parent
d0207f50c6
commit
b4751da22e
5 changed files with 152 additions and 68 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue