From 1e217fb82166e35abf9fea4f71e7563464dd6166 Mon Sep 17 00:00:00 2001 From: estiaksoyeb Date: Sun, 12 Jul 2026 01:28:32 +0600 Subject: [PATCH] feat: implement glass theme for player controls, buttons, title, sliders, OSD, and slide-to-unlock --- .../rex/preferences/AppearancePreferences.kt | 1 + .../rex/ui/player/controls/PlayerControls.kt | 4 +- .../controls/PlayerControlsLandscape.kt | 1 - .../player/controls/PlayerControlsPortrait.kt | 1 - .../player/controls/PlayerControlsShared.kt | 68 ++++++- .../controls/components/ControlsButton.kt | 28 ++- .../controls/components/CurrentChapter.kt | 39 +++- .../controls/components/GlassModifier.kt | 185 ++++++++++++++++++ .../controls/components/PlayerUpdates.kt | 35 +++- .../controls/components/SlideToUnlock.kt | 56 +++++- .../controls/components/VerticalSliders.kt | 77 ++++++-- .../controls/components/sheets/MoreSheet.kt | 8 + .../AppearancePreferencesScreen.kt | 18 ++ .../PlayerControlsPreferencesScreen.kt | 18 ++ .../ui/preferences/SearchablePreference.kt | 7 + app/src/main/res/values/strings.xml | 2 + 16 files changed, 517 insertions(+), 31 deletions(-) create mode 100644 app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/GlassModifier.kt diff --git a/app/src/main/kotlin/xyz/mpv/rex/preferences/AppearancePreferences.kt b/app/src/main/kotlin/xyz/mpv/rex/preferences/AppearancePreferences.kt index b3569e31..efa07be3 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/preferences/AppearancePreferences.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/preferences/AppearancePreferences.kt @@ -28,6 +28,7 @@ class AppearancePreferences( val useSystemFont = preferenceStore.getBoolean("use_system_font", false) val unlimitedNameLines = preferenceStore.getBoolean("unlimited_name_lines", false) val hidePlayerButtonsBackground = preferenceStore.getBoolean("hide_player_buttons_background", false) + val enableGlassPlayerControls = preferenceStore.getBoolean("enable_glass_player_controls", false) val enableBounceAnimation = preferenceStore.getBoolean("enable_bounce_animation", false) val showCommunityIcon = preferenceStore.getBoolean("show_community_icon", true) diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControls.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControls.kt index fbe77dbc..51600562 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControls.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControls.kt @@ -67,7 +67,8 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.Surface as M3Surface +import xyz.mpv.rex.ui.player.controls.components.glassSurface import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -1842,3 +1843,4 @@ fun PlayerControls( ) } } + diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsLandscape.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsLandscape.kt index 8c291746..9ec9e0b5 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsLandscape.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsLandscape.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsPortrait.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsPortrait.kt index f885fea7..a89dae2d 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsPortrait.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsPortrait.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsShared.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsShared.kt index 44bae0fa..05b12320 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsShared.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/PlayerControlsShared.kt @@ -59,7 +59,10 @@ import androidx.compose.ui.draw.rotate import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.Surface as M3Surface +import xyz.mpv.rex.ui.player.controls.components.glassSurface +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -1245,3 +1248,66 @@ fun RenderPlayerButton( } } } + +@Composable +fun Surface( + modifier: Modifier = Modifier, + shape: androidx.compose.ui.graphics.Shape = androidx.compose.foundation.shape.CircleShape, + color: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Unspecified, + contentColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Unspecified, + tonalElevation: androidx.compose.ui.unit.Dp = 0.dp, + shadowElevation: androidx.compose.ui.unit.Dp = 0.dp, + border: androidx.compose.foundation.BorderStroke? = null, + content: @Composable () -> Unit +) { + val appearancePreferences = org.koin.compose.koinInject() + val enableGlass by appearancePreferences.enableGlassPlayerControls.collectAsState() + val hideBackground by appearancePreferences.hidePlayerButtonsBackground.collectAsState() + val matchTheme by appearancePreferences.matchPlayerControlsToTheme.collectAsState() + + val activeSurfaceColor = when { + hideBackground -> androidx.compose.ui.graphics.Color.Transparent + matchTheme -> androidx.compose.material3.MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.85f) + else -> androidx.compose.material3.MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.75f) + } + + val isActive = color == activeSurfaceColor + + val glassModifier = if (enableGlass && color != androidx.compose.ui.graphics.Color.Transparent) { + Modifier.glassSurface( + shape = shape as? RoundedCornerShape ?: androidx.compose.foundation.shape.CircleShape, + backgroundColor = if (isActive) androidx.compose.material3.MaterialTheme.colorScheme.primary.copy(alpha = 0.25f) else androidx.compose.ui.graphics.Color.White.copy(alpha = 0.05f), + borderColor = if (isActive) androidx.compose.material3.MaterialTheme.colorScheme.primary.copy(alpha = 0.35f) else androidx.compose.ui.graphics.Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier + } + + val finalColor = if (enableGlass && color != androidx.compose.ui.graphics.Color.Transparent) androidx.compose.ui.graphics.Color.Transparent else color + val finalBorder = if (enableGlass && color != androidx.compose.ui.graphics.Color.Transparent) null else border + + M3Surface( + modifier = modifier.then(glassModifier), + shape = shape, + color = finalColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation, + border = finalBorder, + content = content + ) +} + diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/ControlsButton.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/ControlsButton.kt index ccacccd6..57e9b7e8 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/ControlsButton.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/ControlsButton.kt @@ -46,6 +46,7 @@ fun ControlsButton( val interactionSource = remember { MutableInteractionSource() } val appearancePreferences = koinInject() val hideBackground by appearancePreferences.hidePlayerButtonsBackground.collectAsState() + val enableGlass by appearancePreferences.enableGlassPlayerControls.collectAsState() val clickEvent = LocalPlayerButtonsClickEvent.current val matchTheme by appearancePreferences.matchPlayerControlsToTheme.collectAsState() @@ -57,6 +58,7 @@ fun ControlsButton( } val surfaceColor = when { + enableGlass && !hideBackground -> Color.Transparent hideBackground -> Color.Transparent matchTheme -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.65f) else -> MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.55f) @@ -71,9 +73,33 @@ fun ControlsButton( else -> if (hideBackground) controlColor else MaterialTheme.colorScheme.onSurface } + val glassModifier = if (enableGlass && !hideBackground) { + Modifier.glassSurface( + shape = CircleShape, + backgroundColor = Color.White.copy(alpha = 0.05f), + borderColor = Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier + } + Surface( modifier = modifier + .then(glassModifier) .clip(CircleShape) .combinedClickable( onClick = { @@ -90,7 +116,7 @@ fun ControlsButton( tonalElevation = 0.dp, shadowElevation = 0.dp, border = - if (hideBackground) { + if (enableGlass || hideBackground) { null } else { BorderStroke( diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/CurrentChapter.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/CurrentChapter.kt index daf7fa48..3cbe1b71 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/CurrentChapter.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/CurrentChapter.kt @@ -49,26 +49,47 @@ fun CurrentChapter( onClick: () -> Unit = {}, ) { val appearancePreferences = koinInject() + val enableGlass by appearancePreferences.enableGlassPlayerControls.collectAsState() + + val glassModifier = if (enableGlass) { + Modifier.glassSurface( + shape = RoundedCornerShape(50), + backgroundColor = Color.White.copy(alpha = 0.05f), + borderColor = Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier + } Surface( modifier = modifier + .then(glassModifier) .height(40.dp) .widthIn(max = 220.dp) .clip(RoundedCornerShape(50)) .clickable(onClick = onClick), shape = RoundedCornerShape(50), - color = - MaterialTheme.colorScheme.surfaceContainer.copy( - alpha = 0.55f, - ), + color = if (enableGlass) Color.Transparent else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.55f), contentColor = MaterialTheme.colorScheme.onSurface, tonalElevation = 0.dp, - border = - BorderStroke( - 1.dp, - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), - ), + border = if (enableGlass) null else BorderStroke( + 1.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), + ), ) { AnimatedContent( targetState = chapter, diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/GlassModifier.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/GlassModifier.kt new file mode 100644 index 00000000..cf822b83 --- /dev/null +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/GlassModifier.kt @@ -0,0 +1,185 @@ +package xyz.mpv.rex.ui.player.controls.components + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +// Custom Glass Surface Modifier that renders true directional inner shadows and clipped outer shadows +fun Modifier.glassSurface( + shape: RoundedCornerShape = RoundedCornerShape(24.dp), + backgroundColor: Color = Color.White.copy(alpha = 0.00f), + borderColor: Color = Color.White.copy(alpha = 0.12f), + borderWidth: Dp = 1.dp, + + // Outer shadow + outerShadowColor: Color = Color.Black.copy(alpha = 0.00f), + outerShadowBlur: Dp = 0.dp, + outerShadowOffsetX: Dp = 0.dp, + outerShadowOffsetY: Dp = 0.dp, + + // Inner Highlight (Top-left) + innerHighlightColor: Color = Color.White.copy(alpha = 0.35f), + innerHighlightBlur: Dp = 5.dp, + innerHighlightOffsetX: Dp = (-2).dp, + innerHighlightOffsetY: Dp = (-2).dp, + + // Inner Shadow (Bottom-right) + innerShadowColor: Color = Color.Black.copy(alpha = 0.35f), + innerShadowBlur: Dp = 5.dp, + innerShadowOffsetX: Dp = 2.dp, + innerShadowOffsetY: Dp = 2.dp +) = this.drawWithContent { + val density = this + val outerShadowBlurPx = with(density) { outerShadowBlur.toPx() } + val outerShadowOffsetXPx = with(density) { outerShadowOffsetX.toPx() } + val outerShadowOffsetYPx = with(density) { outerShadowOffsetY.toPx() } + + val cornerRadiusPx = with(density) { shape.topStart.toPx(size, density) } + + drawIntoCanvas { canvas -> + if (outerShadowBlurPx > 0f && outerShadowColor.alpha > 0f) { + val path = Path().apply { + addRoundRect( + RoundRect( + left = 0f, + top = 0f, + right = size.width, + bottom = size.height, + cornerRadius = CornerRadius(cornerRadiusPx) + ) + ) + } + canvas.save() + canvas.clipPath(path, clipOp = ClipOp.Difference) + + val shadowPaint = android.graphics.Paint().apply { + color = android.graphics.Color.TRANSPARENT + setShadowLayer( + outerShadowBlurPx, + outerShadowOffsetXPx, + outerShadowOffsetYPx, + outerShadowColor.toArgb() + ) + } + canvas.nativeCanvas.drawRoundRect( + 0f, 0f, size.width, size.height, + cornerRadiusPx, cornerRadiusPx, + shadowPaint + ) + canvas.restore() + } + } + + drawRoundRect( + color = backgroundColor, + cornerRadius = CornerRadius(cornerRadiusPx) + ) + + val innerShadowBlurPx = with(density) { innerShadowBlur.toPx() } + val innerShadowOffsetXPx = with(density) { innerShadowOffsetX.toPx() } + val innerShadowOffsetYPx = with(density) { innerShadowOffsetY.toPx() } + + val innerHighlightBlurPx = with(density) { innerHighlightBlur.toPx() } + val innerHighlightOffsetXPx = with(density) { innerHighlightOffsetX.toPx() } + val innerHighlightOffsetYPx = with(density) { innerHighlightOffsetY.toPx() } + + drawIntoCanvas { canvas -> + val rectPath = Path().apply { + addRoundRect( + RoundRect( + left = 0f, + top = 0f, + right = size.width, + bottom = size.height, + cornerRadius = CornerRadius(cornerRadiusPx) + ) + ) + } + + canvas.save() + canvas.clipPath(rectPath) + + if (innerHighlightBlurPx > 0f && innerHighlightColor.alpha > 0f) { + val highlightPaint = android.graphics.Paint().apply { + color = android.graphics.Color.TRANSPARENT + setShadowLayer( + innerHighlightBlurPx, + innerHighlightOffsetXPx, + innerHighlightOffsetYPx, + innerHighlightColor.toArgb() + ) + style = android.graphics.Paint.Style.FILL + } + + val invertedPath = android.graphics.Path().apply { + addRect( + -innerHighlightBlurPx - 20f, + -innerHighlightBlurPx - 20f, + size.width + innerHighlightBlurPx + 20f, + size.height + innerHighlightBlurPx + 20f, + android.graphics.Path.Direction.CW + ) + val buttonPath = android.graphics.Path().apply { + val rectF = android.graphics.RectF(0f, 0f, size.width, size.height) + addRoundRect(rectF, cornerRadiusPx, cornerRadiusPx, android.graphics.Path.Direction.CW) + } + op(buttonPath, android.graphics.Path.Op.DIFFERENCE) + } + canvas.nativeCanvas.drawPath(invertedPath, highlightPaint) + } + + if (innerShadowBlurPx > 0f && innerShadowColor.alpha > 0f) { + val shadowPaint = android.graphics.Paint().apply { + color = android.graphics.Color.TRANSPARENT + setShadowLayer( + innerShadowBlurPx, + innerShadowOffsetXPx, + innerShadowOffsetYPx, + innerShadowColor.toArgb() + ) + style = android.graphics.Paint.Style.FILL + } + + val invertedShadowPath = android.graphics.Path().apply { + addRect( + -innerShadowBlurPx - 20f, + -innerShadowBlurPx - 20f, + size.width + innerShadowBlurPx + 20f, + size.height + innerShadowBlurPx + 20f, + android.graphics.Path.Direction.CW + ) + val buttonPath = android.graphics.Path().apply { + val rectF = android.graphics.RectF(0f, 0f, size.width, size.height) + addRoundRect(rectF, cornerRadiusPx, cornerRadiusPx, android.graphics.Path.Direction.CW) + } + op(buttonPath, android.graphics.Path.Op.DIFFERENCE) + } + canvas.nativeCanvas.drawPath(invertedShadowPath, shadowPaint) + } + + canvas.restore() + } + + drawContent() + + if (borderWidth > 0.dp && borderColor.alpha > 0f) { + val borderWidthPx = with(density) { borderWidth.toPx() } + drawRoundRect( + color = borderColor, + cornerRadius = CornerRadius(cornerRadiusPx), + style = Stroke(width = borderWidthPx) + ) + } +} diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/PlayerUpdates.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/PlayerUpdates.kt index 158b100b..98a91673 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/PlayerUpdates.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/PlayerUpdates.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -28,7 +29,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.koin.compose.koinInject import xyz.mpv.rex.R +import xyz.mpv.rex.preferences.AppearancePreferences +import xyz.mpv.rex.preferences.preference.collectAsState import xyz.mpv.rex.ui.theme.spacing @Composable @@ -36,17 +40,44 @@ fun PlayerUpdate( modifier: Modifier = Modifier, content: @Composable () -> Unit = {}, ) { + val appearancePreferences = koinInject() + val enableGlass by appearancePreferences.enableGlassPlayerControls.collectAsState() + + val glassModifier = if (enableGlass) { + Modifier.glassSurface( + shape = RoundedCornerShape(100.dp), + backgroundColor = Color.White.copy(alpha = 0.05f), + borderColor = Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier + } + Surface( shape = RoundedCornerShape(100.dp), - color = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.55f), + color = if (enableGlass) Color.Transparent else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.55f), contentColor = MaterialTheme.colorScheme.onSurface, tonalElevation = 0.dp, shadowElevation = 0.dp, - border = BorderStroke( + border = if (enableGlass) null else BorderStroke( 1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), ), modifier = modifier + .then(glassModifier) .animateContentSize(), ) { Box( diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/SlideToUnlock.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/SlideToUnlock.kt index e1129cf6..d7ce4673 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/SlideToUnlock.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/SlideToUnlock.kt @@ -23,6 +23,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import org.koin.compose.koinInject +import xyz.mpv.rex.preferences.AppearancePreferences +import xyz.mpv.rex.preferences.preference.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -49,18 +52,67 @@ fun SlideToUnlock( ) { val coroutineScope = rememberCoroutineScope() + val appearancePrefs = koinInject() + val enableGlass by appearancePrefs.enableGlassPlayerControls.collectAsState() + var containerWidthPx by remember { mutableFloatStateOf(0f) } val sliderSize = 56.dp val offsetX = remember { Animatable(0f) } var isDragging by remember { mutableStateOf(false) } + val containerModifier = if (enableGlass) { + Modifier.glassSurface( + shape = RoundedCornerShape(32.dp), + backgroundColor = Color.White.copy(alpha = 0.05f), + borderColor = Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier.background(Color.Black.copy(alpha = 0.6f)) + } + + val sliderModifier = if (enableGlass) { + Modifier.glassSurface( + shape = CircleShape, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25f), + borderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.35f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier.background(MaterialTheme.colorScheme.primary) + } + Box( modifier = modifier .width(200.dp) .height(64.dp) .clip(RoundedCornerShape(32.dp)) - .background(Color.Black.copy(alpha = 0.6f)) + .then(containerModifier) .padding(4.dp) .onSizeChanged { size -> containerWidthPx = size.width.toFloat() @@ -95,7 +147,7 @@ fun SlideToUnlock( .offset { IntOffset(offsetX.value.roundToInt(), 0) } .size(sliderSize) .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) + .then(sliderModifier) .pointerInput(containerWidthPx) { if (containerWidthPx <= 0f) return@pointerInput diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/VerticalSliders.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/VerticalSliders.kt index c946a1f3..4f1d200e 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/VerticalSliders.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/VerticalSliders.kt @@ -33,13 +33,14 @@ import xyz.mpv.rex.preferences.AppearancePreferences import xyz.mpv.rex.preferences.preference.collectAsState import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import xyz.mpv.rex.ui.player.controls.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -71,6 +72,7 @@ fun VerticalSlider( // Read Toggle for Bounce Animation from Preferences val appearancePrefs = koinInject() val enableBounceAnimation by appearancePrefs.enableBounceAnimation.collectAsState() + val enableGlass by appearancePrefs.enableGlassPlayerControls.collectAsState() val trackWidthAnim = remember { Animatable(22f) } @@ -102,6 +104,35 @@ fun VerticalSlider( val trackWidth = trackWidthAnim.value.dp + val trackModifier = if (enableGlass) { + Modifier.glassSurface( + shape = RoundedCornerShape(16.dp), + backgroundColor = Color.White.copy(alpha = 0.05f), + borderColor = Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier + .background(MaterialTheme.colorScheme.background) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), + shape = RoundedCornerShape(16.dp), + ) + } + // Outer fixed-width container prevents layout shifts Box( modifier = @@ -116,12 +147,7 @@ fun VerticalSlider( .fillMaxHeight() .width(trackWidth) .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.background) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), - shape = RoundedCornerShape(16.dp), - ), + .then(trackModifier), contentAlignment = Alignment.BottomCenter, ) { val targetHeight by animateFloatAsState(percentage(coercedValue, range), label = "vsliderheight") @@ -161,6 +187,7 @@ fun VerticalSlider( // Read Toggle for Bounce Animation from Preferences val appearancePrefs = koinInject() val enableBounceAnimation by appearancePrefs.enableBounceAnimation.collectAsState() + val enableGlass by appearancePrefs.enableGlassPlayerControls.collectAsState() val trackWidthAnim = remember { Animatable(22f) } @@ -192,6 +219,35 @@ fun VerticalSlider( val trackWidth = trackWidthAnim.value.dp + val trackModifier = if (enableGlass) { + Modifier.glassSurface( + shape = RoundedCornerShape(16.dp), + backgroundColor = Color.White.copy(alpha = 0.05f), + borderColor = Color.White.copy(alpha = 0.15f), + borderWidth = 1.dp, + outerShadowColor = Color.Black.copy(alpha = 0.00f), + outerShadowBlur = 0.dp, + outerShadowOffsetX = 0.dp, + outerShadowOffsetY = 0.dp, + innerHighlightColor = Color.White.copy(alpha = 0.35f), + innerHighlightBlur = 5.dp, + innerHighlightOffsetX = (-2).dp, + innerHighlightOffsetY = (-2).dp, + innerShadowColor = Color.Black.copy(alpha = 0.35f), + innerShadowBlur = 5.dp, + innerShadowOffsetX = 2.dp, + innerShadowOffsetY = 2.dp + ) + } else { + Modifier + .background(MaterialTheme.colorScheme.background) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), + shape = RoundedCornerShape(16.dp), + ) + } + // Outer fixed-width container Box( modifier = modifier @@ -205,12 +261,7 @@ fun VerticalSlider( .fillMaxHeight() .width(trackWidth) .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.background) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), - shape = RoundedCornerShape(16.dp), - ), + .then(trackModifier), contentAlignment = Alignment.BottomCenter, ) { val targetHeight by animateFloatAsState(percentage(coercedValue, range), label = "vsliderheight") diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/sheets/MoreSheet.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/sheets/MoreSheet.kt index 9b2a9ecf..5c59f4c7 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/sheets/MoreSheet.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/player/controls/components/sheets/MoreSheet.kt @@ -580,6 +580,7 @@ fun InteractionTab() { val playerPreferences = koinInject() val hideBackground by appearancePreferences.hidePlayerButtonsBackground.collectAsState() + val enableGlass by appearancePreferences.enableGlassPlayerControls.collectAsState() val enableBounceAnimation by appearancePreferences.enableBounceAnimation.collectAsState() val preventSeekbarTap by gesturePreferences.preventSeekbarTap.collectAsState() val useSingleTapForCenter by gesturePreferences.useSingleTapForCenter.collectAsState() @@ -646,6 +647,13 @@ fun InteractionTab() { onCheckedChange = { appearancePreferences.hidePlayerButtonsBackground.set(it) } ) + InteractionSwitch( + label = stringResource(R.string.pref_appearance_enable_glass_player_controls_title), + description = stringResource(R.string.pref_appearance_enable_glass_player_controls_summary), + checked = enableGlass, + onCheckedChange = { appearancePreferences.enableGlassPlayerControls.set(it) } + ) + InteractionSwitch( label = stringResource(R.string.pref_gesture_prevent_seekbar_tap_title), description = stringResource(R.string.pref_gesture_prevent_seekbar_tap_summary), diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/AppearancePreferencesScreen.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/AppearancePreferencesScreen.kt index 223baf51..c8453485 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/AppearancePreferencesScreen.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/AppearancePreferencesScreen.kt @@ -240,6 +240,24 @@ object AppearancePreferencesScreen : Screen { PreferenceDivider() + val enableGlassPlayerControls by preferences.enableGlassPlayerControls.collectAsState() + SwitchPreference( + value = enableGlassPlayerControls, + onValueChange = { preferences.enableGlassPlayerControls.set(it) }, + title = { + Text( + text = stringResource(id = R.string.pref_appearance_enable_glass_player_controls_title), + ) + }, + summary = { + Text( + text = stringResource(id = R.string.pref_appearance_enable_glass_player_controls_summary), + ) + }, + ) + + PreferenceDivider() + val playerAlwaysDarkMode by preferences.playerAlwaysDarkMode.collectAsState() SwitchPreference( value = playerAlwaysDarkMode, diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/PlayerControlsPreferencesScreen.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/PlayerControlsPreferencesScreen.kt index e75acedd..db61411b 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/PlayerControlsPreferencesScreen.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/PlayerControlsPreferencesScreen.kt @@ -296,6 +296,7 @@ object PlayerControlsPreferencesScreen : Screen { item { val enableBounceAnimation by appearancePrefs.enableBounceAnimation.collectAsState() val hidePlayerButtonsBackground by appearancePrefs.hidePlayerButtonsBackground.collectAsState() + val enableGlassPlayerControls by appearancePrefs.enableGlassPlayerControls.collectAsState() val playerAlwaysDarkMode by appearancePrefs.playerAlwaysDarkMode.collectAsState() val playerTimeToDisappear by playerPrefs.playerTimeToDisappear.collectAsState() val predefinedTimeValues = listOf(500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000) @@ -339,6 +340,23 @@ object PlayerControlsPreferencesScreen : Screen { PreferenceDivider() + SwitchPreference( + value = enableGlassPlayerControls, + onValueChange = { appearancePrefs.enableGlassPlayerControls.set(it) }, + title = { + Text( + text = stringResource(id = R.string.pref_appearance_enable_glass_player_controls_title), + ) + }, + summary = { + Text( + text = stringResource(id = R.string.pref_appearance_enable_glass_player_controls_summary), + ) + }, + ) + + PreferenceDivider() + SwitchPreference( value = playerAlwaysDarkMode, onValueChange = { appearancePrefs.playerAlwaysDarkMode.set(it) }, diff --git a/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/SearchablePreference.kt b/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/SearchablePreference.kt index d9ec9a3b..bde6e0e0 100644 --- a/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/SearchablePreference.kt +++ b/app/src/main/kotlin/xyz/mpv/rex/ui/preferences/SearchablePreference.kt @@ -113,6 +113,13 @@ object SearchablePreferences { category = "Appearance", screen = PlayerControlsPreferencesScreen, )) + add(SearchablePreference( + titleRes = R.string.pref_appearance_enable_glass_player_controls_title, + summaryRes = R.string.pref_appearance_enable_glass_player_controls_summary, + keywords = listOf("glass", "glassmorphism", "buttons", "shadow", "inner shadow", "highlight", "player", "appearance"), + category = "Appearance", + screen = PlayerControlsPreferencesScreen, + )) add(SearchablePreference( titleRes = R.string.pref_player_display_hide_player_control_time, keywords = listOf("time", "hide", "controls", "disappear", "timeout", "ms"), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fbb6941..0310cf10 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,8 @@ Seekbar and volume/brightness sliders animate with a bounce effect Hide player buttons background Hide the background of all player control buttons + Glass player controls + Apply glass-morphism style to player control buttons Match player controls to theme Color player buttons using the current theme colors Player always dark mode