android: little more liquid glass

This commit is contained in:
Kavish Devar
2025-09-23 01:20:41 +05:30
parent 8760757b76
commit 26de42243f
3 changed files with 95 additions and 128 deletions

View File

@@ -1,47 +1,39 @@
package me.kavishdevar.librepods.composables package me.kavishdevar.librepods.composables
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import com.kyant.backdrop.Backdrop
import dev.chrisbanes.haze.HazeState import com.kyant.backdrop.drawBackdrop
import dev.chrisbanes.haze.hazeEffect import com.kyant.backdrop.effects.blur
import dev.chrisbanes.haze.materials.CupertinoMaterials import com.kyant.backdrop.effects.colorFilter
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import com.kyant.backdrop.effects.refraction
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
@ExperimentalHazeMaterialsApi
@Composable @Composable
fun ConfirmationDialog( fun ConfirmationDialog(
showDialog: MutableState<Boolean>, showDialog: MutableState<Boolean>,
@@ -51,133 +43,100 @@ fun ConfirmationDialog(
dismissText: String = "Cancel", dismissText: String = "Cancel",
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> Unit = { showDialog.value = false }, onDismiss: () -> Unit = { showDialog.value = false },
hazeState: HazeState, backdrop: Backdrop,
) { ) {
val isDarkTheme = isSystemInDarkTheme()
val textColor = if (isDarkTheme) Color.White else Color.Black
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
if (showDialog.value) { if (showDialog.value) {
Dialog(onDismissRequest = { showDialog.value = false }) { val isLightTheme = !isSystemInDarkTheme()
val contentColor = if (isLightTheme) Color.Black else Color.White
val accentColor = if (isLightTheme) Color(0xFF0088FF) else Color(0xFF0091FF)
val containerColor = if (isLightTheme) Color(0xFFFAFAFA).copy(0.6f) else Color(0xFF121212).copy(0.4f)
val dimColor = if (isLightTheme) Color(0xFF29293A).copy(0.23f) else Color(0xFF121212).copy(0.56f)
Box(
Modifier
.background(dimColor)
.fillMaxSize()
.clickable(onClick = onDismiss)
) {
Box( Box(
modifier = Modifier Modifier
.align(Alignment.Center)
.drawBackdrop(
backdrop,
{ RoundedCornerShape(48f.dp) },
// highlight = { Highlight { HighlightStyle.Solid } },
onDrawSurface = { drawRect(containerColor) }
) {
colorFilter(
brightness = if (isLightTheme) 0.2f else 0.1f,
saturation = 1.5f
)
blur(if (isLightTheme) 16f.dp.toPx() else 8f.dp.toPx())
refraction(24f.dp.toPx(), 48f.dp.toPx(), true)
}
.fillMaxWidth(0.75f) .fillMaxWidth(0.75f)
.requiredWidthIn(min = 200.dp, max = 360.dp) .requiredWidthIn(min = 200.dp, max = 360.dp)
.background(Color.Transparent, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.hazeEffect(
hazeState,
style = CupertinoMaterials.regular(
containerColor = if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f)
)
)
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.Start) {
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(28.dp))
Text( Text(
title, title,
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = textColor, color = contentColor,
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
), ),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
message, message,
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
color = textColor.copy(alpha = 0.8f), color = contentColor.copy(alpha = 0.8f),
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
), ),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(
thickness = 1.dp,
color = Color(0x40888888),
modifier = Modifier.fillMaxWidth()
)
var leftPressed by remember { mutableStateOf(false) }
var rightPressed by remember { mutableStateOf(false) }
val pressedColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
Row( Row(
modifier = Modifier Modifier
.fillMaxWidth() .padding(24.dp, 12.dp, 24.dp, 24.dp)
.height(48.dp) .fillMaxWidth(),
.pointerInput(Unit) { horizontalArrangement = Arrangement.spacedBy(16.dp),
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val position = event.changes.first().position
val width = size.width.toFloat()
val height = size.height.toFloat()
val isWithinBounds = position.y >= 0 && position.y <= height
val isLeft = position.x < width / 2
event.changes.first().consume()
when (event.type) {
PointerEventType.Press -> {
if (isWithinBounds) {
leftPressed = isLeft
rightPressed = !isLeft
} else {
leftPressed = false
rightPressed = false
}
}
PointerEventType.Move -> {
if (isWithinBounds) {
leftPressed = isLeft
rightPressed = !isLeft
} else {
leftPressed = false
rightPressed = false
}
}
PointerEventType.Release -> {
if (isWithinBounds) {
if (leftPressed) {
onDismiss()
} else if (rightPressed) {
onConfirm()
}
}
leftPressed = false
rightPressed = false
}
}
}
}
},
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Box( Box(
modifier = Modifier Modifier
.clip(RoundedCornerShape(50.dp))
.background(containerColor.copy(0.2f))
.clickable(onClick = onDismiss)
.height(48.dp)
.weight(1f) .weight(1f)
.fillMaxHeight() .padding(horizontal = 16.dp),
.background(if (leftPressed) pressedColor else Color.Transparent),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text(dismissText, color = accentColor) Text(
dismissText,
style = TextStyle(contentColor, 16.sp)
)
} }
Box( Box(
modifier = Modifier Modifier
.width(1.dp) .clip(RoundedCornerShape(50.dp))
.fillMaxHeight() .background(accentColor)
.background(Color(0x40888888)) .clickable(onClick = onConfirm)
) .height(48.dp)
Box(
modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxHeight() .padding(horizontal = 16.dp),
.background(if (rightPressed) pressedColor else Color.Transparent),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text(confirmText, color = accentColor) Text(
confirmText,
style = TextStyle(Color.White, 16.sp)
)
} }
} }
} }

View File

@@ -133,16 +133,18 @@ fun StyledSlider(
val density = LocalDensity.current val density = LocalDensity.current
val content = @Composable { val content = @Composable {
Box(Modifier.fillMaxWidth()) { Box(
Modifier.fillMaxWidth(if (startIcon == null && endIcon == null) 0.95f else 1f)
) {
Box(Modifier Box(Modifier
.backdrop(sliderBackdrop) .backdrop(sliderBackdrop)
.fillMaxWidth()) { .fillMaxWidth()) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth(1f) .fillMaxWidth(1f)
.padding(vertical = 8.dp), .padding(vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
if (label != null) { if (label != null) {
Text( Text(
@@ -152,13 +154,14 @@ fun StyledSlider(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = labelTextColor, color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
) ),
) )
} }
if (startLabel != null || endLabel != null) { if (startLabel != null || endLabel != null) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
@@ -183,7 +186,10 @@ fun StyledSlider(
} }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.then(if (startIcon == null && endIcon == null) Modifier.padding(horizontal = 12.dp) else Modifier),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(0.dp) horizontalArrangement = Arrangement.spacedBy(0.dp)
) { ) {
@@ -191,7 +197,7 @@ fun StyledSlider(
Text( Text(
text = startIcon, text = startIcon,
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 18.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = labelTextColor, color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
@@ -240,7 +246,7 @@ fun StyledSlider(
Text( Text(
text = endIcon, text = endIcon,
style = TextStyle( style = TextStyle(
fontSize = 16.sp, fontSize = 18.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = labelTextColor, color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro)) fontFamily = FontFamily(Font(R.font.sf_pro))
@@ -260,10 +266,10 @@ fun StyledSlider(
Modifier Modifier
.graphicsLayer { .graphicsLayer {
val startOffset = val startOffset =
if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else 0f if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else with(density) { 8.dp.toPx() }
translationX = translationX =
startOffset + fraction * trackWidthState.floatValue - size.width / 2f startOffset + fraction * trackWidthState.floatValue - size.width / 2f
translationY = trackPositionState.floatValue / 2f translationY = if (startLabel != null || endLabel != null) trackPositionState.floatValue + with(density) { 22.dp.toPx() } + size.height / 2f else trackPositionState.floatValue + with(density) { 4.dp.toPx() }
} }
.draggable( .draggable(
rememberDraggableState { delta -> rememberDraggableState { delta ->
@@ -372,13 +378,13 @@ private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
} }
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
fun StyledSliderPreview() { fun StyledSliderPreview() {
val a = remember { mutableFloatStateOf(1f) } val a = remember { mutableFloatStateOf(1f) }
Box( Box(
Modifier Modifier
.background(if (isSystemInDarkTheme()) Color(0xFF121212) else Color(0xFFF0F0F0)) .background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF0F0F0))
.padding(16.dp) .padding(16.dp)
.fillMaxSize() .fillMaxSize()
) { ) {
@@ -393,8 +399,8 @@ fun StyledSliderPreview() {
}, },
valueRange = 0f..2f, valueRange = 0f..2f,
independent = true, independent = true,
startIcon = "A", startLabel = "A",
endIcon = "B" endLabel = "B"
) )
} }
} }

View File

@@ -70,6 +70,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.kyant.backdrop.backdrop
import com.kyant.backdrop.rememberBackdrop
import dev.chrisbanes.haze.HazeEffectScope import dev.chrisbanes.haze.HazeEffectScope
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
@@ -78,7 +80,6 @@ import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.ConfirmationDialog import me.kavishdevar.librepods.composables.ConfirmationDialog
@@ -90,8 +91,6 @@ import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
import me.kavishdevar.librepods.utils.sendTransparencySettings import me.kavishdevar.librepods.utils.sendTransparencySettings
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
private var debounceJob: Job? = null
private var phoneMediaDebounceJob: Job? = null
private const val TAG = "AccessibilitySettings" private const val TAG = "AccessibilitySettings"
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
@@ -109,6 +108,7 @@ fun HearingAidScreen(navController: NavController) {
val aacpManager = remember { ServiceManager.getService()?.aacpManager } val aacpManager = remember { ServiceManager.getService()?.aacpManager }
val showDialog = remember { mutableStateOf(false) } val showDialog = remember { mutableStateOf(false) }
val backdrop = rememberBackdrop()
val hearingAidEnabled = remember { val hearingAidEnabled = remember {
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID } val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
@@ -164,7 +164,9 @@ fun HearingAidScreen(navController: NavController) {
) )
) )
}, },
snackbarHost = { SnackbarHost(snackbarHostState) } snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = Modifier
.backdrop(backdrop)
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -210,7 +212,7 @@ fun HearingAidScreen(navController: NavController) {
} else { } else {
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02)) aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte()) aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
hearingAidEnabled.value = value hearingAidEnabled.value = false
} }
} }
@@ -450,6 +452,6 @@ fun HearingAidScreen(navController: NavController) {
} }
} }
}, },
hazeState = hazeState backdrop = backdrop
) )
} }