mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
android: small ui tweaks
This commit is contained in:
1
android/.gitignore
vendored
1
android/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
crowdin.yml
|
||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user