mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
android: liquidglass sliders
This commit is contained in:
@@ -62,5 +62,8 @@ 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"))))
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"
|
||||||
|
tools:ignore="ForegroundServicesPolicy" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
@@ -32,6 +33,8 @@
|
|||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
android:maxSdkVersion="30" />
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS"
|
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|
||||||
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
|
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
|
||||||
|
|
||||||
@ExperimentalHazeMaterialsApi
|
|
||||||
class CustomDevice : ComponentActivity() {
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContent {
|
|
||||||
LibrePodsTheme {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "main") {
|
|
||||||
composable("main") {
|
|
||||||
AccessibilitySettingsScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -108,12 +108,14 @@ import com.google.accompanist.permissions.isGranted
|
|||||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||||
|
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.AppSettingsScreen
|
import me.kavishdevar.librepods.screens.AppSettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.DebugScreen
|
import me.kavishdevar.librepods.screens.DebugScreen
|
||||||
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
||||||
import me.kavishdevar.librepods.screens.HearingAidScreen
|
import me.kavishdevar.librepods.screens.HearingAidScreen
|
||||||
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
|
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
|
||||||
|
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.LongPress
|
import me.kavishdevar.librepods.screens.LongPress
|
||||||
import me.kavishdevar.librepods.screens.Onboarding
|
import me.kavishdevar.librepods.screens.Onboarding
|
||||||
import me.kavishdevar.librepods.screens.RenameScreen
|
import me.kavishdevar.librepods.screens.RenameScreen
|
||||||
@@ -382,6 +384,12 @@ fun Main() {
|
|||||||
composable("onboarding") {
|
composable("onboarding") {
|
||||||
Onboarding(navController, context)
|
Onboarding(navController, context)
|
||||||
}
|
}
|
||||||
|
composable("accessibility") {
|
||||||
|
AccessibilitySettingsScreen(navController)
|
||||||
|
}
|
||||||
|
composable("transparency_customization") {
|
||||||
|
TransparencySettingsScreen(navController)
|
||||||
|
}
|
||||||
composable("hearing_aid") {
|
composable("hearing_aid") {
|
||||||
HearingAidScreen(navController)
|
HearingAidScreen(navController)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.Font
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun AccessibilitySlider(
|
|
||||||
label: String? = null,
|
|
||||||
value: Float,
|
|
||||||
onValueChange: (Float) -> Unit,
|
|
||||||
valueRange: ClosedFloatingPointRange<Float>,
|
|
||||||
widthFrac: Float = 1f
|
|
||||||
) {
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(widthFrac),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
if (label != null) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = androidx.compose.ui.text.font.FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Slider(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
valueRange = valueRange,
|
|
||||||
onValueChangeFinished = {
|
|
||||||
// Round to 2 decimal places
|
|
||||||
onValueChange((value * 100).roundToInt() / 100f)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(36.dp),
|
|
||||||
colors = SliderDefaults.colors(
|
|
||||||
thumbColor = thumbColor,
|
|
||||||
activeTrackColor = activeTrackColor,
|
|
||||||
inactiveTrackColor = trackColor
|
|
||||||
),
|
|
||||||
thumb = {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.shadow(4.dp, CircleShape)
|
|
||||||
.background(thumbColor, CircleShape)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
track = {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(12.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(4.dp)
|
|
||||||
.background(trackColor, RoundedCornerShape(4.dp))
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth((value - valueRange.start) / (valueRange.endInclusive - valueRange.start))
|
|
||||||
.height(4.dp)
|
|
||||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AccessibilitySliderPreview() {
|
|
||||||
AccessibilitySlider(
|
|
||||||
label = "Test Slider",
|
|
||||||
value = 1.0f,
|
|
||||||
onValueChange = {},
|
|
||||||
valueRange = 0f..2f
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -61,13 +61,16 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
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 kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
fun CallControlSettings() {
|
fun CallControlSettings(hazeState: HazeState) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
@@ -306,7 +309,8 @@ fun CallControlSettings() {
|
|||||||
0x03
|
0x03
|
||||||
) else byteArrayOf(0x00, 0x02)
|
) else byteArrayOf(0x00, 0x02)
|
||||||
service.aacpManager.sendControlCommand(0x24, bytes)
|
service.aacpManager.sendControlCommand(0x24, bytes)
|
||||||
}
|
},
|
||||||
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,7 +437,8 @@ fun CallControlSettings() {
|
|||||||
0x02
|
0x02
|
||||||
) else byteArrayOf(0x00, 0x03)
|
) else byteArrayOf(0x00, 0x03)
|
||||||
service.aacpManager.sendControlCommand(0x24, bytes)
|
service.aacpManager.sendControlCommand(0x24, bytes)
|
||||||
}
|
},
|
||||||
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,8 +446,9 @@ fun CallControlSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun CallControlSettingsPreview() {
|
fun CallControlSettingsPreview() {
|
||||||
CallControlSettings()
|
CallControlSettings(HazeState())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidthIn
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -25,7 +27,6 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -51,18 +52,24 @@ fun ConfirmationDialog(
|
|||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit = { showDialog.value = false },
|
onDismiss: () -> Unit = { showDialog.value = false },
|
||||||
hazeState: HazeState,
|
hazeState: HazeState,
|
||||||
isDarkTheme: Boolean,
|
|
||||||
textColor: Color,
|
|
||||||
activeTrackColor: Color
|
|
||||||
) {
|
) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val accentColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
if (showDialog.value) {
|
if (showDialog.value) {
|
||||||
Dialog(onDismissRequest = { showDialog.value = false }) {
|
Dialog(onDismissRequest = { showDialog.value = false }) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(0.75f)
|
||||||
.background(if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f), RoundedCornerShape(14.dp))
|
.requiredWidthIn(min = 200.dp, max = 360.dp)
|
||||||
|
.background(Color.Transparent, RoundedCornerShape(14.dp))
|
||||||
.clip(RoundedCornerShape(14.dp))
|
.clip(RoundedCornerShape(14.dp))
|
||||||
.hazeEffect(hazeState, CupertinoMaterials.regular())
|
.hazeEffect(
|
||||||
|
hazeState,
|
||||||
|
style = CupertinoMaterials.regular(
|
||||||
|
containerColor = if (isDarkTheme) Color(0xFF1C1C1E).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f)
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp))
|
androidx.compose.foundation.layout.Spacer(modifier = Modifier.height(24.dp))
|
||||||
@@ -155,7 +162,7 @@ fun ConfirmationDialog(
|
|||||||
.background(if (leftPressed) pressedColor else Color.Transparent),
|
.background(if (leftPressed) pressedColor else Color.Transparent),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(dismissText, color = activeTrackColor)
|
Text(dismissText, color = accentColor)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -170,7 +177,7 @@ fun ConfirmationDialog(
|
|||||||
.background(if (rightPressed) pressedColor else Color.Transparent),
|
.background(if (rightPressed) pressedColor else Color.Transparent),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(confirmText, color = activeTrackColor)
|
Text(confirmText, color = accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -70,19 +71,29 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.layout.positionInParent
|
import androidx.compose.ui.layout.positionInParent
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
|
import dev.chrisbanes.haze.HazeEffectScope
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.HazeTint
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
fun MicrophoneSettings() {
|
fun MicrophoneSettings(hazeState: HazeState) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
@@ -287,19 +298,22 @@ fun MicrophoneSettings() {
|
|||||||
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
|
AACPManager.Companion.ControlCommandIdentifiers.MIC_MODE.value,
|
||||||
byteArrayOf(byteValue.toByte())
|
byteArrayOf(byteValue.toByte())
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun MicrophoneSettingsPreview() {
|
fun MicrophoneSettingsPreview() {
|
||||||
MicrophoneSettings()
|
MicrophoneSettings(HazeState())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
fun DragSelectableDropdown(
|
fun DragSelectableDropdown(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
@@ -311,7 +325,8 @@ fun DragSelectableDropdown(
|
|||||||
onOptionSelected: (String) -> Unit,
|
onOptionSelected: (String) -> Unit,
|
||||||
externalHoveredIndex: Int? = null,
|
externalHoveredIndex: Int? = null,
|
||||||
externalDragActive: Boolean = false,
|
externalDragActive: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
hazeState: HazeState,
|
||||||
|
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
val relativeOffset = touchOffset?.let { it - boxPosition } ?: Offset.Zero
|
val relativeOffset = touchOffset?.let { it - boxPosition } ?: Offset.Zero
|
||||||
@@ -328,9 +343,7 @@ fun DragSelectableDropdown(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.width(300.dp)
|
.width(300.dp)
|
||||||
.background(
|
.background(Color.Transparent)
|
||||||
if (isSystemInDarkTheme()) Color(0xFF2C2C2E) else Color(0xFFFFFFFF)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
) {
|
) {
|
||||||
@@ -389,14 +402,13 @@ fun DragSelectableDropdown(
|
|||||||
} else {
|
} else {
|
||||||
index == hoveredIndex
|
index == hoveredIndex
|
||||||
}
|
}
|
||||||
|
val isSystemInDarkTheme = isSystemInDarkTheme()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(itemHeight)
|
.height(itemHeight)
|
||||||
.background(
|
.background(
|
||||||
if (isHovered) (if (isSystemInDarkTheme()) Color(0xFF3A3A3C) else Color(
|
Color.Transparent
|
||||||
0xFFD1D1D6
|
|
||||||
)) else Color.Transparent
|
|
||||||
)
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
@@ -405,6 +417,22 @@ fun DragSelectableDropdown(
|
|||||||
onOptionSelected(text)
|
onOptionSelected(text)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}
|
}
|
||||||
|
.hazeEffect(
|
||||||
|
state = hazeState,
|
||||||
|
style = CupertinoMaterials.regular(),
|
||||||
|
block = fun HazeEffectScope.() {
|
||||||
|
alpha = 1f
|
||||||
|
backgroundColor = if (isSystemInDarkTheme) {
|
||||||
|
Color(0xB02C2C2E)
|
||||||
|
} else {
|
||||||
|
Color(0xB0FFFFFF)
|
||||||
|
}
|
||||||
|
tints = if (isHovered) listOf(
|
||||||
|
HazeTint(
|
||||||
|
color = if (isSystemInDarkTheme) Color(0x338A8A8A) else Color(0x40D9D9D9)
|
||||||
|
)
|
||||||
|
) else listOf()
|
||||||
|
})
|
||||||
.padding(horizontal = 12.dp),
|
.padding(horizontal = 12.dp),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
@@ -415,7 +443,11 @@ fun DragSelectableDropdown(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.75f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = text == selectedOption,
|
checked = text == selectedOption,
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableFloatState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.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.layout
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
|
import androidx.compose.ui.util.fastRoundToInt
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import com.kyant.backdrop.Backdrop
|
||||||
|
import com.kyant.backdrop.backdrop
|
||||||
|
import com.kyant.backdrop.drawBackdrop
|
||||||
|
import com.kyant.backdrop.effects.refractionWithDispersion
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
|
import com.kyant.backdrop.rememberBackdrop
|
||||||
|
import com.kyant.backdrop.rememberCombinedBackdropDrawer
|
||||||
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledSlider(
|
||||||
|
label: String? = null,
|
||||||
|
mutableFloatState: MutableFloatState,
|
||||||
|
onValueChange: (Float) -> Unit,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
|
backdrop: Backdrop = rememberBackdrop(),
|
||||||
|
snapPoints: List<Float> = emptyList(),
|
||||||
|
snapThreshold: Float = 0.05f,
|
||||||
|
startIcon: String? = null,
|
||||||
|
endIcon: String? = null,
|
||||||
|
startLabel: String? = null,
|
||||||
|
endLabel: String? = null,
|
||||||
|
independent: Boolean = false,
|
||||||
|
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
val isLightTheme = !isSystemInDarkTheme()
|
||||||
|
val accentColor =
|
||||||
|
if (isLightTheme) Color(0xFF0088FF)
|
||||||
|
else Color(0xFF0091FF)
|
||||||
|
val trackColor =
|
||||||
|
if (isLightTheme) Color(0xFF787878).copy(0.2f)
|
||||||
|
else Color(0xFF787880).copy(0.36f)
|
||||||
|
val labelTextColor = if (isLightTheme) Color.Black else Color.White
|
||||||
|
|
||||||
|
val fraction by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
((mutableFloatState.floatValue - valueRange.start) / (valueRange.endInclusive - valueRange.start))
|
||||||
|
.fastCoerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
val trackBackdrop = rememberBackdrop()
|
||||||
|
val innerShadowLayer =
|
||||||
|
rememberGraphicsLayer().apply {
|
||||||
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
|
}
|
||||||
|
|
||||||
|
val content = @Composable {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(1f).padding(vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
if (label != null) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = labelTextColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startLabel != null || endLabel != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = startLabel ?: "",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = labelTextColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = endLabel ?: "",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = labelTextColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
if (startIcon != null) {
|
||||||
|
Text(
|
||||||
|
text = startIcon,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = labelTextColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BoxWithConstraints(
|
||||||
|
Modifier
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val trackWidth = constraints.maxWidth
|
||||||
|
|
||||||
|
Box(Modifier.backdrop(trackBackdrop)) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
.background(trackColor)
|
||||||
|
.height(6f.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(28.dp))
|
||||||
|
.background(accentColor)
|
||||||
|
.height(6f.dp)
|
||||||
|
.layout { measurable, constraints ->
|
||||||
|
val placeable = measurable.measure(constraints)
|
||||||
|
val fraction = fraction
|
||||||
|
val width = (fraction * constraints.maxWidth).fastRoundToInt()
|
||||||
|
layout(width, placeable.height) {
|
||||||
|
placeable.place(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
val fraction = fraction
|
||||||
|
translationX =
|
||||||
|
(-size.width / 2f + fraction * trackWidth)
|
||||||
|
.fastCoerceIn(
|
||||||
|
-size.width / 4f,
|
||||||
|
trackWidth - size.width * 3f / 4f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.draggable(
|
||||||
|
rememberDraggableState { delta ->
|
||||||
|
val trackWidth = trackWidth - with(density) { 40f.dp.toPx() }
|
||||||
|
val targetFraction = fraction + delta / trackWidth
|
||||||
|
val targetValue =
|
||||||
|
lerp(valueRange.start, valueRange.endInclusive, targetFraction)
|
||||||
|
.fastCoerceIn(valueRange.start, valueRange.endInclusive)
|
||||||
|
val snappedValue = if (snapPoints.isNotEmpty()) snapIfClose(
|
||||||
|
targetValue,
|
||||||
|
snapPoints,
|
||||||
|
snapThreshold
|
||||||
|
) else targetValue
|
||||||
|
onValueChange(snappedValue)
|
||||||
|
},
|
||||||
|
Orientation.Horizontal,
|
||||||
|
startDragImmediately = true,
|
||||||
|
onDragStarted = {
|
||||||
|
animationScope.launch {
|
||||||
|
progressAnimation.animateTo(1f, progressAnimationSpec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragStopped = {
|
||||||
|
animationScope.launch {
|
||||||
|
progressAnimation.animateTo(0f, progressAnimationSpec)
|
||||||
|
onValueChange((mutableFloatState.floatValue * 100).roundToInt() / 100f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.drawBackdrop(
|
||||||
|
rememberCombinedBackdropDrawer(backdrop, trackBackdrop),
|
||||||
|
{ RoundedCornerShape(28.dp) },
|
||||||
|
highlight = {
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
Highlight.AmbientDefault.copy(alpha = progress)
|
||||||
|
},
|
||||||
|
shadow = {
|
||||||
|
Shadow(
|
||||||
|
elevation = 4f.dp,
|
||||||
|
color = Color.Black.copy(0.08f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
layer = {
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
val scale = lerp(1f, 1.5f, progress)
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
},
|
||||||
|
onDrawSurface = {
|
||||||
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
||||||
|
|
||||||
|
val shape = RoundedCornerShape(28.dp)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
||||||
|
}
|
||||||
|
.size(40f.dp, 24f.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (endIcon != null) {
|
||||||
|
Text(
|
||||||
|
text = endIcon,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = labelTextColor,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (independent) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||||
|
.heightIn(min = 55.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
||||||
|
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
||||||
|
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun StyledSliderPreview() {
|
||||||
|
StyledSlider(
|
||||||
|
mutableFloatState = remember {mutableFloatStateOf(1f)},
|
||||||
|
onValueChange = {},
|
||||||
|
valueRange = 0f..2f,
|
||||||
|
independent = true,
|
||||||
|
startIcon = "A",
|
||||||
|
endIcon = "B"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
* by the Free Software Foundation, either version 3 of the License.
|
* by the Free Software Foundation, either version 3 of the License.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU Affero General Public License for more details.
|
* GNU Affero General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
@@ -85,4 +85,4 @@ fun StyledSwitch(
|
|||||||
@Composable
|
@Composable
|
||||||
fun StyledSwitchPreview() {
|
fun StyledSwitchPreview() {
|
||||||
StyledSwitch(checked = true, onCheckedChange = {})
|
StyledSwitch(checked = true, onCheckedChange = {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,190 +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.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.Font
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ToneVolumeSlider() {
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
val sliderValueFromAACP = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
val sliderValue = remember { mutableFloatStateOf(
|
|
||||||
sliderValueFromAACP?.toFloat() ?: -1f
|
|
||||||
) }
|
|
||||||
val listener = object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)?.toFloat()
|
|
||||||
if (newValue != null) {
|
|
||||||
sliderValue.floatValue = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, listener)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d("ToneVolumeSlider", "Slider value: ${sliderValue.floatValue}")
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(0.95f),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "\uDBC0\uDEA1",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = labelTextColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 4.dp)
|
|
||||||
)
|
|
||||||
Slider(
|
|
||||||
value = sliderValue.floatValue,
|
|
||||||
onValueChange = {
|
|
||||||
sliderValue.floatValue = snapIfClose(it, listOf(100f))
|
|
||||||
},
|
|
||||||
valueRange = 0f..125f,
|
|
||||||
onValueChangeFinished = {
|
|
||||||
sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(100f))
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.CHIME_VOLUME.value,
|
|
||||||
value = byteArrayOf(sliderValue.floatValue.toInt().toByte(),
|
|
||||||
0x50.toByte()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(36.dp),
|
|
||||||
colors = SliderDefaults.colors(
|
|
||||||
thumbColor = thumbColor,
|
|
||||||
activeTrackColor = activeTrackColor,
|
|
||||||
inactiveTrackColor = trackColor
|
|
||||||
),
|
|
||||||
thumb = {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.shadow(4.dp, CircleShape)
|
|
||||||
.background(thumbColor, CircleShape)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
track = {
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(12.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(4.dp)
|
|
||||||
.background(trackColor, RoundedCornerShape(4.dp))
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(sliderValue.floatValue / 125)
|
|
||||||
.height(4.dp)
|
|
||||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "\uDBC0\uDEA9",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = labelTextColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(end = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ToneVolumeSliderPreview() {
|
|
||||||
ToneVolumeSlider()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
|
||||||
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
|
||||||
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -84,14 +84,13 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import dev.chrisbanes.haze.HazeEffectScope
|
import dev.chrisbanes.haze.HazeEffectScope
|
||||||
import dev.chrisbanes.haze.HazeState
|
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.CustomDevice
|
|
||||||
import me.kavishdevar.librepods.composables.AudioSettings
|
import me.kavishdevar.librepods.composables.AudioSettings
|
||||||
import me.kavishdevar.librepods.composables.BatteryView
|
import me.kavishdevar.librepods.composables.BatteryView
|
||||||
import me.kavishdevar.librepods.composables.CallControlSettings
|
import me.kavishdevar.librepods.composables.CallControlSettings
|
||||||
@@ -146,7 +145,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = rememberHazeState( blurEnabled = true )
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -250,7 +249,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
block = fun HazeEffectScope.() {
|
block = fun HazeEffectScope.() {
|
||||||
alpha =
|
alpha =
|
||||||
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
mDensity.floatValue = density
|
mDensity.floatValue = density
|
||||||
val strokeWidth = 0.7.dp.value * density
|
val strokeWidth = 0.7.dp.value * density
|
||||||
@@ -364,7 +364,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
NoiseControlSettings(service = service)
|
NoiseControlSettings(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
CallControlSettings()
|
CallControlSettings(hazeState = hazeState)
|
||||||
|
|
||||||
// camera control goes here, airpods side is done, i just need to figure out how to listen to app open/close events
|
// camera control goes here, airpods side is done, i just need to figure out how to listen to app open/close events
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
ConnectionSettings()
|
ConnectionSettings()
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
MicrophoneSettings()
|
MicrophoneSettings(hazeState)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IndependentToggle(
|
IndependentToggle(
|
||||||
@@ -388,15 +388,12 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
default = false,
|
default = false,
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
NavigationButton(to = "head_tracking", stringResource(R.string.head_gestures), navController)
|
NavigationButton(to = "head_tracking", stringResource(R.string.head_gestures), navController)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
NavigationButton(to = "", "Accessibility", navController = navController, onClick = {
|
NavigationButton(to = "accessibility", "Accessibility", navController = navController)
|
||||||
val intent = Intent(context, CustomDevice::class.java)
|
|
||||||
context.startActivity(intent)
|
|
||||||
})
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IndependentToggle(
|
IndependentToggle(
|
||||||
|
|||||||
@@ -21,37 +21,16 @@ package me.kavishdevar.librepods.screens
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -59,22 +38,14 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -96,17 +67,12 @@ 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.AccessibilitySlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
import me.kavishdevar.librepods.composables.IndependentToggle
|
||||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.ToneVolumeSlider
|
|
||||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
|
import me.kavishdevar.librepods.utils.ATTManager
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
@@ -114,14 +80,13 @@ import java.nio.ByteOrder
|
|||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
private var debounceJob: Job? = null
|
||||||
private var phoneMediaDebounceJob: Job? = null
|
|
||||||
private const val TAG = "HearingAidAdjustments"
|
private const val TAG = "HearingAidAdjustments"
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
@ExperimentalHazeMaterialsApi
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HearingAidAdjustmentsScreen(navController: NavController) {
|
fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
@@ -131,14 +96,14 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
|
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val radareOffsetFinder = remember { RadareOffsetFinder(context) }
|
remember { RadareOffsetFinder(context) }
|
||||||
val isSdpOffsetAvailable = remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
||||||
val service = ServiceManager.getService()
|
val service = ServiceManager.getService()
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7),
|
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7),
|
||||||
@@ -192,9 +157,9 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
.verticalScroll(verticalScrollState),
|
.verticalScroll(verticalScrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
val enabled = remember { mutableStateOf(false) }
|
remember { mutableStateOf(false) }
|
||||||
val amplificationSliderValue = remember { mutableFloatStateOf(0.5f) }
|
val amplificationSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||||
val balanceSliderValue = remember { mutableFloatStateOf(0.5f) }
|
val balanceSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||||
val toneSliderValue = remember { mutableFloatStateOf(0.5f) }
|
val toneSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||||
@@ -210,9 +175,9 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
val initialLoadComplete = remember { mutableStateOf(false) }
|
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val initialReadSucceeded = remember { mutableStateOf(false) }
|
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||||
val initialReadAttempts = remember { mutableStateOf(0) }
|
val initialReadAttempts = remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
val HearingAidSettings = remember {
|
val hearingAidSettings = remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
HearingAidSettings(
|
HearingAidSettings(
|
||||||
leftEQ = eq.value,
|
leftEQ = eq.value,
|
||||||
@@ -295,7 +260,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
HearingAidSettings.value = HearingAidSettings(
|
hearingAidSettings.value = HearingAidSettings(
|
||||||
leftEQ = eq.value,
|
leftEQ = eq.value,
|
||||||
rightEQ = eq.value,
|
rightEQ = eq.value,
|
||||||
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||||
@@ -310,8 +275,8 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
balance = balanceSliderValue.floatValue,
|
balance = balanceSliderValue.floatValue,
|
||||||
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
ownVoiceAmplification = ownVoiceAmplification.floatValue
|
||||||
)
|
)
|
||||||
Log.d(TAG, "Updated settings: ${HearingAidSettings.value}")
|
Log.d(TAG, "Updated settings: ${hearingAidSettings.value}")
|
||||||
sendHearingAidSettings(attManager, HearingAidSettings.value)
|
sendHearingAidSettings(attManager, hearingAidSettings.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -342,7 +307,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
|
|
||||||
var parsedSettings: HearingAidSettings? = null
|
var parsedSettings: HearingAidSettings? = null
|
||||||
for (attempt in 1..3) {
|
for (attempt in 1..3) {
|
||||||
initialReadAttempts.value = attempt
|
initialReadAttempts.intValue = attempt
|
||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.HEARING_AID)
|
val data = attManager.read(ATTHandles.HEARING_AID)
|
||||||
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
parsedSettings = parseHearingAidSettingsResponse(data = data)
|
||||||
@@ -369,7 +334,7 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
ownVoiceAmplification.floatValue = parsedSettings.ownVoiceAmplification
|
||||||
initialReadSucceeded.value = true
|
initialReadSucceeded.value = true
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Failed to read/parse initial hearing aid settings after ${initialReadAttempts.value} attempts")
|
Log.d(TAG, "Failed to read/parse initial hearing aid settings after ${initialReadAttempts.intValue} attempts")
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -378,10 +343,6 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDarkThemeLocal = isSystemInDarkTheme()
|
|
||||||
var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
|
||||||
val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.amplification).uppercase(),
|
text = stringResource(R.string.amplification).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -392,48 +353,16 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
)
|
)
|
||||||
Box(
|
StyledSlider(
|
||||||
modifier = Modifier
|
valueRange = -1f..1f,
|
||||||
.fillMaxWidth()
|
mutableFloatState = amplificationSliderValue,
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
onValueChange = {
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
amplificationSliderValue.floatValue = it
|
||||||
.height(55.dp)
|
},
|
||||||
) {
|
startIcon = "",
|
||||||
Row(
|
endIcon = "",
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
independent = true,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
)
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 4.dp)
|
|
||||||
)
|
|
||||||
AccessibilitySlider(
|
|
||||||
valueRange = -1f..1f,
|
|
||||||
value = amplificationSliderValue.floatValue,
|
|
||||||
onValueChange = {
|
|
||||||
amplificationSliderValue.floatValue = snapIfClose(it, listOf(-0.5f, -0.25f, 0f, 0.25f, 0.5f))
|
|
||||||
},
|
|
||||||
widthFrac = 0.90f
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(end = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
@@ -455,47 +384,17 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
)
|
)
|
||||||
Box(
|
StyledSlider(
|
||||||
modifier = Modifier
|
valueRange = -1f..1f,
|
||||||
.fillMaxWidth()
|
mutableFloatState = balanceSliderValue,
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
onValueChange = {
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
balanceSliderValue.floatValue = it
|
||||||
) {
|
},
|
||||||
Column {
|
snapPoints = listOf(0f),
|
||||||
Row(
|
startLabel = stringResource(R.string.left),
|
||||||
modifier = Modifier
|
endLabel = stringResource(R.string.right),
|
||||||
.fillMaxWidth()
|
independent = true,
|
||||||
.padding(8.dp),
|
)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.left),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.right),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AccessibilitySlider(
|
|
||||||
valueRange = -1f..1f,
|
|
||||||
value = balanceSliderValue.floatValue,
|
|
||||||
onValueChange = {
|
|
||||||
balanceSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.tone).uppercase(),
|
text = stringResource(R.string.tone).uppercase(),
|
||||||
@@ -507,47 +406,16 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
)
|
)
|
||||||
Box(
|
StyledSlider(
|
||||||
modifier = Modifier
|
valueRange = -1f..1f,
|
||||||
.fillMaxWidth()
|
mutableFloatState = toneSliderValue,
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
onValueChange = {
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
toneSliderValue.floatValue = it
|
||||||
) {
|
},
|
||||||
Column {
|
startLabel = stringResource(R.string.darker),
|
||||||
Row(
|
endLabel = stringResource(R.string.brighter),
|
||||||
modifier = Modifier
|
independent = true,
|
||||||
.fillMaxWidth()
|
)
|
||||||
.padding(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.darker),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.brighter),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AccessibilitySlider(
|
|
||||||
valueRange = -1f..1f,
|
|
||||||
value = toneSliderValue.floatValue,
|
|
||||||
onValueChange = {
|
|
||||||
toneSliderValue.floatValue = snapIfClose(it, listOf(0f))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||||
@@ -560,47 +428,16 @@ fun HearingAidAdjustmentsScreen(navController: NavController) {
|
|||||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
StyledSlider(
|
||||||
modifier = Modifier
|
valueRange = 0f..1f,
|
||||||
.fillMaxWidth()
|
mutableFloatState = ambientNoiseReductionSliderValue,
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
onValueChange = {
|
||||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
ambientNoiseReductionSliderValue.floatValue = it
|
||||||
) {
|
},
|
||||||
Column {
|
startLabel = stringResource(R.string.less),
|
||||||
Row(
|
endLabel = stringResource(R.string.more),
|
||||||
modifier = Modifier
|
independent = true,
|
||||||
.fillMaxWidth()
|
)
|
||||||
.padding(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.less),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.more),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = labelTextColor,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AccessibilitySlider(
|
|
||||||
valueRange = 0f..1f,
|
|
||||||
value = ambientNoiseReductionSliderValue.floatValue,
|
|
||||||
onValueChange = {
|
|
||||||
ambientNoiseReductionSliderValue.floatValue = snapIfClose(it, listOf(0.1f, 0.3f, 0.5f, 0.7f, 0.9f))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessibilityToggle(
|
AccessibilityToggle(
|
||||||
text = stringResource(R.string.conversation_boost),
|
text = stringResource(R.string.conversation_boost),
|
||||||
@@ -668,8 +505,6 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
|
|||||||
if (data.size < 104) return null
|
if (data.size < 104) return null
|
||||||
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
val phoneEnabled = buffer.get() == 0x01.toByte()
|
|
||||||
val mediaEnabled = buffer.get() == 0x01.toByte()
|
|
||||||
buffer.getShort() // skip 0x60 0x00
|
buffer.getShort() // skip 0x60 0x00
|
||||||
|
|
||||||
val leftEQ = FloatArray(8)
|
val leftEQ = FloatArray(8)
|
||||||
@@ -718,7 +553,7 @@ private fun parseHearingAidSettingsResponse(data: ByteArray): HearingAidSettings
|
|||||||
|
|
||||||
private fun sendHearingAidSettings(
|
private fun sendHearingAidSettings(
|
||||||
attManager: ATTManager,
|
attManager: ATTManager,
|
||||||
HearingAidSettings: HearingAidSettings
|
hearingAidSettings: HearingAidSettings
|
||||||
) {
|
) {
|
||||||
debounceJob?.cancel()
|
debounceJob?.cancel()
|
||||||
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
@@ -736,19 +571,19 @@ private fun sendHearingAidSettings(
|
|||||||
buffer.put(2, 0x64)
|
buffer.put(2, 0x64)
|
||||||
|
|
||||||
// Left ear adjustments
|
// Left ear adjustments
|
||||||
buffer.putFloat(36, HearingAidSettings.leftAmplification)
|
buffer.putFloat(36, hearingAidSettings.leftAmplification)
|
||||||
buffer.putFloat(40, HearingAidSettings.leftTone)
|
buffer.putFloat(40, hearingAidSettings.leftTone)
|
||||||
buffer.putFloat(44, if (HearingAidSettings.leftConversationBoost) 1.0f else 0.0f)
|
buffer.putFloat(44, if (hearingAidSettings.leftConversationBoost) 1.0f else 0.0f)
|
||||||
buffer.putFloat(48, HearingAidSettings.leftAmbientNoiseReduction)
|
buffer.putFloat(48, hearingAidSettings.leftAmbientNoiseReduction)
|
||||||
|
|
||||||
// Right ear adjustments
|
// Right ear adjustments
|
||||||
buffer.putFloat(84, HearingAidSettings.rightAmplification)
|
buffer.putFloat(84, hearingAidSettings.rightAmplification)
|
||||||
buffer.putFloat(88, HearingAidSettings.rightTone)
|
buffer.putFloat(88, hearingAidSettings.rightTone)
|
||||||
buffer.putFloat(92, if (HearingAidSettings.rightConversationBoost) 1.0f else 0.0f)
|
buffer.putFloat(92, if (hearingAidSettings.rightConversationBoost) 1.0f else 0.0f)
|
||||||
buffer.putFloat(96, HearingAidSettings.rightAmbientNoiseReduction)
|
buffer.putFloat(96, hearingAidSettings.rightAmbientNoiseReduction)
|
||||||
|
|
||||||
// Own voice amplification
|
// Own voice amplification
|
||||||
buffer.putFloat(100, HearingAidSettings.ownVoiceAmplification)
|
buffer.putFloat(100, hearingAidSettings.ownVoiceAmplification)
|
||||||
|
|
||||||
Log.d(TAG, "Sending updated settings: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
|
Log.d(TAG, "Sending updated settings: ${currentData.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
|
|
||||||
@@ -758,8 +593,3 @@ private fun sendHearingAidSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.05f): Float {
|
|
||||||
val nearest = points.minByOrNull { kotlin.math.abs(it - value) } ?: value
|
|
||||||
return if (kotlin.math.abs(nearest - value) <= threshold) nearest else value
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@@ -28,43 +27,30 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -72,16 +58,10 @@ 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.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -99,27 +79,15 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
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.AccessibilitySlider
|
|
||||||
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
||||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
import me.kavishdevar.librepods.composables.ToneVolumeSlider
|
|
||||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.TransparencySettings
|
|
||||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
||||||
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
private var debounceJob: Job? = null
|
private var debounceJob: Job? = null
|
||||||
@@ -139,15 +107,6 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
||||||
|
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
val context = LocalContext.current
|
|
||||||
val radareOffsetFinder = remember { RadareOffsetFinder(context) }
|
|
||||||
val isSdpOffsetAvailable = remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
|
||||||
val service = ServiceManager.getService()
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
val showDialog = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -385,7 +344,11 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
onAdjustMediaChange(!adjustMediaEnabled.value)
|
onAdjustMediaChange(!adjustMediaEnabled.value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
.background(
|
||||||
|
animatedBackgroundColorAdjustMedia,
|
||||||
|
RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -430,7 +393,11 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
onAdjustPhoneChange(!adjustPhoneEnabled.value)
|
onAdjustPhoneChange(!adjustPhoneEnabled.value)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
.background(
|
||||||
|
animatedBackgroundColorAdjustPhone,
|
||||||
|
RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -466,11 +433,10 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
if (!enrolled) {
|
if (!enrolled) {
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
|
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
|
||||||
} else {
|
} else {
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
|
aacpManager.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x01))
|
||||||
}
|
}
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte())
|
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x01.toByte())
|
||||||
hearingAidEnabled.value = true
|
hearingAidEnabled.value = true
|
||||||
// Disable transparency mode
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
||||||
@@ -484,9 +450,6 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hazeState = hazeState,
|
hazeState = hazeState
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
textColor = textColor,
|
|
||||||
activeTrackColor = activeTrackColor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import dev.chrisbanes.haze.HazeEffectScope
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.hazeSource
|
||||||
|
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
|
import me.kavishdevar.librepods.utils.ATTManager
|
||||||
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
|
import me.kavishdevar.librepods.utils.TransparencySettings
|
||||||
|
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
||||||
|
import me.kavishdevar.librepods.utils.sendTransparencySettings
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
private const val TAG = "TransparencySettings"
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TransparencySettingsScreen(navController: NavController) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val verticalScrollState = rememberScrollState()
|
||||||
|
val hazeState = remember { HazeState() }
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val attManager = ServiceManager.getService()?.attManager ?: return
|
||||||
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
|
val isSdpOffsetAvailable =
|
||||||
|
remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
||||||
|
|
||||||
|
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
||||||
|
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = if (isSystemInDarkTheme()) Color(
|
||||||
|
0xFF000000
|
||||||
|
) else Color(
|
||||||
|
0xFFF2F2F7
|
||||||
|
),
|
||||||
|
topBar = {
|
||||||
|
val darkMode = isSystemInDarkTheme()
|
||||||
|
val mDensity = remember { mutableFloatStateOf(1f) }
|
||||||
|
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.customize_transparency_mode),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (darkMode) Color.White else Color.Black,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.hazeEffect(
|
||||||
|
state = hazeState,
|
||||||
|
style = CupertinoMaterials.thick(),
|
||||||
|
block = fun HazeEffectScope.() {
|
||||||
|
alpha =
|
||||||
|
if (verticalScrollState.value > 60.dp.value * mDensity.floatValue) 1f else 0f
|
||||||
|
})
|
||||||
|
.drawBehind {
|
||||||
|
mDensity.floatValue = density
|
||||||
|
val strokeWidth = 0.7.dp.value * density
|
||||||
|
val y = size.height - strokeWidth / 2
|
||||||
|
if (verticalScrollState.value > 60.dp.value * density) {
|
||||||
|
drawLine(
|
||||||
|
if (darkMode) Color.DarkGray else Color.LightGray,
|
||||||
|
Offset(0f, y),
|
||||||
|
Offset(size.width, y),
|
||||||
|
strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.hazeSource(hazeState)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.verticalScroll(verticalScrollState),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
val enabled = remember { mutableStateOf(false) }
|
||||||
|
val amplificationSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||||
|
val balanceSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||||
|
val toneSliderValue = remember { mutableFloatStateOf(0.5f) }
|
||||||
|
val ambientNoiseReductionSliderValue = remember { mutableFloatStateOf(0.0f) }
|
||||||
|
val conversationBoostEnabled = remember { mutableStateOf(false) }
|
||||||
|
val eq = remember { mutableStateOf(FloatArray(8)) }
|
||||||
|
val phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||||
|
|
||||||
|
val initialLoadComplete = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val initialReadSucceeded = remember { mutableStateOf(false) }
|
||||||
|
val initialReadAttempts = remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
val transparencySettings = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
TransparencySettings(
|
||||||
|
enabled = enabled.value,
|
||||||
|
leftEQ = eq.value,
|
||||||
|
rightEQ = eq.value,
|
||||||
|
leftAmplification = amplificationSliderValue.floatValue + (0.5f - balanceSliderValue.floatValue) * amplificationSliderValue.floatValue * 2,
|
||||||
|
rightAmplification = amplificationSliderValue.floatValue + (balanceSliderValue.floatValue - 0.5f) * amplificationSliderValue.floatValue * 2,
|
||||||
|
leftTone = toneSliderValue.floatValue,
|
||||||
|
rightTone = toneSliderValue.floatValue,
|
||||||
|
leftConversationBoost = conversationBoostEnabled.value,
|
||||||
|
rightConversationBoost = conversationBoostEnabled.value,
|
||||||
|
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
|
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
|
netAmplification = amplificationSliderValue.floatValue,
|
||||||
|
balance = balanceSliderValue.floatValue
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val transparencyListener = remember {
|
||||||
|
object : (ByteArray) -> Unit {
|
||||||
|
override fun invoke(value: ByteArray) {
|
||||||
|
val parsed = parseTransparencySettingsResponse(value)
|
||||||
|
if (parsed != null) {
|
||||||
|
enabled.value = parsed.enabled
|
||||||
|
amplificationSliderValue.floatValue = parsed.netAmplification
|
||||||
|
balanceSliderValue.floatValue = parsed.balance
|
||||||
|
toneSliderValue.floatValue = parsed.leftTone
|
||||||
|
ambientNoiseReductionSliderValue.floatValue =
|
||||||
|
parsed.leftAmbientNoiseReduction
|
||||||
|
conversationBoostEnabled.value = parsed.leftConversationBoost
|
||||||
|
eq.value = parsed.leftEQ.copyOf()
|
||||||
|
Log.d(TAG, "Updated transparency settings from notification")
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to parse transparency settings from notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
enabled.value,
|
||||||
|
amplificationSliderValue.floatValue,
|
||||||
|
balanceSliderValue.floatValue,
|
||||||
|
toneSliderValue.floatValue,
|
||||||
|
conversationBoostEnabled.value,
|
||||||
|
ambientNoiseReductionSliderValue.floatValue,
|
||||||
|
eq.value,
|
||||||
|
initialLoadComplete.value,
|
||||||
|
initialReadSucceeded.value
|
||||||
|
) {
|
||||||
|
if (!initialLoadComplete.value) {
|
||||||
|
Log.d(TAG, "Initial device load not complete - skipping send")
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialReadSucceeded.value) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Initial device read not successful yet - skipping send until read succeeds"
|
||||||
|
)
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
transparencySettings.value = TransparencySettings(
|
||||||
|
enabled = enabled.value,
|
||||||
|
leftEQ = eq.value,
|
||||||
|
rightEQ = eq.value,
|
||||||
|
leftAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue < 0) -balanceSliderValue.floatValue else 0f,
|
||||||
|
rightAmplification = amplificationSliderValue.floatValue + if (balanceSliderValue.floatValue > 0) balanceSliderValue.floatValue else 0f,
|
||||||
|
leftTone = toneSliderValue.floatValue,
|
||||||
|
rightTone = toneSliderValue.floatValue,
|
||||||
|
leftConversationBoost = conversationBoostEnabled.value,
|
||||||
|
rightConversationBoost = conversationBoostEnabled.value,
|
||||||
|
leftAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
|
rightAmbientNoiseReduction = ambientNoiseReductionSliderValue.floatValue,
|
||||||
|
netAmplification = amplificationSliderValue.floatValue,
|
||||||
|
balance = balanceSliderValue.floatValue
|
||||||
|
)
|
||||||
|
Log.d("TransparencySettings", "Updated settings: ${transparencySettings.value}")
|
||||||
|
sendTransparencySettings(attManager, transparencySettings.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
attManager.unregisterListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
Log.d(TAG, "Connecting to ATT...")
|
||||||
|
try {
|
||||||
|
attManager.enableNotifications(ATTHandles.TRANSPARENCY)
|
||||||
|
attManager.registerListener(ATTHandles.TRANSPARENCY, transparencyListener)
|
||||||
|
|
||||||
|
// If we have an AACP manager, prefer its EQ data to populate EQ controls first
|
||||||
|
try {
|
||||||
|
if (aacpManager != null) {
|
||||||
|
Log.d(TAG, "Found AACPManager, reading cached EQ data")
|
||||||
|
val aacpEQ = aacpManager.eqData
|
||||||
|
if (aacpEQ.isNotEmpty()) {
|
||||||
|
eq.value = aacpEQ.copyOf()
|
||||||
|
phoneMediaEQ.value = aacpEQ.copyOf()
|
||||||
|
Log.d(TAG, "Populated EQ from AACPManager: ${aacpEQ.toList()}")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "AACPManager EQ data empty")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "No AACPManager available")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error reading EQ from AACPManager: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedSettings: TransparencySettings? = null
|
||||||
|
for (attempt in 1..3) {
|
||||||
|
initialReadAttempts.intValue = attempt
|
||||||
|
try {
|
||||||
|
val data = attManager.read(ATTHandles.TRANSPARENCY)
|
||||||
|
parsedSettings = parseTransparencySettingsResponse(data = data)
|
||||||
|
if (parsedSettings != null) {
|
||||||
|
Log.d(TAG, "Parsed settings on attempt $attempt")
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Parsing returned null on attempt $attempt")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Read attempt $attempt failed: ${e.message}")
|
||||||
|
}
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedSettings != null) {
|
||||||
|
Log.d(TAG, "Initial transparency settings: $parsedSettings")
|
||||||
|
enabled.value = parsedSettings.enabled
|
||||||
|
amplificationSliderValue.floatValue = parsedSettings.netAmplification
|
||||||
|
balanceSliderValue.floatValue = parsedSettings.balance
|
||||||
|
toneSliderValue.floatValue = parsedSettings.leftTone
|
||||||
|
ambientNoiseReductionSliderValue.floatValue =
|
||||||
|
parsedSettings.leftAmbientNoiseReduction
|
||||||
|
conversationBoostEnabled.value = parsedSettings.leftConversationBoost
|
||||||
|
eq.value = parsedSettings.leftEQ.copyOf()
|
||||||
|
initialReadSucceeded.value = true
|
||||||
|
} else {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Failed to read/parse initial transparency settings after ${initialReadAttempts.intValue} attempts"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
initialLoadComplete.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show transparency mode section if SDP offset is available
|
||||||
|
if (isSdpOffsetAvailable.value) {
|
||||||
|
AccessibilityToggle(
|
||||||
|
text = stringResource(R.string.transparency_mode),
|
||||||
|
mutableState = enabled,
|
||||||
|
independent = true,
|
||||||
|
description = stringResource(R.string.customize_transparency_mode_description)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.amplification).uppercase(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
|
)
|
||||||
|
StyledSlider(
|
||||||
|
valueRange = -1f..1f,
|
||||||
|
mutableFloatState = amplificationSliderValue,
|
||||||
|
onValueChange = {
|
||||||
|
amplificationSliderValue.floatValue = it
|
||||||
|
},
|
||||||
|
startIcon = "",
|
||||||
|
endIcon = "",
|
||||||
|
independent = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.balance).uppercase(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
|
)
|
||||||
|
StyledSlider(
|
||||||
|
valueRange = -1f..1f,
|
||||||
|
mutableFloatState = balanceSliderValue,
|
||||||
|
onValueChange = {
|
||||||
|
balanceSliderValue.floatValue = it
|
||||||
|
},
|
||||||
|
snapPoints = listOf(0f),
|
||||||
|
startLabel = stringResource(R.string.left),
|
||||||
|
endLabel = stringResource(R.string.right),
|
||||||
|
independent = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tone).uppercase(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
|
)
|
||||||
|
StyledSlider(
|
||||||
|
valueRange = -1f..1f,
|
||||||
|
mutableFloatState = toneSliderValue,
|
||||||
|
onValueChange = {
|
||||||
|
toneSliderValue.floatValue = it
|
||||||
|
},
|
||||||
|
startLabel = stringResource(R.string.darker),
|
||||||
|
endLabel = stringResource(R.string.brighter),
|
||||||
|
independent = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
StyledSlider(
|
||||||
|
valueRange = 0f..1f,
|
||||||
|
mutableFloatState = ambientNoiseReductionSliderValue,
|
||||||
|
onValueChange = {
|
||||||
|
ambientNoiseReductionSliderValue.floatValue = it
|
||||||
|
},
|
||||||
|
startLabel = stringResource(R.string.less),
|
||||||
|
endLabel = stringResource(R.string.more),
|
||||||
|
independent = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
AccessibilityToggle(
|
||||||
|
text = stringResource(R.string.conversation_boost),
|
||||||
|
mutableState = conversationBoostEnabled,
|
||||||
|
independent = true,
|
||||||
|
description = stringResource(R.string.conversation_boost_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show transparency mode EQ section if SDP offset is available
|
||||||
|
if (isSdpOffsetAvailable.value) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.equalizer).uppercase(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
val eqValue = remember(eq.value[i]) { mutableFloatStateOf(eq.value[i]) }
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(38.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = String.format("%.2f", eqValue.floatValue),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = eqValue.floatValue,
|
||||||
|
onValueChange = { newVal ->
|
||||||
|
eqValue.floatValue = newVal
|
||||||
|
val newEQ = eq.value.copyOf()
|
||||||
|
newEQ[i] = eqValue.floatValue
|
||||||
|
eq.value = newEQ
|
||||||
|
},
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.height(36.dp),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = thumbColor,
|
||||||
|
activeTrackColor = activeTrackColor,
|
||||||
|
inactiveTrackColor = trackColor
|
||||||
|
),
|
||||||
|
thumb = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.shadow(4.dp, CircleShape)
|
||||||
|
.background(thumbColor, CircleShape)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
track = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(12.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
.background(trackColor, RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(eqValue.floatValue / 100f)
|
||||||
|
.height(4.dp)
|
||||||
|
.background(
|
||||||
|
activeTrackColor,
|
||||||
|
RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.band_label, i + 1),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,7 +178,6 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
throw IllegalStateException("End of stream reached")
|
throw IllegalStateException("End of stream reached")
|
||||||
}
|
}
|
||||||
val data = buffer.copyOfRange(0, len)
|
val data = buffer.copyOfRange(0, len)
|
||||||
Log.wtf(TAG, "Read ${data.size} bytes from ATT")
|
|
||||||
Log.d(TAG, "readPDU: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
Log.d(TAG, "readPDU: ${data.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ composeBom = "2025.04.00"
|
|||||||
annotations = "26.0.2"
|
annotations = "26.0.2"
|
||||||
navigationCompose = "2.8.9"
|
navigationCompose = "2.8.9"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
haze = "1.5.3"
|
haze = "1.6.10"
|
||||||
hazeMaterials = "1.5.3"
|
hazeMaterials = "1.6.10"
|
||||||
sliceBuilders = "1.1.0-alpha02"
|
|
||||||
sliceCore = "1.1.0-alpha02"
|
|
||||||
sliceView = "1.1.0-alpha02"
|
|
||||||
dynamicanimation = "1.1.0"
|
dynamicanimation = "1.1.0"
|
||||||
|
foundationLayout = "1.9.1"
|
||||||
|
uiTooling = "1.9.1"
|
||||||
|
mockk = "1.14.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
@@ -33,10 +33,10 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio
|
|||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
|
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }
|
||||||
haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "hazeMaterials" }
|
haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "hazeMaterials" }
|
||||||
androidx-slice-builders = { group = "androidx.slice", name = "slice-builders", version.ref = "sliceBuilders" }
|
|
||||||
androidx-slice-core = { group = "androidx.slice", name = "slice-core", version.ref = "sliceCore" }
|
|
||||||
androidx-slice-view = { group = "androidx.slice", name = "slice-view", version.ref = "sliceView" }
|
|
||||||
androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynamicanimation", version.ref = "dynamicanimation" }
|
androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynamicanimation", version.ref = "dynamicanimation" }
|
||||||
|
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
|
||||||
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
|
||||||
|
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user