android: small ui tweaks

This commit is contained in:
Kavish Devar
2025-09-23 23:52:28 +05:30
parent 5f08edd49c
commit 7e5ee6726f
10 changed files with 700 additions and 605 deletions

1
android/.gitignore vendored
View File

@@ -1,3 +1,4 @@
crowdin.yml
*.iml *.iml
.gradle .gradle
/local.properties /local.properties

View File

@@ -20,22 +20,12 @@
package me.kavishdevar.librepods.composables package me.kavishdevar.librepods.composables
import androidx.compose.foundation.background
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.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -43,19 +33,19 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
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 me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.roundToInt import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AdaptiveStrengthSlider() { fun AdaptiveStrengthSlider() {
val sliderValue = remember { mutableFloatStateOf(0f) } val sliderValue = remember { mutableFloatStateOf(0f) }
@@ -100,80 +90,20 @@ fun AdaptiveStrengthSlider() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Slider( StyledSlider(
value = sliderValue.floatValue, mutableFloatState = sliderValue,
onValueChange = { onValueChange = {
sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f)) sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f))
}, },
valueRange = 0f..100f, valueRange = 0f..100f,
onValueChangeFinished = { snapPoints = listOf(0f, 50f, 100f),
sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(0f, 50f, 100f)) startLabel = stringResource(R.string.less_noise),
service.aacpManager.sendControlCommand( endLabel = stringResource(R.string.more_noise),
identifier = AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value, independent = false
value = (100 - sliderValue.floatValue).toInt()
)
},
modifier = Modifier
.fillMaxWidth()
.height(36.dp),
colors = SliderDefaults.colors(
thumbColor = thumbColor,
inactiveTrackColor = trackColor
),
thumb = {
Box(
modifier = Modifier
.size(24.dp)
.shadow(4.dp, CircleShape)
.background(thumbColor, CircleShape)
)
},
track = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(12.dp),
contentAlignment = Alignment.CenterStart
)
{
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.background(trackColor, RoundedCornerShape(4.dp))
)
}
}
) )
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Less Noise",
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = labelTextColor
),
modifier = Modifier.padding(start = 4.dp)
)
Text(
text = "More Noise",
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = labelTextColor
),
modifier = Modifier.padding(end = 4.dp)
)
}
} }
} }

View File

@@ -287,7 +287,7 @@ fun CallControlSettings(hazeState: HazeState) {
) )
} }
DragSelectableDropdown( StyledDropdown(
expanded = showSinglePressDropdown, expanded = showSinglePressDropdown,
onDismissRequest = { onDismissRequest = {
showSinglePressDropdown = false showSinglePressDropdown = false
@@ -415,7 +415,7 @@ fun CallControlSettings(hazeState: HazeState) {
) )
} }
DragSelectableDropdown( StyledDropdown(
expanded = showDoublePressDropdown, expanded = showDoublePressDropdown,
onDismissRequest = { onDismissRequest = {
showDoublePressDropdown = false showDoublePressDropdown = false

View File

@@ -269,7 +269,7 @@ fun MicrophoneSettings(hazeState: HazeState) {
val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right) val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right)
val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left) val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left)
DragSelectableDropdown( StyledDropdown(
expanded = showDropdown, expanded = showDropdown,
onDismissRequest = { onDismissRequest = {
showDropdown = false showDropdown = false
@@ -312,173 +312,3 @@ fun MicrophoneSettings(hazeState: HazeState) {
fun MicrophoneSettingsPreview() { fun MicrophoneSettingsPreview() {
MicrophoneSettings(HazeState()) MicrophoneSettings(HazeState())
} }
@ExperimentalHazeMaterialsApi
@Composable
fun DragSelectableDropdown(
expanded: Boolean,
onDismissRequest: () -> Unit,
options: List<String>,
selectedOption: String,
touchOffset: Offset?,
boxPosition: Offset,
onOptionSelected: (String) -> Unit,
externalHoveredIndex: Int? = null,
externalDragActive: Boolean = false,
hazeState: HazeState,
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
) {
if (expanded) {
val relativeOffset = touchOffset?.let { it - boxPosition } ?: Offset.Zero
Popup(
offset = IntOffset(relativeOffset.x.toInt(), relativeOffset.y.toInt()),
onDismissRequest = onDismissRequest
) {
AnimatedVisibility(
visible = true,
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
) {
Card(
modifier = modifier
.padding(8.dp)
.width(300.dp)
.background(Color.Transparent)
.clip(RoundedCornerShape(8.dp)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
var hoveredIndex by remember { mutableStateOf<Int?>(null) }
val itemHeight = 48.dp
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
var lastDragPosition by remember { mutableStateOf<Offset?>(null) }
LaunchedEffect(externalHoveredIndex, externalDragActive) {
if (externalDragActive) {
hoveredIndex = externalHoveredIndex
}
}
Column(
modifier = Modifier
.onGloballyPositioned { coordinates ->
popupSize = coordinates.size
}
.pointerInput(popupSize) {
detectDragGestures(
onDragStart = { offset ->
hoveredIndex = (offset.y / itemHeight.toPx()).toInt()
lastDragPosition = offset
},
onDrag = { change, _ ->
val y = change.position.y
hoveredIndex = (y / itemHeight.toPx()).toInt()
lastDragPosition = change.position
},
onDragEnd = {
val pos = lastDragPosition
val withinBounds = pos != null &&
pos.x >= 0f && pos.y >= 0f &&
pos.x <= popupSize.width.toFloat() && pos.y <= popupSize.height.toFloat()
if (withinBounds) {
hoveredIndex?.let { idx ->
if (idx in options.indices) {
onOptionSelected(options[idx])
}
}
onDismissRequest()
} else {
hoveredIndex = null
}
}
)
}
) {
options.forEachIndexed { index, text ->
val isHovered =
if (externalDragActive && externalHoveredIndex != null) {
index == externalHoveredIndex
} else {
index == hoveredIndex
}
val isSystemInDarkTheme = isSystemInDarkTheme()
Box(
modifier = Modifier
.fillMaxWidth()
.height(itemHeight)
.background(
Color.Transparent
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onOptionSelected(text)
onDismissRequest()
}
.hazeEffect(
state = hazeState,
style = CupertinoMaterials.regular(),
block = fun HazeEffectScope.() {
alpha = 1f
backgroundColor = if (isSystemInDarkTheme) {
Color(0xB02C2C2E)
} else {
Color(0xB0FFFFFF)
}
tints = if (isHovered) listOf(
HazeTint(
color = if (isSystemInDarkTheme) Color(0x338A8A8A) else Color(0x40D9D9D9)
)
) else listOf()
})
.padding(horizontal = 12.dp),
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text,
style = TextStyle(
fontSize = 16.sp,
color = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.75f),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Checkbox(
checked = text == selectedOption,
onCheckedChange = { onOptionSelected(text) },
colors = CheckboxDefaults.colors().copy(
checkedCheckmarkColor = Color(0xFF007AFF),
uncheckedCheckmarkColor = Color.Transparent,
checkedBoxColor = Color.Transparent,
uncheckedBoxColor = Color.Transparent,
checkedBorderColor = Color.Transparent,
uncheckedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
disabledCheckedBoxColor = Color.Transparent,
disabledUncheckedBoxColor = Color.Transparent,
disabledUncheckedBorderColor = Color.Transparent
)
)
}
}
if (index != options.lastIndex) {
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* LibrePods - AirPods liberated from Apples ecosystem
*
* Copyright (C) 2025 LibrePods contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.librepods.composables
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import dev.chrisbanes.haze.HazeEffectScope
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.R
@ExperimentalHazeMaterialsApi
@Composable
fun StyledDropdown(
expanded: Boolean,
onDismissRequest: () -> Unit,
options: List<String>,
selectedOption: String,
touchOffset: Offset?,
boxPosition: Offset,
onOptionSelected: (String) -> Unit,
externalHoveredIndex: Int? = null,
externalDragActive: Boolean = false,
hazeState: HazeState,
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
) {
if (expanded) {
val relativeOffset = touchOffset?.let { it - boxPosition } ?: Offset.Zero
Popup(
offset = IntOffset(relativeOffset.x.toInt(), relativeOffset.y.toInt()),
onDismissRequest = onDismissRequest
) {
AnimatedVisibility(
visible = true,
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
) {
Card(
modifier = modifier
.padding(8.dp)
.width(300.dp)
.background(Color.Transparent)
.clip(RoundedCornerShape(8.dp)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
var hoveredIndex by remember { mutableStateOf<Int?>(null) }
val itemHeight = 48.dp
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
var lastDragPosition by remember { mutableStateOf<Offset?>(null) }
LaunchedEffect(externalHoveredIndex, externalDragActive) {
if (externalDragActive) {
hoveredIndex = externalHoveredIndex
}
}
Column(
modifier = Modifier
.onGloballyPositioned { coordinates ->
popupSize = coordinates.size
}
.pointerInput(popupSize) {
detectDragGestures(
onDragStart = { offset ->
hoveredIndex = (offset.y / itemHeight.toPx()).toInt()
lastDragPosition = offset
},
onDrag = { change, _ ->
val y = change.position.y
hoveredIndex = (y / itemHeight.toPx()).toInt()
lastDragPosition = change.position
},
onDragEnd = {
val pos = lastDragPosition
val withinBounds = pos != null &&
pos.x >= 0f && pos.y >= 0f &&
pos.x <= popupSize.width.toFloat() && pos.y <= popupSize.height.toFloat()
if (withinBounds) {
hoveredIndex?.let { idx ->
if (idx in options.indices) {
onOptionSelected(options[idx])
}
}
onDismissRequest()
} else {
hoveredIndex = null
}
}
)
}
) {
options.forEachIndexed { index, text ->
val isHovered =
if (externalDragActive && externalHoveredIndex != null) {
index == externalHoveredIndex
} else {
index == hoveredIndex
}
val isSystemInDarkTheme = isSystemInDarkTheme()
Box(
modifier = Modifier
.fillMaxWidth()
.height(itemHeight)
.background(
Color.Transparent
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onOptionSelected(text)
onDismissRequest()
}
.hazeEffect(
state = hazeState,
style = CupertinoMaterials.regular(),
block = fun HazeEffectScope.() {
alpha = 1f
backgroundColor = if (isSystemInDarkTheme) {
Color(0xB02C2C2E)
} else {
Color(0xB0FFFFFF)
}
tints = if (isHovered) listOf(
HazeTint(
color = if (isSystemInDarkTheme) Color(0x338A8A8A) else Color(0x40D9D9D9)
)
) else listOf()
})
.padding(horizontal = 12.dp),
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text,
style = TextStyle(
fontSize = 16.sp,
color = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.75f),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Checkbox(
checked = text == selectedOption,
onCheckedChange = { onOptionSelected(text) },
colors = CheckboxDefaults.colors().copy(
checkedCheckmarkColor = Color(0xFF007AFF),
uncheckedCheckmarkColor = Color.Transparent,
checkedBoxColor = Color.Transparent,
uncheckedBoxColor = Color.Transparent,
checkedBorderColor = Color.Transparent,
uncheckedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
disabledCheckedBoxColor = Color.Transparent,
disabledUncheckedBoxColor = Color.Transparent,
disabledUncheckedBorderColor = Color.Transparent
)
)
}
}
if (index != options.lastIndex) {
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
}
}
}
}
}
}
}
}

View File

@@ -19,6 +19,7 @@
package me.kavishdevar.librepods.composables package me.kavishdevar.librepods.composables
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -87,7 +88,7 @@ import kotlin.math.roundToInt
@Composable @Composable
fun StyledSlider( fun StyledSlider(
label: String? = null, label: String? = null, // New optional parameter for the label
mutableFloatState: MutableFloatState, mutableFloatState: MutableFloatState,
onValueChange: (Float) -> Unit, onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>, valueRange: ClosedFloatingPointRange<Float>,
@@ -146,18 +147,6 @@ fun StyledSlider(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
if (label != null) {
Text(
text = label,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = labelTextColor,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
)
}
if (startLabel != null || endLabel != null) { if (startLabel != null || endLabel != null) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -358,17 +347,38 @@ fun StyledSlider(
} }
if (independent) { if (independent) {
Box(
Column (
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.background(backgroundColor, RoundedCornerShape(14.dp)) verticalArrangement = Arrangement.spacedBy(2.dp)
.padding(horizontal = 8.dp, vertical = 0.dp)
.heightIn(min = 55.dp),
contentAlignment = Alignment.Center
) { ) {
content() if (label != null) {
Text(
text = label,
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = labelTextColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp)
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(14.dp))
.padding(horizontal = 8.dp, vertical = 0.dp)
.heightIn(min = 55.dp),
contentAlignment = Alignment.Center
) {
content()
}
} }
} else { } else {
if (label != null) Log.w("StyledSlider", "Label is ignored when independent is false")
content() content()
} }
} }

View File

@@ -25,6 +25,7 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -41,6 +42,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
@@ -48,6 +51,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
@@ -62,6 +66,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -73,6 +78,9 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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
@@ -94,10 +102,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.StyledSlider
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
import me.kavishdevar.librepods.composables.NavigationButton import me.kavishdevar.librepods.composables.NavigationButton
import me.kavishdevar.librepods.composables.SinglePodANCSwitch import me.kavishdevar.librepods.composables.SinglePodANCSwitch
import me.kavishdevar.librepods.composables.StyledSlider
import me.kavishdevar.librepods.composables.StyledDropdown
import me.kavishdevar.librepods.composables.StyledSwitch import me.kavishdevar.librepods.composables.StyledSwitch
import me.kavishdevar.librepods.composables.VolumeControlSwitch import me.kavishdevar.librepods.composables.VolumeControlSwitch
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
@@ -132,6 +141,36 @@ fun AccessibilitySettingsScreen(navController: NavController) {
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF) val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
val hearingAidEnabled = remember { mutableStateOf(
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 0x01.toByte()
) }
val hearingAidListener = remember {
object : AACPManager.ControlCommandListener {
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
}
}
}
}
LaunchedEffect(Unit) {
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
}
DisposableEffect(Unit) {
onDispose {
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
}
}
Scaffold( Scaffold(
containerColor = if (isSystemInDarkTheme()) Color( containerColor = if (isSystemInDarkTheme()) Color(
0xFF000000 0xFF000000
@@ -411,23 +450,14 @@ fun AccessibilitySettingsScreen(navController: NavController) {
} }
} }
Text(
text = stringResource(R.string.tone_volume).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.tone_volume).uppercase(),
mutableFloatState = toneVolumeValue, mutableFloatState = toneVolumeValue,
onValueChange = { onValueChange = {
toneVolumeValue.floatValue = it toneVolumeValue.floatValue = it
}, },
valueRange = 0f..125f, valueRange = 0f..100f,
snapPoints = listOf(100f), snapPoints = listOf(75f),
startIcon = "\uDBC0\uDEA1", startIcon = "\uDBC0\uDEA1",
endIcon = "\uDBC0\uDEA9", endIcon = "\uDBC0\uDEA9",
independent = true independent = true
@@ -442,8 +472,25 @@ fun AccessibilitySettingsScreen(navController: NavController) {
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
SinglePodANCSwitch() SinglePodANCSwitch()
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
VolumeControlSwitch() VolumeControlSwitch()
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
LoudSoundReductionSwitch() LoudSoundReductionSwitch()
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
DropdownMenuComponent( DropdownMenuComponent(
label = stringResource(R.string.press_speed), label = stringResource(R.string.press_speed),
@@ -461,8 +508,15 @@ fun AccessibilitySettingsScreen(navController: NavController) {
?: 0.toByte() ?: 0.toByte()
) )
}, },
textColor = textColor textColor = textColor,
hazeState = hazeState
) )
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
DropdownMenuComponent( DropdownMenuComponent(
label = stringResource(R.string.press_and_hold_duration), label = stringResource(R.string.press_and_hold_duration),
options = listOf( options = listOf(
@@ -479,8 +533,15 @@ fun AccessibilitySettingsScreen(navController: NavController) {
?: 0.toByte() ?: 0.toByte()
) )
}, },
textColor = textColor textColor = textColor,
hazeState = hazeState
) )
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888),
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
)
DropdownMenuComponent( DropdownMenuComponent(
label = stringResource(R.string.volume_swipe_speed), label = stringResource(R.string.volume_swipe_speed),
options = listOf( options = listOf(
@@ -497,234 +558,237 @@ fun AccessibilitySettingsScreen(navController: NavController) {
?: 1.toByte() ?: 1.toByte()
) )
}, },
textColor = textColor textColor = textColor,
hazeState = hazeState
) )
} }
NavigationButton( if (!hearingAidEnabled.value) {
to = "transparency_customization", NavigationButton(
name = stringResource(R.string.customize_transparency_mode), to = "transparency_customization",
navController = navController name = stringResource(R.string.customize_transparency_mode),
) navController = navController
Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(R.string.apply_eq_to).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
Column(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(14.dp))
.padding(vertical = 0.dp)
) {
val darkModeLocal = isSystemInDarkTheme()
val phoneShape = RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
var phoneBackgroundColor by remember {
mutableStateOf(
if (darkModeLocal) Color(
0xFF1C1C1E
) else Color(0xFFFFFFFF)
)
}
val phoneAnimatedBackgroundColor by animateColorAsState(
targetValue = phoneBackgroundColor,
animationSpec = tween(durationMillis = 500)
) )
Row( Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(R.string.apply_eq_to).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
Column(
modifier = Modifier modifier = Modifier
.height(48.dp)
.fillMaxWidth() .fillMaxWidth()
.background(phoneAnimatedBackgroundColor, phoneShape) .background(backgroundColor, RoundedCornerShape(14.dp))
.pointerInput(Unit) { .padding(vertical = 0.dp)
detectTapGestures(
onPress = {
phoneBackgroundColor =
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
tryAwaitRelease()
phoneBackgroundColor =
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
phoneEQEnabled.value = !phoneEQEnabled.value
}
)
}
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( val darkModeLocal = isSystemInDarkTheme()
stringResource(R.string.phone),
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier.weight(1f)
)
Checkbox(
checked = phoneEQEnabled.value,
onCheckedChange = { phoneEQEnabled.value = it },
colors = CheckboxDefaults.colors().copy(
checkedCheckmarkColor = Color(0xFF007AFF),
uncheckedCheckmarkColor = Color.Transparent,
checkedBoxColor = Color.Transparent,
uncheckedBoxColor = Color.Transparent,
checkedBorderColor = Color.Transparent,
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier
.height(24.dp)
.scale(1.5f)
)
}
HorizontalDivider( val phoneShape = RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
thickness = 1.5.dp, var phoneBackgroundColor by remember {
color = Color(0x40888888) mutableStateOf(
) if (darkModeLocal) Color(
0xFF1C1C1E
val mediaShape = RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp) ) else Color(0xFFFFFFFF)
var mediaBackgroundColor by remember { )
mutableStateOf( }
if (darkModeLocal) Color( val phoneAnimatedBackgroundColor by animateColorAsState(
0xFF1C1C1E targetValue = phoneBackgroundColor,
) else Color(0xFFFFFFFF) animationSpec = tween(durationMillis = 500)
) )
}
val mediaAnimatedBackgroundColor by animateColorAsState(
targetValue = mediaBackgroundColor,
animationSpec = tween(durationMillis = 500)
)
Row(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.background(mediaAnimatedBackgroundColor, mediaShape)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
mediaBackgroundColor =
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
tryAwaitRelease()
mediaBackgroundColor =
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
mediaEQEnabled.value = !mediaEQEnabled.value
}
)
}
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.media),
fontSize = 16.sp,
color = textColor,
fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier.weight(1f)
)
Checkbox(
checked = mediaEQEnabled.value,
onCheckedChange = { mediaEQEnabled.value = it },
colors = CheckboxDefaults.colors().copy(
checkedCheckmarkColor = Color(0xFF007AFF),
uncheckedCheckmarkColor = Color.Transparent,
checkedBoxColor = Color.Transparent,
uncheckedBoxColor = Color.Transparent,
checkedBorderColor = Color.Transparent,
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier
.height(24.dp)
.scale(1.5f)
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(14.dp))
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
for (i in 0 until 8) {
val eqPhoneValue =
remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.height(48.dp)
.fillMaxWidth() .fillMaxWidth()
.height(38.dp) .background(phoneAnimatedBackgroundColor, phoneShape)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
phoneBackgroundColor =
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
tryAwaitRelease()
phoneBackgroundColor =
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
phoneEQEnabled.value = !phoneEQEnabled.value
}
)
}
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = String.format("%.2f", eqPhoneValue.floatValue), stringResource(R.string.phone),
fontSize = 12.sp, fontSize = 16.sp,
color = textColor, color = textColor,
modifier = Modifier.padding(bottom = 4.dp) fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier.weight(1f)
) )
Checkbox(
Slider( checked = phoneEQEnabled.value,
value = eqPhoneValue.floatValue, onCheckedChange = { phoneEQEnabled.value = it },
onValueChange = { newVal -> colors = CheckboxDefaults.colors().copy(
eqPhoneValue.floatValue = newVal checkedCheckmarkColor = Color(0xFF007AFF),
val newEQ = phoneMediaEQ.value.copyOf() uncheckedCheckmarkColor = Color.Transparent,
newEQ[i] = eqPhoneValue.floatValue checkedBoxColor = Color.Transparent,
phoneMediaEQ.value = newEQ uncheckedBoxColor = Color.Transparent,
}, checkedBorderColor = Color.Transparent,
valueRange = 0f..100f, uncheckedBorderColor = Color.Transparent
modifier = Modifier
.fillMaxWidth(0.9f)
.height(36.dp),
colors = SliderDefaults.colors(
thumbColor = thumbColor,
activeTrackColor = activeTrackColor,
inactiveTrackColor = trackColor
), ),
thumb = { modifier = Modifier
Box( .height(24.dp)
modifier = Modifier .scale(1.5f)
.size(24.dp) )
.shadow(4.dp, CircleShape) }
.background(thumbColor, CircleShape)
HorizontalDivider(
thickness = 1.5.dp,
color = Color(0x40888888)
)
val mediaShape = RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
var mediaBackgroundColor by remember {
mutableStateOf(
if (darkModeLocal) Color(
0xFF1C1C1E
) else Color(0xFFFFFFFF)
)
}
val mediaAnimatedBackgroundColor by animateColorAsState(
targetValue = mediaBackgroundColor,
animationSpec = tween(durationMillis = 500)
)
Row(
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
.background(mediaAnimatedBackgroundColor, mediaShape)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
mediaBackgroundColor =
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
tryAwaitRelease()
mediaBackgroundColor =
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
mediaEQEnabled.value = !mediaEQEnabled.value
}
) )
}, }
track = { .padding(horizontal = 16.dp),
Box( verticalAlignment = Alignment.CenterVertically
modifier = Modifier ) {
.fillMaxWidth() Text(
.height(12.dp), stringResource(R.string.media),
contentAlignment = Alignment.CenterStart fontSize = 16.sp,
) color = textColor,
{ fontFamily = FontFamily(Font(R.font.sf_pro)),
modifier = Modifier.weight(1f)
)
Checkbox(
checked = mediaEQEnabled.value,
onCheckedChange = { mediaEQEnabled.value = it },
colors = CheckboxDefaults.colors().copy(
checkedCheckmarkColor = Color(0xFF007AFF),
uncheckedCheckmarkColor = Color.Transparent,
checkedBoxColor = Color.Transparent,
uncheckedBoxColor = Color.Transparent,
checkedBorderColor = Color.Transparent,
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier
.height(24.dp)
.scale(1.5f)
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(14.dp))
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
for (i in 0 until 8) {
val eqPhoneValue =
remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(38.dp)
) {
Text(
text = String.format("%.2f", eqPhoneValue.floatValue),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.padding(bottom = 4.dp)
)
Slider(
value = eqPhoneValue.floatValue,
onValueChange = { newVal ->
eqPhoneValue.floatValue = newVal
val newEQ = phoneMediaEQ.value.copyOf()
newEQ[i] = eqPhoneValue.floatValue
phoneMediaEQ.value = newEQ
},
valueRange = 0f..100f,
modifier = Modifier
.fillMaxWidth(0.9f)
.height(36.dp),
colors = SliderDefaults.colors(
thumbColor = thumbColor,
activeTrackColor = activeTrackColor,
inactiveTrackColor = trackColor
),
thumb = {
Box(
modifier = Modifier
.size(24.dp)
.shadow(4.dp, CircleShape)
.background(thumbColor, CircleShape)
)
},
track = {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(4.dp) .height(12.dp),
.background(trackColor, RoundedCornerShape(4.dp)) contentAlignment = Alignment.CenterStart
)
Box(
modifier = Modifier
.fillMaxWidth(eqPhoneValue.floatValue / 100f)
.height(4.dp)
.background(activeTrackColor, RoundedCornerShape(4.dp))
) )
{
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.background(trackColor, RoundedCornerShape(4.dp))
)
Box(
modifier = Modifier
.fillMaxWidth(eqPhoneValue.floatValue / 100f)
.height(4.dp)
.background(activeTrackColor, RoundedCornerShape(4.dp))
)
}
} }
} )
)
Text( Text(
text = stringResource(R.string.band_label, i + 1), text = stringResource(R.string.band_label, i + 1),
fontSize = 12.sp, fontSize = 12.sp,
color = textColor, color = textColor,
modifier = Modifier.padding(top = 4.dp) modifier = Modifier.padding(top = 4.dp)
) )
}
} }
} }
} }
@@ -832,55 +896,129 @@ fun AccessibilityToggle(
} }
} }
@Composable @Composable
private fun DropdownMenuComponent( private fun DropdownMenuComponent(
label: String, label: String,
options: List<String>, options: List<String>,
selectedOption: String, selectedOption: String,
onOptionSelected: (String) -> Unit, onOptionSelected: (String) -> Unit,
textColor: Color textColor: Color,
hazeState: HazeState
) { ) {
var expanded by remember { mutableStateOf(false) } val density = LocalDensity.current
val itemHeightPx = with(density) { 48.dp.toPx() }
Column( var expanded by remember { mutableStateOf(false) }
var touchOffset by remember { mutableStateOf<Offset?>(null) }
var boxPosition by remember { mutableStateOf(Offset.Zero) }
var lastDismissTime by remember { mutableLongStateOf(0L) }
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
var parentDragActive by remember { mutableStateOf(false) }
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp) .padding(start = 12.dp, end = 12.dp)
.height(55.dp)
.pointerInput(Unit) {
detectTapGestures { offset ->
val now = System.currentTimeMillis()
if (expanded) {
expanded = false
lastDismissTime = now
} else {
if (now - lastDismissTime > 250L) {
touchOffset = offset
expanded = true
}
}
}
}
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
val now = System.currentTimeMillis()
touchOffset = offset
if (!expanded && now - lastDismissTime > 250L) {
expanded = true
}
lastDismissTime = now
parentDragActive = true
parentHoveredIndex = 0
},
onDrag = { change, _ ->
val current = change.position
val touch = touchOffset ?: current
val posInPopupY = current.y - touch.y
val idx = (posInPopupY / itemHeightPx).toInt()
parentHoveredIndex = idx
},
onDragEnd = {
parentDragActive = false
parentHoveredIndex?.let { idx ->
if (idx in options.indices) {
onOptionSelected(options[idx])
expanded = false
lastDismissTime = System.currentTimeMillis()
}
}
parentHoveredIndex = null
},
onDragCancel = {
parentDragActive = false
parentHoveredIndex = null
}
)
},
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = label, text = label,
style = TextStyle( fontSize = 16.sp,
fontSize = 16.sp, color = textColor,
fontWeight = FontWeight.Medium, modifier = Modifier.padding(bottom = 4.dp)
color = textColor
)
) )
Box( Box(
modifier = Modifier modifier = Modifier.onGloballyPositioned { coordinates ->
.fillMaxWidth() boxPosition = coordinates.positionInParent()
.clickable { expanded = true } }
.padding(8.dp)
) { ) {
Text( Row(
text = selectedOption, verticalAlignment = Alignment.CenterVertically
modifier = Modifier.padding(16.dp), ) {
) Text(
} text = selectedOption,
fontSize = 16.sp,
DropdownMenu( color = textColor.copy(alpha = 0.8f)
expanded = expanded, )
onDismissRequest = { expanded = false } Icon(
) { Icons.Default.KeyboardArrowDown,
options.forEach { option -> contentDescription = null,
DropdownMenuItem( modifier = Modifier.size(18.dp),
onClick = { tint = textColor.copy(alpha = 0.6f)
onOptionSelected(option)
expanded = false
},
text = { Text(text = option) }
) )
} }
StyledDropdown(
expanded = expanded,
onDismissRequest = {
expanded = false
lastDismissTime = System.currentTimeMillis()
},
options = options,
selectedOption = selectedOption,
touchOffset = touchOffset,
boxPosition = boxPosition,
externalHoveredIndex = parentHoveredIndex,
externalDragActive = parentDragActive,
onOptionSelected = { option ->
onOptionSelected(option)
expanded = false
},
hazeState = hazeState
)
} }
} }
} }

View File

@@ -343,17 +343,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
} }
} }
Text(
text = stringResource(R.string.amplification).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.amplification).uppercase(),
valueRange = -1f..1f, valueRange = -1f..1f,
mutableFloatState = amplificationSliderValue, mutableFloatState = amplificationSliderValue,
onValueChange = { onValueChange = {
@@ -374,17 +365,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
description = stringResource(R.string.swipe_amplification_description) description = stringResource(R.string.swipe_amplification_description)
) )
Text(
text = stringResource(R.string.balance).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.balance).uppercase(),
valueRange = -1f..1f, valueRange = -1f..1f,
mutableFloatState = balanceSliderValue, mutableFloatState = balanceSliderValue,
onValueChange = { onValueChange = {
@@ -396,17 +378,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
independent = true, independent = true,
) )
Text(
text = stringResource(R.string.tone).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.tone).uppercase(),
valueRange = -1f..1f, valueRange = -1f..1f,
mutableFloatState = toneSliderValue, mutableFloatState = toneSliderValue,
onValueChange = { onValueChange = {
@@ -417,18 +390,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
independent = true, independent = true,
) )
Text(
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.ambient_noise_reduction).uppercase(),
valueRange = 0f..1f, valueRange = 0f..1f,
mutableFloatState = ambientNoiseReductionSliderValue, mutableFloatState = ambientNoiseReductionSliderValue,
onValueChange = { onValueChange = {

View File

@@ -363,17 +363,8 @@ fun TransparencySettingsScreen(navController: NavController) {
description = stringResource(R.string.customize_transparency_mode_description) description = stringResource(R.string.customize_transparency_mode_description)
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.amplification).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.amplification).uppercase(),
valueRange = -1f..1f, valueRange = -1f..1f,
mutableFloatState = amplificationSliderValue, mutableFloatState = amplificationSliderValue,
onValueChange = { onValueChange = {
@@ -384,17 +375,8 @@ fun TransparencySettingsScreen(navController: NavController) {
independent = true independent = true
) )
Text(
text = stringResource(R.string.balance).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.balance).uppercase(),
valueRange = -1f..1f, valueRange = -1f..1f,
mutableFloatState = balanceSliderValue, mutableFloatState = balanceSliderValue,
onValueChange = { onValueChange = {
@@ -406,17 +388,8 @@ fun TransparencySettingsScreen(navController: NavController) {
independent = true, independent = true,
) )
Text(
text = stringResource(R.string.tone).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.tone).uppercase(),
valueRange = -1f..1f, valueRange = -1f..1f,
mutableFloatState = toneSliderValue, mutableFloatState = toneSliderValue,
onValueChange = { onValueChange = {
@@ -427,18 +400,8 @@ fun TransparencySettingsScreen(navController: NavController) {
independent = true, independent = true,
) )
Text(
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f),
fontFamily = FontFamily(Font(R.font.sf_pro))
),
modifier = Modifier.padding(8.dp, bottom = 0.dp)
)
StyledSlider( StyledSlider(
label = stringResource(R.string.ambient_noise_reduction).uppercase(),
valueRange = 0f..1f, valueRange = 0f..1f,
mutableFloatState = ambientNoiseReductionSliderValue, mutableFloatState = ambientNoiseReductionSliderValue,
onValueChange = { onValueChange = {

View File

@@ -25,7 +25,8 @@ data class TransparencySettings(
val leftAmbientNoiseReduction: Float, val leftAmbientNoiseReduction: Float,
val rightAmbientNoiseReduction: Float, val rightAmbientNoiseReduction: Float,
val netAmplification: Float, val netAmplification: Float,
val balance: Float val balance: Float,
val ownVoiceAmplification: Float? = null
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -44,6 +45,7 @@ data class TransparencySettings(
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
if (!leftEQ.contentEquals(other.leftEQ)) return false if (!leftEQ.contentEquals(other.leftEQ)) return false
if (!rightEQ.contentEquals(other.rightEQ)) return false if (!rightEQ.contentEquals(other.rightEQ)) return false
if (ownVoiceAmplification != other.ownVoiceAmplification) return false
return true return true
} }
@@ -60,6 +62,7 @@ data class TransparencySettings(
result = 31 * result + rightAmbientNoiseReduction.hashCode() result = 31 * result + rightAmbientNoiseReduction.hashCode()
result = 31 * result + leftEQ.contentHashCode() result = 31 * result + leftEQ.contentHashCode()
result = 31 * result + rightEQ.contentHashCode() result = 31 * result + rightEQ.contentHashCode()
result = 31 * result + (ownVoiceAmplification?.hashCode() ?: 0)
return result return result
} }
} }
@@ -91,6 +94,12 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
val rightConversationBoost = rightConvFloat > 0.5f val rightConversationBoost = rightConvFloat > 0.5f
val rightAmbientNoiseReduction = buffer.float val rightAmbientNoiseReduction = buffer.float
val ownVoiceAmplification = if (buffer.remaining() >= 4) {
buffer.float
} else {
null
}
val avg = (leftAmplification + rightAmplification) / 2 val avg = (leftAmplification + rightAmplification) / 2
val amplification = avg.coerceIn(-1f, 1f) val amplification = avg.coerceIn(-1f, 1f)
val diff = rightAmplification - leftAmplification val diff = rightAmplification - leftAmplification
@@ -109,7 +118,8 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
leftAmbientNoiseReduction = leftAmbientNoiseReduction, leftAmbientNoiseReduction = leftAmbientNoiseReduction,
rightAmbientNoiseReduction = rightAmbientNoiseReduction, rightAmbientNoiseReduction = rightAmbientNoiseReduction,
netAmplification = amplification, netAmplification = amplification,
balance = balance balance = balance,
ownVoiceAmplification = ownVoiceAmplification
) )
} }
@@ -120,7 +130,9 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans
debounceJob = CoroutineScope(Dispatchers.IO).launch { debounceJob = CoroutineScope(Dispatchers.IO).launch {
delay(100) delay(100)
try { try {
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN) val buffer = ByteBuffer.allocate(
if (transparencySettings.ownVoiceAmplification != null) 104 else 100
).order(ByteOrder.LITTLE_ENDIAN)
buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f) buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f)
@@ -140,6 +152,10 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans
buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f) buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f)
buffer.putFloat(transparencySettings.rightAmbientNoiseReduction) buffer.putFloat(transparencySettings.rightAmbientNoiseReduction)
if (transparencySettings.ownVoiceAmplification != null) {
buffer.putFloat(transparencySettings.ownVoiceAmplification)
}
val data = buffer.array() val data = buffer.array()
attManager.write(ATTHandles.TRANSPARENCY, value = data) attManager.write(ATTHandles.TRANSPARENCY, value = data)
} catch (e: IOException) { } catch (e: IOException) {