diff --git a/android/.gitignore b/android/.gitignore index 28a82e3..62ca9dc 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1,3 +1,4 @@ +crowdin.yml *.iml .gradle /local.properties diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AdaptiveStrengthSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/AdaptiveStrengthSlider.kt index e60bea9..7e5c8a4 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AdaptiveStrengthSlider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/AdaptiveStrengthSlider.kt @@ -20,22 +20,12 @@ package me.kavishdevar.librepods.composables -import androidx.compose.foundation.background 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.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.LaunchedEffect import androidx.compose.runtime.DisposableEffect @@ -43,19 +33,19 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import me.kavishdevar.librepods.R import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.utils.AACPManager import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.roundToInt -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AdaptiveStrengthSlider() { val sliderValue = remember { mutableFloatStateOf(0f) } @@ -100,80 +90,20 @@ fun AdaptiveStrengthSlider() { Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), + .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - Slider( - value = sliderValue.floatValue, + StyledSlider( + mutableFloatState = sliderValue, onValueChange = { sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f)) }, valueRange = 0f..100f, - onValueChangeFinished = { - sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(0f, 50f, 100f)) - service.aacpManager.sendControlCommand( - identifier = AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value, - 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)) - ) - } - - } + snapPoints = listOf(0f, 50f, 100f), + startLabel = stringResource(R.string.less_noise), + endLabel = stringResource(R.string.more_noise), + independent = false ) - - 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) - ) - } } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt index c485c3a..3a06981 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/CallControlSettings.kt @@ -287,7 +287,7 @@ fun CallControlSettings(hazeState: HazeState) { ) } - DragSelectableDropdown( + StyledDropdown( expanded = showSinglePressDropdown, onDismissRequest = { showSinglePressDropdown = false @@ -415,7 +415,7 @@ fun CallControlSettings(hazeState: HazeState) { ) } - DragSelectableDropdown( + StyledDropdown( expanded = showDoublePressDropdown, onDismissRequest = { showDoublePressDropdown = false diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt index 8e10609..b87d74f 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/MicrophoneSettings.kt @@ -269,7 +269,7 @@ fun MicrophoneSettings(hazeState: HazeState) { val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right) val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left) - DragSelectableDropdown( + StyledDropdown( expanded = showDropdown, onDismissRequest = { showDropdown = false @@ -312,173 +312,3 @@ fun MicrophoneSettings(hazeState: HazeState) { fun MicrophoneSettingsPreview() { MicrophoneSettings(HazeState()) } - -@ExperimentalHazeMaterialsApi -@Composable -fun DragSelectableDropdown( - expanded: Boolean, - onDismissRequest: () -> Unit, - options: List, - 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(null) } - val itemHeight = 48.dp - - var popupSize by remember { mutableStateOf(IntSize(0, 0)) } - var lastDragPosition by remember { mutableStateOf(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) - ) - } - } - } - } - } - } - } -} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledDropdown.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledDropdown.kt new file mode 100644 index 0000000..46168b8 --- /dev/null +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledDropdown.kt @@ -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 . + */ + +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, + 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(null) } + val itemHeight = 48.dp + + var popupSize by remember { mutableStateOf(IntSize(0, 0)) } + var lastDragPosition by remember { mutableStateOf(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) + ) + } + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt index 64187d1..523eb2c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt @@ -19,6 +19,7 @@ package me.kavishdevar.librepods.composables import android.content.res.Configuration +import android.util.Log import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.spring import androidx.compose.foundation.background @@ -87,7 +88,7 @@ import kotlin.math.roundToInt @Composable fun StyledSlider( - label: String? = null, + label: String? = null, // New optional parameter for the label mutableFloatState: MutableFloatState, onValueChange: (Float) -> Unit, valueRange: ClosedFloatingPointRange, @@ -146,18 +147,6 @@ fun StyledSlider( horizontalAlignment = Alignment.CenterHorizontally, 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) { Row( modifier = Modifier @@ -358,17 +347,38 @@ fun StyledSlider( } if (independent) { - Box( + + Column ( modifier = Modifier - .fillMaxWidth() - .background(backgroundColor, RoundedCornerShape(14.dp)) - .padding(horizontal = 8.dp, vertical = 0.dp) - .heightIn(min = 55.dp), - contentAlignment = Alignment.Center + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) ) { - 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 { + if (label != null) Log.w("StyledSlider", "Label is ignored when independent is false") content() } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt index b6fc44f..95df836 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.isSystemInDarkTheme 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.RoundedCornerShape 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.Checkbox import androidx.compose.material3.CheckboxDefaults @@ -48,6 +51,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults @@ -62,6 +66,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.graphics.Color 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.text.TextStyle import androidx.compose.ui.text.font.Font @@ -94,10 +102,11 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.kavishdevar.librepods.R -import me.kavishdevar.librepods.composables.StyledSlider import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch import me.kavishdevar.librepods.composables.NavigationButton 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.VolumeControlSwitch import me.kavishdevar.librepods.services.ServiceManager @@ -132,6 +141,36 @@ fun AccessibilitySettingsScreen(navController: NavController) { val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5) 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( containerColor = if (isSystemInDarkTheme()) Color( 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( + label = stringResource(R.string.tone_volume).uppercase(), mutableFloatState = toneVolumeValue, onValueChange = { toneVolumeValue.floatValue = it }, - valueRange = 0f..125f, - snapPoints = listOf(100f), + valueRange = 0f..100f, + snapPoints = listOf(75f), startIcon = "\uDBC0\uDEA1", endIcon = "\uDBC0\uDEA9", independent = true @@ -442,8 +472,25 @@ fun AccessibilitySettingsScreen(navController: NavController) { verticalArrangement = Arrangement.SpaceBetween ) { SinglePodANCSwitch() + HorizontalDivider( + thickness = 1.5.dp, + color = Color(0x40888888), + modifier = Modifier.padding(start = 12.dp, end = 0.dp) + ) + VolumeControlSwitch() + HorizontalDivider( + thickness = 1.5.dp, + color = Color(0x40888888), + modifier = Modifier.padding(start = 12.dp, end = 0.dp) + ) + LoudSoundReductionSwitch() + HorizontalDivider( + thickness = 1.5.dp, + color = Color(0x40888888), + modifier = Modifier.padding(start = 12.dp, end = 0.dp) + ) DropdownMenuComponent( label = stringResource(R.string.press_speed), @@ -461,8 +508,15 @@ fun AccessibilitySettingsScreen(navController: NavController) { ?: 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( label = stringResource(R.string.press_and_hold_duration), options = listOf( @@ -479,8 +533,15 @@ fun AccessibilitySettingsScreen(navController: NavController) { ?: 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( label = stringResource(R.string.volume_swipe_speed), options = listOf( @@ -497,234 +558,237 @@ fun AccessibilitySettingsScreen(navController: NavController) { ?: 1.toByte() ) }, - textColor = textColor + textColor = textColor, + hazeState = hazeState ) } - NavigationButton( - to = "transparency_customization", - 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) + if (!hearingAidEnabled.value) { + NavigationButton( + to = "transparency_customization", + name = stringResource(R.string.customize_transparency_mode), + navController = navController ) - 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 - .height(48.dp) .fillMaxWidth() - .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 + .background(backgroundColor, RoundedCornerShape(14.dp)) + .padding(vertical = 0.dp) ) { - Text( - 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) - ) - } + val darkModeLocal = isSystemInDarkTheme() - 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 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) ) - } - 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( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .height(48.dp) .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 = String.format("%.2f", eqPhoneValue.floatValue), - fontSize = 12.sp, + stringResource(R.string.phone), + fontSize = 16.sp, color = textColor, - modifier = Modifier.padding(bottom = 4.dp) + fontFamily = FontFamily(Font(R.font.sf_pro)), + modifier = Modifier.weight(1f) ) - - 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 + 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 ), - thumb = { - Box( - modifier = Modifier - .size(24.dp) - .shadow(4.dp, CircleShape) - .background(thumbColor, CircleShape) + modifier = Modifier + .height(24.dp) + .scale(1.5f) + ) + } + + 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 = { - Box( - modifier = Modifier - .fillMaxWidth() - .height(12.dp), - contentAlignment = Alignment.CenterStart - ) - { + } + .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( + 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( 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)) + .height(12.dp), + contentAlignment = Alignment.CenterStart ) + { + 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 = stringResource(R.string.band_label, i + 1), - fontSize = 12.sp, - color = textColor, - modifier = Modifier.padding(top = 4.dp) - ) + Text( + text = stringResource(R.string.band_label, i + 1), + fontSize = 12.sp, + color = textColor, + modifier = Modifier.padding(top = 4.dp) + ) + } } } } @@ -832,55 +896,129 @@ fun AccessibilityToggle( } } + @Composable private fun DropdownMenuComponent( label: String, options: List, selectedOption: String, 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(null) } + var boxPosition by remember { mutableStateOf(Offset.Zero) } + var lastDismissTime by remember { mutableLongStateOf(0L) } + var parentHoveredIndex by remember { mutableStateOf(null) } + var parentDragActive by remember { mutableStateOf(false) } + + Row( modifier = Modifier .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 = label, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = textColor - ) + fontSize = 16.sp, + color = textColor, + modifier = Modifier.padding(bottom = 4.dp) ) - Box( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true } - .padding(8.dp) + modifier = Modifier.onGloballyPositioned { coordinates -> + boxPosition = coordinates.positionInParent() + } ) { - Text( - text = selectedOption, - modifier = Modifier.padding(16.dp), - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - options.forEach { option -> - DropdownMenuItem( - onClick = { - onOptionSelected(option) - expanded = false - }, - text = { Text(text = option) } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = selectedOption, + fontSize = 16.sp, + color = textColor.copy(alpha = 0.8f) + ) + Icon( + Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = textColor.copy(alpha = 0.6f) ) } + + 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 + ) } } } \ No newline at end of file diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt index 36815e2..1dacdf6 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidAdjustmentsScreen.kt @@ -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( + label = stringResource(R.string.amplification).uppercase(), valueRange = -1f..1f, mutableFloatState = amplificationSliderValue, onValueChange = { @@ -374,17 +365,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController 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( + label = stringResource(R.string.balance).uppercase(), valueRange = -1f..1f, mutableFloatState = balanceSliderValue, onValueChange = { @@ -396,17 +378,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController 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( + label = stringResource(R.string.tone).uppercase(), valueRange = -1f..1f, mutableFloatState = toneSliderValue, onValueChange = { @@ -417,18 +390,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController 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( + label = stringResource(R.string.ambient_noise_reduction).uppercase(), valueRange = 0f..1f, mutableFloatState = ambientNoiseReductionSliderValue, onValueChange = { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt index dc2353a..e47e417 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/TransparencySettingsScreen.kt @@ -363,17 +363,8 @@ fun TransparencySettingsScreen(navController: NavController) { description = stringResource(R.string.customize_transparency_mode_description) ) 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( + label = stringResource(R.string.amplification).uppercase(), valueRange = -1f..1f, mutableFloatState = amplificationSliderValue, onValueChange = { @@ -384,17 +375,8 @@ fun TransparencySettingsScreen(navController: NavController) { 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( + label = stringResource(R.string.balance).uppercase(), valueRange = -1f..1f, mutableFloatState = balanceSliderValue, onValueChange = { @@ -406,17 +388,8 @@ fun TransparencySettingsScreen(navController: NavController) { 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( + label = stringResource(R.string.tone).uppercase(), valueRange = -1f..1f, mutableFloatState = toneSliderValue, onValueChange = { @@ -427,18 +400,8 @@ fun TransparencySettingsScreen(navController: NavController) { 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( + label = stringResource(R.string.ambient_noise_reduction).uppercase(), valueRange = 0f..1f, mutableFloatState = ambientNoiseReductionSliderValue, onValueChange = { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt index b8de0e1..fdf84bc 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/TransparencyUtils.kt @@ -25,7 +25,8 @@ data class TransparencySettings( val leftAmbientNoiseReduction: Float, val rightAmbientNoiseReduction: Float, val netAmplification: Float, - val balance: Float + val balance: Float, + val ownVoiceAmplification: Float? = null ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -44,6 +45,7 @@ data class TransparencySettings( if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false if (!leftEQ.contentEquals(other.leftEQ)) return false if (!rightEQ.contentEquals(other.rightEQ)) return false + if (ownVoiceAmplification != other.ownVoiceAmplification) return false return true } @@ -60,6 +62,7 @@ data class TransparencySettings( result = 31 * result + rightAmbientNoiseReduction.hashCode() result = 31 * result + leftEQ.contentHashCode() result = 31 * result + rightEQ.contentHashCode() + result = 31 * result + (ownVoiceAmplification?.hashCode() ?: 0) return result } } @@ -91,6 +94,12 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? { val rightConversationBoost = rightConvFloat > 0.5f val rightAmbientNoiseReduction = buffer.float + val ownVoiceAmplification = if (buffer.remaining() >= 4) { + buffer.float + } else { + null + } + val avg = (leftAmplification + rightAmplification) / 2 val amplification = avg.coerceIn(-1f, 1f) val diff = rightAmplification - leftAmplification @@ -109,7 +118,8 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? { leftAmbientNoiseReduction = leftAmbientNoiseReduction, rightAmbientNoiseReduction = rightAmbientNoiseReduction, netAmplification = amplification, - balance = balance + balance = balance, + ownVoiceAmplification = ownVoiceAmplification ) } @@ -120,7 +130,9 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans debounceJob = CoroutineScope(Dispatchers.IO).launch { delay(100) 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) @@ -140,6 +152,10 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f) buffer.putFloat(transparencySettings.rightAmbientNoiseReduction) + if (transparencySettings.ownVoiceAmplification != null) { + buffer.putFloat(transparencySettings.ownVoiceAmplification) + } + val data = buffer.array() attManager.write(ATTHandles.TRANSPARENCY, value = data) } catch (e: IOException) {