mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-18 00:44:57 +00:00
android: update usages for toggle
This commit is contained in:
@@ -62,8 +62,11 @@ dependencies {
|
|||||||
implementation(libs.haze)
|
implementation(libs.haze)
|
||||||
implementation(libs.haze.materials)
|
implementation(libs.haze.materials)
|
||||||
implementation(libs.androidx.dynamicanimation)
|
implementation(libs.androidx.dynamicanimation)
|
||||||
implementation(libs.androidx.compose.foundation.layout)
|
|
||||||
compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
|
||||||
implementation(fileTree(mapOf("dir" to "lib", "include" to listOf("*.aar"))))
|
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
implementation(libs.androidx.compose.foundation.layout)
|
||||||
|
// compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||||
|
// implementation(fileTree(mapOf("dir" to "lib", "include" to listOf("*.aar"))))
|
||||||
|
compileOnly(files("libs/libxposed-api-100.aar"))
|
||||||
|
debugImplementation(files("libs/backdrop-debug.aar"))
|
||||||
|
releaseImplementation(files("libs/backdrop-release.aar"))
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
android/app/libs/backdrop-debug.aar
Normal file
BIN
android/app/libs/backdrop-debug.aar
Normal file
Binary file not shown.
BIN
android/app/libs/backdrop-release.aar
Normal file
BIN
android/app/libs/backdrop-release.aar
Normal file
Binary file not shown.
@@ -42,6 +42,7 @@ import androidx.navigation.NavController
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -95,7 +96,12 @@ fun AudioSettings(navController: NavController) {
|
|||||||
.padding(start = 12.dp, end = 0.dp)
|
.padding(start = 12.dp, end = 0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
LoudSoundReductionSwitch()
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.loud_sound_reduction),
|
||||||
|
description = stringResource(R.string.loud_sound_reduction_description),
|
||||||
|
attHandle = ATTHandles.LOUD_SOUND_REDUCTION,
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import android.content.Context.MODE_PRIVATE
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AutomaticConnectionSwitch() {
|
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
|
|
||||||
val sharedPreferenceKey = "automatic_connection_ctrl_cmd"
|
|
||||||
|
|
||||||
val automaticConnectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
|
|
||||||
var automaticConnectionEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
if (automaticConnectionEnabledValue != null) {
|
|
||||||
automaticConnectionEnabledValue == 1.toByte()
|
|
||||||
} else {
|
|
||||||
sharedPreferences.getBoolean(sharedPreferenceKey, false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAutomaticConnection(enabled: Boolean) {
|
|
||||||
automaticConnectionEnabled = enabled
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG.value,
|
|
||||||
enabled
|
|
||||||
)
|
|
||||||
// todo: send other connected devices smartAudioRoutingDisabled or something, check packets again.
|
|
||||||
|
|
||||||
sharedPreferences.edit()
|
|
||||||
.putBoolean(sharedPreferenceKey, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
val automaticConnectionListener = object: AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
val enabled = newValue == 1.toByte()
|
|
||||||
automaticConnectionEnabled = enabled
|
|
||||||
|
|
||||||
sharedPreferences.edit()
|
|
||||||
.putBoolean(sharedPreferenceKey, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
|
||||||
automaticConnectionListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
|
||||||
automaticConnectionListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
updateAutomaticConnection(!automaticConnectionEnabled)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.automatically_connect),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.automatically_connect_description),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = automaticConnectionEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
updateAutomaticConnection(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AutomaticConnectionSwitchPreview() {
|
|
||||||
AutomaticConnectionSwitch()
|
|
||||||
}
|
|
||||||
@@ -30,9 +30,15 @@ import androidx.compose.material3.HorizontalDivider
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
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 kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionSettings() {
|
fun ConnectionSettings() {
|
||||||
@@ -45,7 +51,13 @@ fun ConnectionSettings() {
|
|||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
EarDetectionSwitch()
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.ear_detection),
|
||||||
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
||||||
|
sharedPreferenceKey = "automatic_ear_detection",
|
||||||
|
sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE),
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
@@ -53,7 +65,14 @@ fun ConnectionSettings() {
|
|||||||
.padding(start = 12.dp, end = 0.dp)
|
.padding(start = 12.dp, end = 0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
AutomaticConnectionSwitch()
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.automatically_connect),
|
||||||
|
description = stringResource(R.string.automatically_connect_description),
|
||||||
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||||
|
sharedPreferenceKey = "automatic_connection_ctrl_cmd",
|
||||||
|
sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE),
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import android.content.Context.MODE_PRIVATE
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EarDetectionSwitch() {
|
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
|
|
||||||
val sharedPreferenceKey = "automatic_ear_detection"
|
|
||||||
|
|
||||||
val earDetectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
|
|
||||||
var earDetectionEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
if (earDetectionEnabledValue != null) {
|
|
||||||
earDetectionEnabledValue == 1.toByte()
|
|
||||||
} else {
|
|
||||||
sharedPreferences.getBoolean(sharedPreferenceKey, false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateEarDetection(enabled: Boolean) {
|
|
||||||
earDetectionEnabled = enabled
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG.value,
|
|
||||||
enabled
|
|
||||||
)
|
|
||||||
service.setEarDetection(enabled)
|
|
||||||
|
|
||||||
sharedPreferences.edit()
|
|
||||||
.putBoolean(sharedPreferenceKey, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
val earDetectionListener = object: AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
val enabled = newValue == 1.toByte()
|
|
||||||
earDetectionEnabled = enabled
|
|
||||||
|
|
||||||
sharedPreferences.edit()
|
|
||||||
.putBoolean(sharedPreferenceKey, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
|
||||||
earDetectionListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
|
||||||
earDetectionListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
updateEarDetection(!earDetectionEnabled)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.ear_detection),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = earDetectionEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
updateEarDetection(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun EarDetectionSwitchPreview() {
|
|
||||||
EarDetectionSwitch()
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoudSoundReductionSwitch() {
|
|
||||||
var loudSoundReductionEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: return
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
attManager.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
|
|
||||||
|
|
||||||
var parsed = false
|
|
||||||
for (attempt in 1..3) {
|
|
||||||
try {
|
|
||||||
val data = attManager.read(ATTHandles.LOUD_SOUND_REDUCTION)
|
|
||||||
loudSoundReductionEnabled = data[0].toInt() != 0
|
|
||||||
Log.d("LoudSoundReduction", "Read attempt $attempt: enabled=${loudSoundReductionEnabled}")
|
|
||||||
parsed = true
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("LoudSoundReduction", "Read attempt $attempt failed: ${e.message}")
|
|
||||||
}
|
|
||||||
delay(200)
|
|
||||||
}
|
|
||||||
if (!parsed) {
|
|
||||||
Log.d("LoudSoundReduction", "Failed to read loud sound reduction state after 3 attempts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(loudSoundReductionEnabled) {
|
|
||||||
if (attManager.socket?.isConnected != true) return@LaunchedEffect
|
|
||||||
attManager.write(ATTHandles.LOUD_SOUND_REDUCTION, if (loudSoundReductionEnabled) byteArrayOf(1) else byteArrayOf(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
val loudSoundListener = remember {
|
|
||||||
object : (ByteArray) -> Unit {
|
|
||||||
override fun invoke(value: ByteArray) {
|
|
||||||
if (value.isNotEmpty()) {
|
|
||||||
loudSoundReductionEnabled = value[0].toInt() != 0
|
|
||||||
Log.d("LoudSoundReduction", "Updated from notification: enabled=$loudSoundReductionEnabled")
|
|
||||||
} else {
|
|
||||||
Log.w("LoudSoundReduction", "Empty value in notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
attManager.registerListener(ATTHandles.LOUD_SOUND_REDUCTION, loudSoundListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
attManager.unregisterListener(ATTHandles.LOUD_SOUND_REDUCTION, loudSoundListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
loudSoundReductionEnabled = !loudSoundReductionEnabled
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.loud_sound_reduction),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.loud_sound_reduction_description),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = loudSoundReductionEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
loudSoundReductionEnabled = it
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PersonalizedVolumeSwitch() {
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
|
|
||||||
val adaptiveVolumeEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
|
|
||||||
var adaptiveVolumeEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
adaptiveVolumeEnabledValue == 1.toByte()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updatePersonalizedVolume(enabled: Boolean) {
|
|
||||||
adaptiveVolumeEnabled = enabled
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG.value,
|
|
||||||
enabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val adaptiveVolumeListener = object: AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
adaptiveVolumeEnabled = newValue == 1.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
|
||||||
adaptiveVolumeListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
|
||||||
adaptiveVolumeListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
updatePersonalizedVolume(!adaptiveVolumeEnabled)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.personalized_volume),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.personalized_volume_description),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = adaptiveVolumeEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
updatePersonalizedVolume(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun PersonalizedVolumeSwitchPreview() {
|
|
||||||
PersonalizedVolumeSwitch()
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SinglePodANCSwitch() {
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
val singleANCEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var singleANCEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
singleANCEnabledValue == 1.toByte()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val listener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
singleANCEnabled = newValue == 1.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, listener)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSingleEnabled(enabled: Boolean) {
|
|
||||||
singleANCEnabled = enabled
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE.value,
|
|
||||||
enabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
updateSingleEnabled(!singleANCEnabled)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Noise Cancellation with Single AirPod",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = singleANCEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
updateSingleEnabled(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun SinglePodANCSwitchPreview() {
|
|
||||||
SinglePodANCSwitch()
|
|
||||||
}
|
|
||||||
@@ -18,26 +18,58 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.Animatable
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
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.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.BlurEffect
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.TileMode
|
||||||
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
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.util.fastCoerceIn
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import com.kyant.backdrop.drawBackdrop
|
||||||
|
import com.kyant.backdrop.effects.refractionWithDispersion
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSwitch(
|
fun StyledSwitch(
|
||||||
@@ -47,42 +79,172 @@ fun StyledSwitch(
|
|||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
val thumbColor = Color.White
|
val onColor = if (enabled) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
||||||
val trackColor = if (enabled) (
|
val offColor = if (enabled) if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
||||||
if (isDarkTheme) {
|
|
||||||
if (checked) Color(0xFF34C759) else Color(0xFF5B5B5E)
|
val trackWidth = 70.dp
|
||||||
} else {
|
val trackHeight = 31.dp
|
||||||
if (checked) Color(0xFF34C759) else Color(0xFFD1D1D6)
|
val thumbHeight = 27.dp
|
||||||
}
|
val thumbWidth = 36.dp
|
||||||
) else {
|
|
||||||
if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
val switchBackdrop = rememberLayerBackdrop()
|
||||||
|
val fraction by remember {
|
||||||
|
derivedStateOf { if (checked) 1f else 0f }
|
||||||
}
|
}
|
||||||
|
val animatedFraction = remember { Animatable(fraction) }
|
||||||
|
val trackWidthPx = remember { mutableFloatStateOf(0f) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
|
val colorAnimationSpec = tween<Color>(300, easing = FastOutSlowInEasing)
|
||||||
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
|
val innerShadowLayer = rememberGraphicsLayer().apply {
|
||||||
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
|
}
|
||||||
|
val animatedTrackColor = remember { Animatable(if (checked) onColor else offColor) }
|
||||||
|
|
||||||
|
LaunchedEffect(checked) {
|
||||||
val thumbOffsetX by animateDpAsState(targetValue = if (checked) 20.dp else 0.dp, label = "Test")
|
val targetColor = if (checked) onColor else offColor
|
||||||
|
animatedTrackColor.animateTo(targetColor, colorAnimationSpec)
|
||||||
|
val targetFrac = if (checked) 1f else 0f
|
||||||
|
animatedFraction.animateTo(targetFrac, progressAnimationSpec)
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(51.dp)
|
.width(trackWidth)
|
||||||
.height(31.dp)
|
.height(trackHeight),
|
||||||
.clip(RoundedCornerShape(15.dp))
|
|
||||||
.background(trackColor) // Dynamic track background
|
|
||||||
.padding(horizontal = 3.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = thumbOffsetX)
|
.layerBackdrop(switchBackdrop)
|
||||||
.size(27.dp)
|
.clip(RoundedCornerShape(trackHeight / 2))
|
||||||
.clip(CircleShape)
|
.background(animatedTrackColor.value)
|
||||||
.background(thumbColor)
|
.width(trackWidth)
|
||||||
.clickable { if (enabled) onCheckedChange(!checked) }
|
.height(trackHeight)
|
||||||
|
.onSizeChanged { trackWidthPx.floatValue = it.width.toFloat() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
translationX = animatedFraction.value * (trackWidthPx.floatValue - with(density) { thumbWidth.toPx() + 4.dp.toPx() })
|
||||||
|
}
|
||||||
|
.then(if (enabled) Modifier.draggable(
|
||||||
|
rememberDraggableState { delta ->
|
||||||
|
if (trackWidthPx.floatValue > 0f) {
|
||||||
|
val newFraction = (animatedFraction.value + delta / trackWidthPx.floatValue).fastCoerceIn(0f, 1f)
|
||||||
|
animationScope.launch {
|
||||||
|
animatedFraction.snapTo(newFraction)
|
||||||
|
}
|
||||||
|
val newChecked = newFraction >= 0.5f
|
||||||
|
if (newChecked != checked) {
|
||||||
|
onCheckedChange(newChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Orientation.Horizontal,
|
||||||
|
startDragImmediately = true,
|
||||||
|
onDragStarted = {
|
||||||
|
animationScope.launch {
|
||||||
|
progressAnimation.animateTo(1f, progressAnimationSpec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragStopped = {
|
||||||
|
animationScope.launch {
|
||||||
|
progressAnimation.animateTo(0f, progressAnimationSpec)
|
||||||
|
val snappedFraction = if (animatedFraction.value >= 0.5f) 1f else 0f
|
||||||
|
animatedFraction.animateTo(snappedFraction, progressAnimationSpec)
|
||||||
|
onCheckedChange(snappedFraction >= 0.5f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) else Modifier)
|
||||||
|
.drawBackdrop(
|
||||||
|
rememberCombinedBackdrop(backdrop, switchBackdrop),
|
||||||
|
{ RoundedCornerShape(thumbHeight / 2) },
|
||||||
|
highlight = {
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
Highlight.AmbientDefault.copy(alpha = progress)
|
||||||
|
},
|
||||||
|
shadow = {
|
||||||
|
Shadow(
|
||||||
|
radius = 4f.dp,
|
||||||
|
color = Color.Black.copy(0.05f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
layer = {
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
val scale = lerp(1f, 2f, progress)
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
},
|
||||||
|
onDrawSurface = {
|
||||||
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
||||||
|
|
||||||
|
val shape = RoundedCornerShape(thumbHeight / 2)
|
||||||
|
val outline = shape.createOutline(size, layoutDirection, this)
|
||||||
|
val innerShadowOffset = 4f.dp.toPx()
|
||||||
|
val innerShadowBlurRadius = 4f.dp.toPx()
|
||||||
|
|
||||||
|
innerShadowLayer.alpha = progress
|
||||||
|
innerShadowLayer.renderEffect =
|
||||||
|
BlurEffect(
|
||||||
|
innerShadowBlurRadius,
|
||||||
|
innerShadowBlurRadius,
|
||||||
|
TileMode.Decal
|
||||||
|
)
|
||||||
|
innerShadowLayer.record {
|
||||||
|
drawOutline(outline, Color.Black.copy(0.2f))
|
||||||
|
translate(0f, innerShadowOffset) {
|
||||||
|
drawOutline(
|
||||||
|
outline,
|
||||||
|
Color.Transparent,
|
||||||
|
blendMode = BlendMode.Clear
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawLayer(innerShadowLayer)
|
||||||
|
|
||||||
|
drawRect(Color.White.copy(1f - progress))
|
||||||
|
},
|
||||||
|
effects = {
|
||||||
|
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.width(thumbWidth)
|
||||||
|
.height(thumbHeight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSwitchPreview() {
|
fun StyledSwitchPreview() {
|
||||||
StyledSwitch(checked = true, onCheckedChange = {})
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(backgroundColor)
|
||||||
|
.width(100.dp)
|
||||||
|
.height(100.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val checked = remember { mutableStateOf(true) }
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked.value = it
|
||||||
|
},
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(1000)
|
||||||
|
checked.value = false
|
||||||
|
delay(1000)
|
||||||
|
checked.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,11 @@ 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 androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.R
|
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 me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -72,7 +74,8 @@ fun StyledToggle(
|
|||||||
checkedState: MutableState<Boolean> = remember { mutableStateOf(false) } ,
|
checkedState: MutableState<Boolean> = remember { mutableStateOf(false) } ,
|
||||||
sharedPreferenceKey: String? = null,
|
sharedPreferenceKey: String? = null,
|
||||||
sharedPreferences: SharedPreferences? = null,
|
sharedPreferences: SharedPreferences? = null,
|
||||||
independent: Boolean = true
|
independent: Boolean = true,
|
||||||
|
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
@@ -88,6 +91,7 @@ fun StyledToggle(
|
|||||||
}
|
}
|
||||||
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
||||||
}
|
}
|
||||||
|
onCheckedChange?.invoke(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (independent) {
|
if (independent) {
|
||||||
@@ -100,7 +104,7 @@ fun StyledToggle(
|
|||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f)
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
modifier = Modifier.padding(8.dp, bottom = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -232,7 +236,10 @@ fun StyledToggle(
|
|||||||
label: String,
|
label: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers,
|
controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers,
|
||||||
independent: Boolean = true
|
independent: Boolean = true,
|
||||||
|
sharedPreferenceKey: String? = null,
|
||||||
|
sharedPreferences: SharedPreferences? = null,
|
||||||
|
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val service = ServiceManager.getService() ?: return
|
val service = ServiceManager.getService() ?: return
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
@@ -243,9 +250,19 @@ fun StyledToggle(
|
|||||||
var checked by remember { mutableStateOf(checkedValue == 1.toByte()) }
|
var checked by remember { mutableStateOf(checkedValue == 1.toByte()) }
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
if (sharedPreferenceKey != null && sharedPreferences != null) {
|
||||||
|
checked = sharedPreferences.getBoolean(sharedPreferenceKey, checked)
|
||||||
|
}
|
||||||
fun cb() {
|
fun cb() {
|
||||||
service.aacpManager.sendControlCommand(identifier = controlCommandIdentifier.value, value = checked)
|
service.aacpManager.sendControlCommand(identifier = controlCommandIdentifier.value, value = checked)
|
||||||
|
if (sharedPreferences != null) {
|
||||||
|
if (sharedPreferenceKey == null) {
|
||||||
|
Log.e("StyledToggle", "SharedPreferenceKey is null but SharedPreferences is provided.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
||||||
|
}
|
||||||
|
onCheckedChange?.invoke(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
val listener = remember {
|
val listener = remember {
|
||||||
@@ -277,7 +294,225 @@ fun StyledToggle(
|
|||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f)
|
||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
modifier = Modifier.padding(8.dp, bottom = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
checked = !checked
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(55.dp)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (description != null) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed.value = true
|
||||||
|
tryAwaitRelease()
|
||||||
|
isPressed.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
checked = !checked
|
||||||
|
cb()
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
if (description != null) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledToggle(
|
||||||
|
title: String? = null,
|
||||||
|
label: String,
|
||||||
|
description: String? = null,
|
||||||
|
attHandle: ATTHandles,
|
||||||
|
independent: Boolean = true,
|
||||||
|
sharedPreferenceKey: String? = null,
|
||||||
|
sharedPreferences: SharedPreferences? = null,
|
||||||
|
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val attManager = ServiceManager.getService()?.attManager ?: return
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
var checked by remember { mutableStateOf(false) }
|
||||||
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
attManager.enableNotifications(attHandle)
|
||||||
|
|
||||||
|
var parsed = false
|
||||||
|
for (attempt in 1..3) {
|
||||||
|
try {
|
||||||
|
val data = attManager.read(attHandle)
|
||||||
|
checked = data[0].toInt() != 0
|
||||||
|
Log.d("StyledToggle", "Read attempt $attempt for $label: enabled=$checked")
|
||||||
|
parsed = true
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("StyledToggle", "Read attempt $attempt for $label failed: ${e.message}")
|
||||||
|
}
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
|
if (!parsed) {
|
||||||
|
Log.d("StyledToggle", "Failed to read state for $label after 3 attempts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharedPreferenceKey != null && sharedPreferences != null) {
|
||||||
|
checked = sharedPreferences.getBoolean(sharedPreferenceKey, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cb() {
|
||||||
|
if (sharedPreferences != null) {
|
||||||
|
if (sharedPreferenceKey == null) {
|
||||||
|
Log.e("StyledToggle", "SharedPreferenceKey is null but SharedPreferences is provided.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
||||||
|
}
|
||||||
|
onCheckedChange?.invoke(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(checked) {
|
||||||
|
if (attManager.socket?.isConnected != true) return@LaunchedEffect
|
||||||
|
attManager.write(attHandle, if (checked) byteArrayOf(1) else byteArrayOf(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
val listener = remember {
|
||||||
|
object : (ByteArray) -> Unit {
|
||||||
|
override fun invoke(value: ByteArray) {
|
||||||
|
if (value.isNotEmpty()) {
|
||||||
|
checked = value[0].toInt() != 0
|
||||||
|
Log.d("StyledToggle", "Updated from notification for $label: enabled=$checked")
|
||||||
|
} else {
|
||||||
|
Log.w("StyledToggle", "Empty value in notification for $label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
attManager.registerListener(attHandle, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
attManager.unregisterListener(attHandle, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (independent) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
if (title != null) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
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.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VolumeControlSwitch() {
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
val volumeControlEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var volumeControlEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
volumeControlEnabledValue == 1.toByte()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val listener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
volumeControlEnabled = newValue == 1.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, listener)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun updateVolumeControlEnabled(enabled: Boolean) {
|
|
||||||
volumeControlEnabled = enabled
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_MODE.value,
|
|
||||||
enabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
updateVolumeControlEnabled(!volumeControlEnabled)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Volume Control",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = volumeControlEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
updateVolumeControlEnabled(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun VolumeControlSwitchPreview() {
|
|
||||||
VolumeControlSwitch()
|
|
||||||
}
|
|
||||||
@@ -62,6 +62,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -87,17 +88,15 @@ 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.LoudSoundReductionSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.composables.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.StyledDropdown
|
import me.kavishdevar.librepods.composables.StyledDropdown
|
||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
|
||||||
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 me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -334,8 +333,67 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DropdownMenuComponent(
|
||||||
|
label = stringResource(R.string.press_speed),
|
||||||
|
description = stringResource(R.string.press_speed_description),
|
||||||
|
options = pressSpeedOptions.values.toList(),
|
||||||
|
selectedOption = selectedPressSpeed?: "Default",
|
||||||
|
onOptionSelected = { newValue ->
|
||||||
|
selectedPressSpeed = newValue
|
||||||
|
aacpManager?.sendControlCommand(
|
||||||
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value,
|
||||||
|
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
||||||
|
?: 0.toByte()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textColor = textColor,
|
||||||
|
hazeState = hazeState,
|
||||||
|
independent = true
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenuComponent(
|
||||||
|
label = stringResource(R.string.press_and_hold_duration),
|
||||||
|
description = stringResource(R.string.press_and_hold_duration_description),
|
||||||
|
options = pressAndHoldDurationOptions.values.toList(),
|
||||||
|
selectedOption = selectedPressAndHoldDuration?: "Default",
|
||||||
|
onOptionSelected = { newValue ->
|
||||||
|
selectedPressAndHoldDuration = newValue
|
||||||
|
aacpManager?.sendControlCommand(
|
||||||
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value,
|
||||||
|
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull()
|
||||||
|
?: 0.toByte()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textColor = textColor,
|
||||||
|
hazeState = hazeState,
|
||||||
|
independent = true
|
||||||
|
)
|
||||||
|
|
||||||
|
StyledToggle(
|
||||||
|
title = stringResource(R.string.noise_control).uppercase(),
|
||||||
|
label = stringResource(R.string.noise_cancellation_single_airpod),
|
||||||
|
description = stringResource(R.string.noise_cancellation_single_airpod_description),
|
||||||
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ONE_BUD_ANC_MODE,
|
||||||
|
independent = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.loud_sound_reduction),
|
||||||
|
description = stringResource(R.string.loud_sound_reduction_description),
|
||||||
|
attHandle = ATTHandles.LOUD_SOUND_REDUCTION
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
|
||||||
|
NavigationButton(
|
||||||
|
to = "transparency_customization",
|
||||||
|
name = stringResource(R.string.customize_transparency_mode),
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
StyledSlider(
|
StyledSlider(
|
||||||
label = stringResource(R.string.tone_volume).uppercase(),
|
label = stringResource(R.string.tone_volume).uppercase(),
|
||||||
|
description = stringResource(R.string.tone_volume_description),
|
||||||
mutableFloatState = toneVolumeValue,
|
mutableFloatState = toneVolumeValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
toneVolumeValue.floatValue = it
|
toneVolumeValue.floatValue = it
|
||||||
@@ -347,114 +405,25 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
independent = true
|
independent = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
DropdownMenuComponent(
|
||||||
modifier = Modifier
|
label = stringResource(R.string.volume_swipe_speed),
|
||||||
.fillMaxWidth()
|
description = stringResource(R.string.volume_swipe_speed_description),
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
options = volumeSwipeSpeedOptions.values.toList(),
|
||||||
.padding(top = 2.dp),
|
selectedOption = selectedVolumeSwipeSpeed?: "Default",
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
onOptionSelected = { newValue ->
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
selectedVolumeSwipeSpeed = newValue
|
||||||
) {
|
aacpManager?.sendControlCommand(
|
||||||
SinglePodANCSwitch()
|
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
|
||||||
HorizontalDivider(
|
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
||||||
thickness = 1.5.dp,
|
?: 1.toByte()
|
||||||
color = Color(0x40888888),
|
)
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
},
|
||||||
)
|
textColor = textColor,
|
||||||
|
hazeState = hazeState,
|
||||||
VolumeControlSwitch()
|
independent = true
|
||||||
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),
|
|
||||||
options = listOf(
|
|
||||||
stringResource(R.string.default_option),
|
|
||||||
stringResource(R.string.slower),
|
|
||||||
stringResource(R.string.slowest)
|
|
||||||
),
|
|
||||||
selectedOption = selectedPressSpeed.toString(),
|
|
||||||
onOptionSelected = { newValue ->
|
|
||||||
selectedPressSpeed = newValue
|
|
||||||
aacpManager?.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL.value,
|
|
||||||
value = pressSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
|
||||||
?: 0.toByte()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
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(
|
|
||||||
stringResource(R.string.default_option),
|
|
||||||
stringResource(R.string.slower),
|
|
||||||
stringResource(R.string.slowest)
|
|
||||||
),
|
|
||||||
selectedOption = selectedPressAndHoldDuration.toString(),
|
|
||||||
onOptionSelected = { newValue ->
|
|
||||||
selectedPressAndHoldDuration = newValue
|
|
||||||
aacpManager?.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.CLICK_HOLD_INTERVAL.value,
|
|
||||||
value = pressAndHoldDurationOptions.filterValues { it == newValue }.keys.firstOrNull()
|
|
||||||
?: 0.toByte()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
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(
|
|
||||||
stringResource(R.string.default_option),
|
|
||||||
stringResource(R.string.longer),
|
|
||||||
stringResource(R.string.longest)
|
|
||||||
),
|
|
||||||
selectedOption = selectedVolumeSwipeSpeed.toString(),
|
|
||||||
onOptionSelected = { newValue ->
|
|
||||||
selectedVolumeSwipeSpeed = newValue
|
|
||||||
aacpManager?.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.VOLUME_SWIPE_INTERVAL.value,
|
|
||||||
value = volumeSwipeSpeedOptions.filterValues { it == newValue }.keys.firstOrNull()
|
|
||||||
?: 1.toByte()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
textColor = textColor,
|
|
||||||
hazeState = hazeState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
|
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
|
||||||
NavigationButton(
|
|
||||||
to = "transparency_customization",
|
|
||||||
name = stringResource(R.string.customize_transparency_mode),
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.apply_eq_to).uppercase(),
|
text = stringResource(R.string.apply_eq_to).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -680,113 +649,6 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AccessibilityToggle(
|
|
||||||
text: String,
|
|
||||||
mutableState: MutableState<Boolean>,
|
|
||||||
independent: Boolean = false,
|
|
||||||
description: String? = null,
|
|
||||||
title: String? = null
|
|
||||||
) {
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
var backgroundColor by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(
|
|
||||||
0xFFFFFFFF
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val animatedBackgroundColor by animateColorAsState(
|
|
||||||
targetValue = backgroundColor,
|
|
||||||
animationSpec = tween(durationMillis = 500)
|
|
||||||
)
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val cornerShape = if (independent) RoundedCornerShape(14.dp) else RoundedCornerShape(0.dp)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
if (title != null) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
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 = 2.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(animatedBackgroundColor, cornerShape)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
backgroundColor =
|
|
||||||
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
|
||||||
tryAwaitRelease()
|
|
||||||
backgroundColor =
|
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
mutableState.value = !mutableState.value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
{
|
|
||||||
val rowHeight = if (independent) 55.dp else 50.dp
|
|
||||||
val rowPadding = if (independent) 12.dp else 4.dp
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(rowHeight)
|
|
||||||
.padding(horizontal = rowPadding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
StyledSwitch(
|
|
||||||
checked = mutableState.value,
|
|
||||||
onCheckedChange = {
|
|
||||||
mutableState.value = it
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Box ( // for some reason, haze and backdrop don't work for uncontained text
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7), cornerShape)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
// modifier = Modifier
|
|
||||||
// .padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
private fun DropdownMenuComponent(
|
private fun DropdownMenuComponent(
|
||||||
@@ -797,6 +659,7 @@ private fun DropdownMenuComponent(
|
|||||||
textColor: Color,
|
textColor: Color,
|
||||||
hazeState: HazeState,
|
hazeState: HazeState,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
|
independent: Boolean = true
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val itemHeightPx = with(density) { 48.dp.toPx() }
|
val itemHeightPx = with(density) { 48.dp.toPx() }
|
||||||
@@ -808,125 +671,164 @@ private fun DropdownMenuComponent(
|
|||||||
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
var parentDragActive by remember { mutableStateOf(false) }
|
var parentDragActive by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Row(
|
Column(modifier = Modifier.fillMaxWidth()){
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxWidth()
|
|
||||||
.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,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor,
|
|
||||||
modifier = Modifier.padding(bottom = 4.dp)
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
|
||||||
boxPosition = coordinates.positionInParent()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp)
|
.then(
|
||||||
.background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7))
|
if (independent) {
|
||||||
){
|
if (description != null) {
|
||||||
Text(
|
Modifier.padding(top = 8.dp, bottom = 4.dp)
|
||||||
text = description ?: "",
|
} else {
|
||||||
style = TextStyle(
|
Modifier.padding(vertical = 8.dp)
|
||||||
fontSize = 12.sp,
|
}
|
||||||
fontWeight = FontWeight.Light,
|
} else Modifier
|
||||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
)
|
||||||
)
|
.background(
|
||||||
|
if (independent) (if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) else Color.Transparent,
|
||||||
|
if (independent) RoundedCornerShape(14.dp) else RoundedCornerShape(0.dp)
|
||||||
|
)
|
||||||
|
.clip(if (independent) RoundedCornerShape(14.dp) else RoundedCornerShape(0.dp))
|
||||||
|
){
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.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
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
if (!independent && description != null){
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||||
|
boxPosition = coordinates.positionInParent()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (independent && description != null){
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
label = "Head Gestures",
|
label = "Head Gestures",
|
||||||
sharedPreferences = sharedPreferences,
|
sharedPreferences = sharedPreferences,
|
||||||
sharedPreferenceKey = "head_gestures",
|
sharedPreferenceKey = "head_gestures",
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.head_gestures_details),
|
stringResource(R.string.head_gestures_details),
|
||||||
|
|||||||
@@ -337,9 +337,9 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
independent = true,
|
independent = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
AccessibilityToggle(
|
StyledToggle(
|
||||||
text = stringResource(R.string.conversation_boost),
|
label = stringResource(R.string.conversation_boost),
|
||||||
mutableState = conversationBoostEnabled,
|
checkedState = conversationBoostEnabled,
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.conversation_boost_description)
|
description = stringResource(R.string.conversation_boost_description)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -246,13 +246,13 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
AccessibilityToggle(
|
StyledToggle(
|
||||||
text = stringResource(R.string.media_assist),
|
title = stringResource(R.string.media_assist).uppercase(),
|
||||||
mutableState = mediaAssistEnabled,
|
label = stringResource(R.string.media_assist),
|
||||||
|
checkedState = mediaAssistEnabled,
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.media_assist_description),
|
description = stringResource(R.string.media_assist_description)
|
||||||
title = stringResource(R.string.media_assist).uppercase()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -377,10 +377,8 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
||||||
val parsed = parseTransparencySettingsResponse(data)
|
val parsed = parseTransparencySettingsResponse(data)
|
||||||
if (parsed != null) {
|
val disabledSettings = parsed.copy(enabled = false)
|
||||||
val disabledSettings = parsed.copy(enabled = false)
|
sendTransparencySettings(attManager, disabledSettings)
|
||||||
sendTransparencySettings(attManager, disabledSettings)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error disabling transparency: ${e.message}")
|
Log.e(TAG, "Error disabling transparency: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import me.kavishdevar.librepods.R
|
|||||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
@@ -288,9 +289,9 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
// Only show transparency mode section if SDP offset is available
|
// Only show transparency mode section if SDP offset is available
|
||||||
if (isSdpOffsetAvailable.value) {
|
if (isSdpOffsetAvailable.value) {
|
||||||
AccessibilityToggle(
|
StyledToggle(
|
||||||
text = stringResource(R.string.transparency_mode),
|
label = stringResource(R.string.transparency_mode),
|
||||||
mutableState = enabled,
|
checkedState = enabled,
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.customize_transparency_mode_description)
|
description = stringResource(R.string.customize_transparency_mode_description)
|
||||||
)
|
)
|
||||||
@@ -344,9 +345,9 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
independent = true,
|
independent = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
AccessibilityToggle(
|
StyledToggle(
|
||||||
text = stringResource(R.string.conversation_boost),
|
label = stringResource(R.string.conversation_boost),
|
||||||
mutableState = conversationBoostEnabled,
|
checkedState = conversationBoostEnabled,
|
||||||
independent = true,
|
independent = true,
|
||||||
description = stringResource(R.string.conversation_boost_description)
|
description = stringResource(R.string.conversation_boost_description)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
|
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
|
||||||
<string name="accessibility">Accessibility</string>
|
<string name="accessibility">Accessibility</string>
|
||||||
<string name="tone_volume">Tone Volume</string>
|
<string name="tone_volume">Tone Volume</string>
|
||||||
|
<string name="tone_volume_description">Adjust the tone volume of sound effects played by AirPods.</string>
|
||||||
<string name="audio">Audio</string>
|
<string name="audio">Audio</string>
|
||||||
<string name="adaptive_audio">Adaptive Audio</string>
|
<string name="adaptive_audio">Adaptive Audio</string>
|
||||||
<string name="customize_adaptive_audio">Customize Adaptive Audio</string>
|
<string name="customize_adaptive_audio">Customize Adaptive Audio</string>
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
<string name="personalized_volume">Personalized Volume</string>
|
<string name="personalized_volume">Personalized Volume</string>
|
||||||
<string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string>
|
<string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string>
|
||||||
<string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string>
|
<string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string>
|
||||||
<string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.</string>
|
<string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPod is in your ear.</string>
|
||||||
<string name="volume_control">Volume Control</string>
|
<string name="volume_control">Volume Control</string>
|
||||||
<string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string>
|
<string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string>
|
||||||
<string name="airpods_not_connected">AirPods not connected</string>
|
<string name="airpods_not_connected">AirPods not connected</string>
|
||||||
@@ -107,8 +108,11 @@
|
|||||||
<string name="transparency_mode">Transparency Mode</string>
|
<string name="transparency_mode">Transparency Mode</string>
|
||||||
<string name="customize_transparency_mode">Customize Transparency Mode</string>
|
<string name="customize_transparency_mode">Customize Transparency Mode</string>
|
||||||
<string name="press_speed">Press Speed</string>
|
<string name="press_speed">Press Speed</string>
|
||||||
|
<string name="press_speed_description">Adjust the speed required to press two or three times on your AirPods.</string>
|
||||||
<string name="press_and_hold_duration">Press and Hold Duration</string>
|
<string name="press_and_hold_duration">Press and Hold Duration</string>
|
||||||
|
<string name="press_and_hold_duration_description">Adjust the duration required to press and hold on your AirPods</string>
|
||||||
<string name="volume_swipe_speed">Volume Swipe Speed</string>
|
<string name="volume_swipe_speed">Volume Swipe Speed</string>
|
||||||
|
<string name="volume_swipe_speed_description">To prevent unintended volume adjustments, select preferred wait time between swipes.</string>
|
||||||
<string name="equalizer">Equalizer</string>
|
<string name="equalizer">Equalizer</string>
|
||||||
<string name="apply_eq_to">Apply EQ to</string>
|
<string name="apply_eq_to">Apply EQ to</string>
|
||||||
<string name="phone">Phone</string>
|
<string name="phone">Phone</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user