mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-05-20 15:58:50 +00:00
android: a very big commit
refactoring ui, mostly
This commit is contained in:
@@ -109,16 +109,17 @@ 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.AccessibilitySettingsScreen
|
||||||
|
import me.kavishdevar.librepods.screens.AdaptiveStrengthScreen
|
||||||
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.HearingAidAdjustmentsScreen
|
import me.kavishdevar.librepods.screens.HearingAidAdjustmentsScreen
|
||||||
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
|
import me.kavishdevar.librepods.screens.HearingAidScreen
|
||||||
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
|
||||||
|
import me.kavishdevar.librepods.screens.TransparencySettingsScreen
|
||||||
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
@@ -201,15 +202,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (data != null && data.scheme == "librepods") {
|
if (data != null && data.scheme == "librepods") {
|
||||||
when (data.host) {
|
when (data.host) {
|
||||||
"add-magic-keys" -> {
|
"add-magic-keys" -> {
|
||||||
// Extract query parameters
|
|
||||||
val queryParams = data.queryParameterNames
|
val queryParams = data.queryParameterNames
|
||||||
queryParams.forEach { param ->
|
queryParams.forEach { param ->
|
||||||
val value = data.getQueryParameter(param)
|
val value = data.getQueryParameter(param)
|
||||||
// Handle your parameters here
|
|
||||||
Log.d("LibrePods", "Parameter: $param = $value")
|
Log.d("LibrePods", "Parameter: $param = $value")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the magic keys addition
|
|
||||||
handleAddMagicKeys(data)
|
handleAddMagicKeys(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +367,7 @@ fun Main() {
|
|||||||
name = navBackStackEntry.arguments?.getString("bud")!!
|
name = navBackStackEntry.arguments?.getString("bud")!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("rename") { navBackStackEntry ->
|
composable("rename") {
|
||||||
RenameScreen(navController)
|
RenameScreen(navController)
|
||||||
}
|
}
|
||||||
composable("app_settings") {
|
composable("app_settings") {
|
||||||
@@ -396,6 +394,9 @@ fun Main() {
|
|||||||
composable("hearing_aid_adjustments") {
|
composable("hearing_aid_adjustments") {
|
||||||
HearingAidAdjustmentsScreen(navController)
|
HearingAidAdjustmentsScreen(navController)
|
||||||
}
|
}
|
||||||
|
composable("adaptive_strength") {
|
||||||
|
AdaptiveStrengthScreen(navController)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods
|
package me.kavishdevar.librepods
|
||||||
|
|||||||
@@ -20,19 +20,17 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -40,12 +38,14 @@ 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 androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettings() {
|
fun AudioSettings(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
|
||||||
|
|
||||||
@@ -63,12 +63,18 @@ fun AudioSettings() {
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(14.dp))
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
PersonalizedVolumeSwitch()
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.personalized_volume),
|
||||||
|
description = stringResource(R.string.personalized_volume_description),
|
||||||
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
@@ -76,7 +82,12 @@ fun AudioSettings() {
|
|||||||
.padding(start = 12.dp, end = 0.dp)
|
.padding(start = 12.dp, end = 0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
ConversationalAwarenessSwitch()
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.conversational_awareness),
|
||||||
|
description = stringResource(R.string.conversational_awareness_description),
|
||||||
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
@@ -92,39 +103,17 @@ fun AudioSettings() {
|
|||||||
.padding(start = 12.dp, end = 0.dp)
|
.padding(start = 12.dp, end = 0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
NavigationButton(
|
||||||
modifier = Modifier
|
to = "adaptive_strength",
|
||||||
.fillMaxWidth()
|
name = stringResource(R.string.adaptive_audio),
|
||||||
.padding(horizontal = 8.dp, vertical = 10.dp)
|
navController = navController,
|
||||||
) {
|
independent = false
|
||||||
Text(
|
)
|
||||||
text = stringResource(R.string.adaptive_audio),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.adaptive_audio_description),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 8.dp, top = 2.dp)
|
|
||||||
.padding(end = 2.dp, start = 2.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
AdaptiveStrengthSlider()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettingsPreview() {
|
fun AudioSettingsPreview() {
|
||||||
AudioSettings()
|
AudioSettings(rememberNavController())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -35,8 +36,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -45,7 +46,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import android.content.Context.MODE_PRIVATE
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -59,9 +59,9 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
fun AutomaticConnectionSwitch() {
|
fun AutomaticConnectionSwitch() {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
val service = ServiceManager.getService()!!
|
val service = ServiceManager.getService()!!
|
||||||
|
|
||||||
val shared_preference_key = "automatic_connection_ctrl_cmd"
|
val sharedPreferenceKey = "automatic_connection_ctrl_cmd"
|
||||||
|
|
||||||
val automaticConnectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
val automaticConnectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG
|
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
@@ -71,7 +71,7 @@ fun AutomaticConnectionSwitch() {
|
|||||||
if (automaticConnectionEnabledValue != null) {
|
if (automaticConnectionEnabledValue != null) {
|
||||||
automaticConnectionEnabledValue == 1.toByte()
|
automaticConnectionEnabledValue == 1.toByte()
|
||||||
} else {
|
} else {
|
||||||
sharedPreferences.getBoolean(shared_preference_key, false)
|
sharedPreferences.getBoolean(sharedPreferenceKey, false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,9 +83,9 @@ fun AutomaticConnectionSwitch() {
|
|||||||
enabled
|
enabled
|
||||||
)
|
)
|
||||||
// todo: send other connected devices smartAudioRoutingDisabled or something, check packets again.
|
// todo: send other connected devices smartAudioRoutingDisabled or something, check packets again.
|
||||||
|
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.putBoolean(shared_preference_key, enabled)
|
.putBoolean(sharedPreferenceKey, enabled)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,14 +95,14 @@ fun AutomaticConnectionSwitch() {
|
|||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
val enabled = newValue == 1.toByte()
|
val enabled = newValue == 1.toByte()
|
||||||
automaticConnectionEnabled = enabled
|
automaticConnectionEnabled = enabled
|
||||||
|
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.putBoolean(shared_preference_key, enabled)
|
.putBoolean(sharedPreferenceKey, enabled)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
service.aacpManager.registerControlCommandListener(
|
service.aacpManager.registerControlCommandListener(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
AACPManager.Companion.ControlCommandIdentifiers.AUTOMATIC_CONNECTION_CONFIG,
|
||||||
|
|||||||
@@ -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/>.
|
||||||
*/
|
*/
|
||||||
@@ -19,31 +19,28 @@
|
|||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
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
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
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
|
||||||
@@ -51,85 +48,78 @@ import androidx.compose.ui.unit.sp
|
|||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
fun BatteryIndicator(
|
||||||
val batteryOutlineColor = Color(0xFFBFBFBF)
|
batteryPercentage: Int,
|
||||||
val batteryFillColor = if (batteryPercentage > 30) Color(0xFF30D158) else Color(0xFFFC3C3C)
|
charging: Boolean = false,
|
||||||
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
prefix: String = "",
|
||||||
|
previousCharging: Boolean = false,
|
||||||
|
) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val backgroundColor = if (isDarkTheme) Color.Black else Color(0xFFF2F2F7)
|
||||||
|
val batteryTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val batteryFillColor = if (batteryPercentage > 25)
|
||||||
|
if (isDarkTheme) Color(0xFF2ED158) else Color(0xFF35C759)
|
||||||
|
else if (isDarkTheme) Color(0xFFFC4244) else Color(0xFFfe373C)
|
||||||
|
|
||||||
val batteryWidth = 40.dp
|
val initialScale = if (previousCharging) 1f else 0f
|
||||||
val batteryHeight = 15.dp
|
val scaleAnim = remember { Animatable(initialScale) }
|
||||||
val batteryCornerRadius = 4.dp
|
val targetScale = if (charging) 1f else 0f
|
||||||
val tipWidth = 5.dp
|
|
||||||
val tipHeight = batteryHeight * 0.375f
|
|
||||||
|
|
||||||
val animatedFillWidth by animateFloatAsState(targetValue = batteryPercentage / 100f)
|
LaunchedEffect(previousCharging, charging) {
|
||||||
val animatedScale by animateFloatAsState(targetValue = if (charging) 1.2f else 1f)
|
scaleAnim.animateTo(targetScale, animationSpec = tween(durationMillis = 250))
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(12.dp)
|
||||||
|
.background(backgroundColor), // just for haze to work
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Row(
|
Box(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
contentAlignment = Alignment.Center
|
||||||
modifier = Modifier.padding(bottom = 4.dp)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier
|
progress = { batteryPercentage / 100f },
|
||||||
.width(batteryWidth)
|
modifier = Modifier.size(40.dp),
|
||||||
.height(batteryHeight)
|
color = batteryFillColor,
|
||||||
) {
|
gapSize = 0.dp,
|
||||||
Box (
|
strokeCap = StrokeCap.Round,
|
||||||
modifier = Modifier
|
strokeWidth = 2.dp,
|
||||||
.fillMaxSize()
|
trackColor = if (isDarkTheme) Color(0xFF0E0E0F) else Color(0xFFE3E3E8)
|
||||||
.border(1.dp, batteryOutlineColor, RoundedCornerShape(batteryCornerRadius))
|
)
|
||||||
)
|
|
||||||
Box(
|
Text(
|
||||||
modifier = Modifier
|
text = "\uDBC0\uDEE6",
|
||||||
.fillMaxHeight()
|
style = TextStyle(
|
||||||
.padding(2.dp)
|
fontSize = 12.sp,
|
||||||
.width(batteryWidth * animatedFillWidth)
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
.background(batteryFillColor, RoundedCornerShape(2.dp))
|
color = batteryFillColor,
|
||||||
)
|
textAlign = TextAlign.Center
|
||||||
if (charging) {
|
),
|
||||||
Text(
|
modifier = Modifier.scale(scaleAnim.value)
|
||||||
text = "\uDBC0\uDEE6",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
color = Color.White,
|
|
||||||
modifier = Modifier
|
|
||||||
.scale(animatedScale)
|
|
||||||
.fillMaxSize(),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(tipWidth)
|
|
||||||
.height(tipHeight)
|
|
||||||
.padding(start = 1.dp)
|
|
||||||
.background(
|
|
||||||
batteryOutlineColor,
|
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = 0.dp,
|
|
||||||
topEnd = 12.dp,
|
|
||||||
bottomStart = 0.dp,
|
|
||||||
bottomEnd = 12.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "$batteryPercentage%",
|
text = "$prefix $batteryPercentage%",
|
||||||
color = batteryTextColor,
|
color = batteryTextColor,
|
||||||
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryIndicatorPreview() {
|
fun BatteryIndicatorPreview() {
|
||||||
BatteryIndicator(batteryPercentage = 48, charging = true)
|
val bg = if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)
|
||||||
}
|
Box(
|
||||||
|
modifier = Modifier.background(bg)
|
||||||
|
) {
|
||||||
|
BatteryIndicator(batteryPercentage = 24, charging = true, prefix = "\uDBC6\uDCE5", previousCharging = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,14 +24,19 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
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.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.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -39,7 +44,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
@@ -57,6 +62,9 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
@Composable
|
@Composable
|
||||||
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
||||||
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
||||||
|
|
||||||
|
val previousBatteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
||||||
|
|
||||||
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@@ -96,16 +104,37 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousBatteryStatus.value = batteryStatus.value
|
||||||
batteryStatus.value = service.getBattery()
|
batteryStatus.value = service.getBattery()
|
||||||
|
|
||||||
if (preview) {
|
if (preview) {
|
||||||
batteryStatus.value = listOf(
|
batteryStatus.value = listOf(
|
||||||
Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING),
|
Battery(BatteryComponent.LEFT, 100, BatteryStatus.NOT_CHARGING),
|
||||||
Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING),
|
Battery(BatteryComponent.RIGHT, 94, BatteryStatus.NOT_CHARGING),
|
||||||
Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING)
|
Battery(BatteryComponent.CASE, 40, BatteryStatus.CHARGING)
|
||||||
)
|
)
|
||||||
|
previousBatteryStatus.value = batteryStatus.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
||||||
|
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
||||||
|
val case = batteryStatus.value.find { it.component == BatteryComponent.CASE }
|
||||||
|
val leftLevel = left?.level ?: 0
|
||||||
|
val rightLevel = right?.level ?: 0
|
||||||
|
val caseLevel = case?.level ?: 0
|
||||||
|
val leftCharging = left?.status == BatteryStatus.CHARGING
|
||||||
|
val rightCharging = right?.status == BatteryStatus.CHARGING
|
||||||
|
val caseCharging = case?.status == BatteryStatus.CHARGING
|
||||||
|
|
||||||
|
val prevLeft = previousBatteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
||||||
|
val prevRight = previousBatteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
||||||
|
val prevCase = previousBatteryStatus.value.find { it.component == BatteryComponent.CASE }
|
||||||
|
val prevLeftCharging = prevLeft?.status == BatteryStatus.CHARGING
|
||||||
|
val prevRightCharging = prevRight?.status == BatteryStatus.CHARGING
|
||||||
|
val prevCaseCharging = prevCase?.status == BatteryStatus.CHARGING
|
||||||
|
|
||||||
|
val singleDisplayed = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -117,43 +146,48 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
contentDescription = stringResource(R.string.buds),
|
contentDescription = stringResource(R.string.buds),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.scale(0.80f)
|
.padding(12.dp)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
leftCharging == rightCharging &&
|
||||||
|
(leftLevel - rightLevel) in -3..3
|
||||||
)
|
)
|
||||||
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
|
||||||
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
|
||||||
if ((right?.status == BatteryStatus.CHARGING && left?.status == BatteryStatus.CHARGING) || (left?.status == BatteryStatus.NOT_CHARGING && right?.status == BatteryStatus.NOT_CHARGING))
|
|
||||||
{
|
{
|
||||||
BatteryIndicator(right.level.let { left.level.coerceAtMost(it) }, left.status == BatteryStatus.CHARGING)
|
BatteryIndicator(
|
||||||
|
leftLevel.coerceAtMost(rightLevel),
|
||||||
|
leftCharging,
|
||||||
|
previousCharging = (prevLeftCharging && prevRightCharging)
|
||||||
|
)
|
||||||
|
singleDisplayed.value = true
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
singleDisplayed.value = false
|
||||||
Row (
|
Row (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
// if (left?.status != BatteryStatus.DISCONNECTED) {
|
if (leftLevel > 0 || left?.status != BatteryStatus.DISCONNECTED) {
|
||||||
if (left?.level != null) {
|
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
left.level,
|
leftLevel,
|
||||||
left.status == BatteryStatus.CHARGING
|
leftCharging,
|
||||||
|
"\uDBC6\uDCE5",
|
||||||
|
previousCharging = prevLeftCharging
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// }
|
if (leftLevel > 0 && rightLevel > 0)
|
||||||
// if (left?.status != BatteryStatus.DISCONNECTED && right?.status != BatteryStatus.DISCONNECTED) {
|
|
||||||
if (left?.level != null && right?.level != null)
|
|
||||||
{
|
{
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
// }
|
if (rightLevel > 0 || right?.status != BatteryStatus.DISCONNECTED)
|
||||||
// if (right?.status != BatteryStatus.DISCONNECTED) {
|
|
||||||
if (right?.level != null)
|
|
||||||
{
|
{
|
||||||
BatteryIndicator(
|
BatteryIndicator(
|
||||||
right.level,
|
rightLevel,
|
||||||
right.status == BatteryStatus.CHARGING
|
rightCharging,
|
||||||
|
"\uDBC6\uDCE8",
|
||||||
|
previousCharging = prevRightCharging
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,26 +197,32 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
val case = batteryStatus.value.find { it.component == BatteryComponent.CASE }
|
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case),
|
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case),
|
||||||
contentDescription = stringResource(R.string.case_alt),
|
contentDescription = stringResource(R.string.case_alt),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.scale(1.25f)
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
// if (case?.status != BatteryStatus.DISCONNECTED) {
|
if (caseLevel > 0 || case?.status != BatteryStatus.DISCONNECTED) {
|
||||||
if (case?.level != null) {
|
BatteryIndicator(
|
||||||
BatteryIndicator(case.level, case.status == BatteryStatus.CHARGING)
|
caseLevel,
|
||||||
|
caseCharging,
|
||||||
|
prefix = if (!singleDisplayed.value) "\uDBC3\uDE6C" else "",
|
||||||
|
previousCharging = prevCaseCharging
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryViewPreview() {
|
fun BatteryViewPreview() {
|
||||||
BatteryView(AirPodsService(), preview = true)
|
val bg = if (isSystemInDarkTheme()) Color.Black else Color(0xFFF2F2F7)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.background(bg)
|
||||||
|
) {
|
||||||
|
BatteryView(AirPodsService(), preview = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import android.graphics.RuntimeShader
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -17,72 +46,207 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
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
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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.util.fastCoerceAtMost
|
||||||
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
|
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.drawBackdrop
|
||||||
import com.kyant.backdrop.effects.blur
|
import com.kyant.backdrop.effects.blur
|
||||||
import com.kyant.backdrop.effects.colorFilter
|
import com.kyant.backdrop.effects.colorControls
|
||||||
import com.kyant.backdrop.effects.refraction
|
import com.kyant.backdrop.effects.refraction
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
|
import com.kyant.backdrop.highlight.HighlightStyle
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.utils.inspectDragGestures
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.tanh
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfirmationDialog(
|
fun ConfirmationDialog(
|
||||||
showDialog: MutableState<Boolean>,
|
showDialog: MutableState<Boolean>,
|
||||||
title: String,
|
title: String,
|
||||||
message: String,
|
message: String,
|
||||||
confirmText: String = "Enable",
|
confirmText: String = "Ok",
|
||||||
dismissText: String = "Cancel",
|
dismissText: String = "Cancel",
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit = { showDialog.value = false },
|
onDismiss: () -> Unit = { showDialog.value = false },
|
||||||
backdrop: Backdrop,
|
backdrop: Backdrop,
|
||||||
) {
|
) {
|
||||||
if (showDialog.value) {
|
AnimatedVisibility(
|
||||||
|
visible = showDialog.value,
|
||||||
|
enter = fadeIn() + scaleIn(initialScale = 1.25f),
|
||||||
|
exit = fadeOut() + scaleOut(targetScale = 0.9f)
|
||||||
|
) {
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
|
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
|
||||||
|
|
||||||
|
val interactiveHighlightShader = remember {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
RuntimeShader(
|
||||||
|
"""
|
||||||
|
uniform float2 size;
|
||||||
|
layout(color) uniform half4 color;
|
||||||
|
uniform float radius;
|
||||||
|
uniform float2 offset;
|
||||||
|
|
||||||
|
half4 main(float2 coord) {
|
||||||
|
float2 center = offset;
|
||||||
|
float dist = distance(coord, center);
|
||||||
|
float intensity = smoothstep(radius, radius * 0.5, dist);
|
||||||
|
return color * intensity;
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val isLightTheme = !isSystemInDarkTheme()
|
val isLightTheme = !isSystemInDarkTheme()
|
||||||
val contentColor = if (isLightTheme) Color.Black else Color.White
|
val contentColor = if (isLightTheme) Color.Black else Color.White
|
||||||
val accentColor = if (isLightTheme) Color(0xFF0088FF) else Color(0xFF0091FF)
|
val accentColor = if (isLightTheme) Color(0xFF0088FF) else Color(0xFF0091FF)
|
||||||
val containerColor = if (isLightTheme) Color(0xFFFAFAFA).copy(0.6f) else Color(0xFF121212).copy(0.4f)
|
val containerColor = if (isLightTheme) Color(0xFFFFFFFF).copy(0.6f) else Color(0xFF101010).copy(0.6f)
|
||||||
val dimColor = if (isLightTheme) Color(0xFF29293A).copy(0.23f) else Color(0xFF121212).copy(0.56f)
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.background(dimColor)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickable(onClick = onDismiss)
|
.clickable(onClick = onDismiss, indication = null, interactionSource = remember { MutableInteractionSource() } )
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
|
.clickable(onClick = {}, indication = null, interactionSource = remember { MutableInteractionSource() } )
|
||||||
.drawBackdrop(
|
.drawBackdrop(
|
||||||
backdrop,
|
backdrop,
|
||||||
{ RoundedCornerShape(48f.dp) },
|
{ RoundedCornerShape(48f.dp) },
|
||||||
// highlight = { Highlight { HighlightStyle.Solid } },
|
highlight = { Highlight { HighlightStyle.Solid } },
|
||||||
onDrawSurface = { drawRect(containerColor) }
|
onDrawSurface = { drawRect(containerColor) },
|
||||||
) {
|
effects = {
|
||||||
colorFilter(
|
colorControls(
|
||||||
brightness = if (isLightTheme) 0.2f else 0.1f,
|
brightness = if (isLightTheme) 0.4f else 0.2f,
|
||||||
saturation = 1.5f
|
saturation = 1.5f
|
||||||
)
|
)
|
||||||
blur(if (isLightTheme) 16f.dp.toPx() else 8f.dp.toPx())
|
blur(if (isLightTheme) 16f.dp.toPx() else 8f.dp.toPx())
|
||||||
refraction(24f.dp.toPx(), 48f.dp.toPx(), true)
|
refraction(24f.dp.toPx(), 48f.dp.toPx(), true)
|
||||||
}
|
},
|
||||||
|
layer = {
|
||||||
|
val width = size.width
|
||||||
|
val height = size.height
|
||||||
|
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
val maxScale = 0f
|
||||||
|
val scale = lerp(1f, 1f + maxScale, progress)
|
||||||
|
|
||||||
|
val maxOffset = size.minDimension
|
||||||
|
val initialDerivative = 0.05f
|
||||||
|
val offset = offsetAnimation.value
|
||||||
|
translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset)
|
||||||
|
translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset)
|
||||||
|
|
||||||
|
val maxDragScale = 0.1f
|
||||||
|
val offsetAngle = atan2(offset.y, offset.x)
|
||||||
|
scaleX =
|
||||||
|
scale +
|
||||||
|
maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
|
||||||
|
(width / height).fastCoerceAtMost(1f)
|
||||||
|
scaleY =
|
||||||
|
scale +
|
||||||
|
maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
|
||||||
|
(height / width).fastCoerceAtMost(1f)
|
||||||
|
},
|
||||||
|
onDrawFront = {
|
||||||
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
||||||
|
if (progress > 0f) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.05f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
interactiveHighlightShader.apply {
|
||||||
|
val offset = pressStartPosition + offsetAnimation.value
|
||||||
|
setFloatUniform("size", size.width, size.height)
|
||||||
|
setColorUniform("color", Color.White.copy(0.075f * progress).toArgb())
|
||||||
|
setFloatUniform("radius", size.maxDimension / 2)
|
||||||
|
setFloatUniform(
|
||||||
|
"offset",
|
||||||
|
offset.x.fastCoerceIn(0f, size.width),
|
||||||
|
offset.y.fastCoerceIn(0f, size.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drawRect(
|
||||||
|
ShaderBrush(interactiveHighlightShader),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.125f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentEffects = {
|
||||||
|
refraction(8f.dp.toPx(), 24f.dp.toPx(), false)
|
||||||
|
}
|
||||||
|
)
|
||||||
.fillMaxWidth(0.75f)
|
.fillMaxWidth(0.75f)
|
||||||
.requiredWidthIn(min = 200.dp, max = 360.dp)
|
.requiredWidthIn(min = 200.dp, max = 360.dp)
|
||||||
|
.pointerInput(animationScope) {
|
||||||
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
|
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
|
||||||
|
val onDragStop: () -> Unit = {
|
||||||
|
animationScope.launch {
|
||||||
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inspectDragGestures(
|
||||||
|
onDragStart = { down ->
|
||||||
|
pressStartPosition = down.position
|
||||||
|
animationScope.launch {
|
||||||
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = { onDragStop() },
|
||||||
|
onDragCancel = onDragStop
|
||||||
|
) { _, dragAmount ->
|
||||||
|
animationScope.launch {
|
||||||
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
Spacer(modifier = Modifier.height(28.dp))
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = contentColor,
|
color = contentColor,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
@@ -103,35 +267,50 @@ fun ConfirmationDialog(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(24.dp, 12.dp, 24.dp, 24.dp)
|
.padding(horizontal = 12.dp)
|
||||||
|
.padding(top = 12.dp, bottom = 24.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Box(
|
// Box(
|
||||||
Modifier
|
// Modifier
|
||||||
.clip(RoundedCornerShape(50.dp))
|
// .clip(RoundedCornerShape(50.dp))
|
||||||
.background(containerColor.copy(0.2f))
|
// .background(containerColor.copy(0.2f))
|
||||||
.clickable(onClick = onDismiss)
|
// .clickable(onClick = onDismiss)
|
||||||
.height(48.dp)
|
// .height(48.dp)
|
||||||
.weight(1f)
|
// .weight(1f)
|
||||||
.padding(horizontal = 16.dp),
|
// .padding(horizontal = 16.dp),
|
||||||
contentAlignment = Alignment.Center
|
// contentAlignment = Alignment.Center
|
||||||
|
// ) {
|
||||||
|
StyledButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
backdrop = backdrop,
|
||||||
|
surfaceColor = if (isLightTheme) Color(0xFFAAAAAA).copy(0.8f) else Color(0xFF202020).copy(0.8f),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
isInteractive = false
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
dismissText,
|
dismissText,
|
||||||
style = TextStyle(contentColor, 16.sp)
|
style = TextStyle(contentColor, 16.sp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
// Box(
|
||||||
Modifier
|
// Modifier
|
||||||
.clip(RoundedCornerShape(50.dp))
|
// .clip(RoundedCornerShape(50.dp))
|
||||||
.background(accentColor)
|
// .background(accentColor)
|
||||||
.clickable(onClick = onConfirm)
|
// .clickable(onClick = onConfirm)
|
||||||
.height(48.dp)
|
// .height(48.dp)
|
||||||
.weight(1f)
|
// .weight(1f)
|
||||||
.padding(horizontal = 16.dp),
|
// .padding(horizontal = 16.dp),
|
||||||
contentAlignment = Alignment.Center
|
// contentAlignment = Alignment.Center
|
||||||
|
// ) {
|
||||||
|
StyledButton(
|
||||||
|
onClick = onConfirm,
|
||||||
|
backdrop = backdrop,
|
||||||
|
surfaceColor = accentColor,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
isInteractive = false
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
confirmText,
|
confirmText,
|
||||||
|
|||||||
@@ -20,34 +20,23 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionSettings() {
|
fun ConnectionSettings() {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
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)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -72,4 +61,4 @@ fun ConnectionSettings() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ConnectionSettingsPreview() {
|
fun ConnectionSettingsPreview() {
|
||||||
ConnectionSettings()
|
ConnectionSettings()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
/*
|
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
|
||||||
*
|
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ConversationalAwarenessSwitch() {
|
|
||||||
val service = ServiceManager.getService()!!
|
|
||||||
val conversationEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
var conversationalAwarenessEnabled by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
conversationEnabledValue == 1.toByte()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConversationalAwareness(enabled: Boolean) {
|
|
||||||
conversationalAwarenessEnabled = enabled
|
|
||||||
service.aacpManager.sendControlCommand(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value,
|
|
||||||
enabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val conversationalAwarenessListener = object: AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG.value) {
|
|
||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
|
||||||
conversationalAwarenessEnabled = newValue == 1.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service.aacpManager.registerControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
|
||||||
conversationalAwarenessListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service.aacpManager.unregisterControlCommandListener(
|
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.CONVERSATION_DETECT_CONFIG,
|
|
||||||
conversationalAwarenessListener
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease()
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
updateConversationalAwareness(!conversationalAwarenessEnabled)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.conversational_awareness),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.conversational_awareness_description),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = textColor.copy(0.6f),
|
|
||||||
lineHeight = 14.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
StyledSwitch(
|
|
||||||
checked = conversationalAwarenessEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
updateConversationalAwareness(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ConversationalAwarenessSwitchPreview() {
|
|
||||||
ConversationalAwarenessSwitch()
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.content.Context.MODE_PRIVATE
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -27,16 +28,14 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
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.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -49,8 +48,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import android.content.Context.MODE_PRIVATE
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
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
|
||||||
@@ -60,8 +57,8 @@ import kotlin.io.encoding.ExperimentalEncodingApi
|
|||||||
fun EarDetectionSwitch() {
|
fun EarDetectionSwitch() {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
val service = ServiceManager.getService()!!
|
val service = ServiceManager.getService()!!
|
||||||
|
|
||||||
val shared_preference_key = "automatic_ear_detection"
|
val sharedPreferenceKey = "automatic_ear_detection"
|
||||||
|
|
||||||
val earDetectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
val earDetectionEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG
|
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG
|
||||||
@@ -72,7 +69,7 @@ fun EarDetectionSwitch() {
|
|||||||
if (earDetectionEnabledValue != null) {
|
if (earDetectionEnabledValue != null) {
|
||||||
earDetectionEnabledValue == 1.toByte()
|
earDetectionEnabledValue == 1.toByte()
|
||||||
} else {
|
} else {
|
||||||
sharedPreferences.getBoolean(shared_preference_key, false)
|
sharedPreferences.getBoolean(sharedPreferenceKey, false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -84,9 +81,9 @@ fun EarDetectionSwitch() {
|
|||||||
enabled
|
enabled
|
||||||
)
|
)
|
||||||
service.setEarDetection(enabled)
|
service.setEarDetection(enabled)
|
||||||
|
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.putBoolean(shared_preference_key, enabled)
|
.putBoolean(sharedPreferenceKey, enabled)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +93,14 @@ fun EarDetectionSwitch() {
|
|||||||
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
val newValue = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
val enabled = newValue == 1.toByte()
|
val enabled = newValue == 1.toByte()
|
||||||
earDetectionEnabled = enabled
|
earDetectionEnabled = enabled
|
||||||
|
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.putBoolean(shared_preference_key, enabled)
|
.putBoolean(sharedPreferenceKey, enabled)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
service.aacpManager.registerControlCommandListener(
|
service.aacpManager.registerControlCommandListener(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
AACPManager.Companion.ControlCommandIdentifiers.EAR_DETECTION_CONFIG,
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
/*
|
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
|
||||||
*
|
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
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.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.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.services.AirPodsService
|
|
||||||
import me.kavishdevar.librepods.R
|
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null, description: String? = null) {
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val snakeCasedName =
|
|
||||||
controlCommandIdentifier?.name ?: name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
|
||||||
var checked by remember { mutableStateOf(default) }
|
|
||||||
|
|
||||||
if (controlCommandIdentifier != null) {
|
|
||||||
checked = service!!.aacpManager.controlCommandStatusList.find {
|
|
||||||
it.identifier == controlCommandIdentifier
|
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0) == 1.toByte()
|
|
||||||
}
|
|
||||||
|
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
|
||||||
|
|
||||||
fun cb() {
|
|
||||||
if (controlCommandIdentifier == null) {
|
|
||||||
sharedPreferences.edit { putBoolean(snakeCasedName, checked) }
|
|
||||||
}
|
|
||||||
if (functionName != null && service != null) {
|
|
||||||
val method =
|
|
||||||
service::class.java.getMethod(functionName, Boolean::class.java)
|
|
||||||
method.invoke(service, checked)
|
|
||||||
}
|
|
||||||
if (controlCommandIdentifier != null) {
|
|
||||||
service?.aacpManager?.sendControlCommand(identifier = controlCommandIdentifier.value, value = checked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(sharedPreferences) {
|
|
||||||
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlCommandIdentifier != null) {
|
|
||||||
val listener = remember {
|
|
||||||
object : AACPManager.ControlCommandListener {
|
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
|
||||||
if (controlCommand.identifier == controlCommandIdentifier.value) {
|
|
||||||
Log.d("IndependentToggle", "Received control command for $name: ${controlCommand.value}")
|
|
||||||
checked = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0) == 1.toByte()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
service?.aacpManager?.registerControlCommandListener(controlCommandIdentifier, listener)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
service?.aacpManager?.unregisterControlCommandListener(controlCommandIdentifier, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
) {
|
|
||||||
Box (
|
|
||||||
modifier = Modifier
|
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
backgroundColor = if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
|
||||||
tryAwaitRelease()
|
|
||||||
backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
checked = !checked
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(55.dp)
|
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = name,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
StyledSwitch(
|
|
||||||
checked = checked,
|
|
||||||
onCheckedChange = {
|
|
||||||
checked = it
|
|
||||||
cb()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun IndependentTogglePreview() {
|
|
||||||
IndependentToggle("Test", AirPodsService(), "test", LocalContext.current.getSharedPreferences("preview", 0), true)
|
|
||||||
}
|
|
||||||
@@ -51,7 +51,6 @@ import androidx.compose.ui.unit.sp
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ fun LoudSoundReductionSwitch() {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: throw IllegalStateException("ATTManager not available")
|
val attManager = ServiceManager.getService()?.attManager ?: return
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
attManager.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
|
attManager.enableNotifications(ATTHandles.LOUD_SOUND_REDUCTION)
|
||||||
|
|
||||||
|
|||||||
@@ -20,19 +20,10 @@
|
|||||||
|
|
||||||
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.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -42,15 +33,9 @@ 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.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -63,7 +48,6 @@ 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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -71,20 +55,10 @@ 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.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 dev.chrisbanes.haze.HazeEffectScope
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
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 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
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ import androidx.navigation.NavController
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationButton(to: String, name: String, navController: NavController, onClick: (() -> Unit)? = null) {
|
fun NavigationButton(to: String, name: String, navController: NavController, onClick: (() -> Unit)? = null, independent: Boolean = true) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
.background(animatedBackgroundColor, RoundedCornerShape(if (independent) 14.dp else 0.dp))
|
||||||
.height(55.dp)
|
.height(55.dp)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ fun NoiseControlSettings(
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
)
|
)
|
||||||
|
@Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -437,7 +438,7 @@ fun NoiseControlSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview()
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSettingsPreview() {
|
fun NoiseControlSettingsPreview() {
|
||||||
NoiseControlSettings(AirPodsService())
|
NoiseControlSettings(AirPodsService())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
@@ -35,8 +35,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -53,10 +53,10 @@ 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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PersonalizedVolumeSwitch() {
|
fun PersonalizedVolumeSwitch() {
|
||||||
val service = ServiceManager.getService()!!
|
val service = ServiceManager.getService()!!
|
||||||
|
|
||||||
val adaptiveVolumeEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
val adaptiveVolumeEnabledValue = service.aacpManager.controlCommandStatusList.find {
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG
|
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
@@ -83,7 +83,7 @@ fun PersonalizedVolumeSwitch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
service.aacpManager.registerControlCommandListener(
|
service.aacpManager.registerControlCommandListener(
|
||||||
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
AACPManager.Companion.ControlCommandIdentifiers.ADAPTIVE_VOLUME_CONFIG,
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphics.RuntimeShader
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
|
import androidx.compose.ui.graphics.isSpecified
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastCoerceAtMost
|
||||||
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import com.kyant.backdrop.Backdrop
|
||||||
|
import com.kyant.backdrop.drawBackdrop
|
||||||
|
import com.kyant.backdrop.effects.blur
|
||||||
|
import com.kyant.backdrop.effects.refraction
|
||||||
|
import com.kyant.backdrop.effects.vibrancy
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.utils.inspectDragGestures
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.tanh
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
backdrop: Backdrop,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isInteractive: Boolean = true,
|
||||||
|
tint: Color = Color.Unspecified,
|
||||||
|
surfaceColor: Color = Color.Unspecified,
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
|
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
|
||||||
|
var isPressed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val interactiveHighlightShader = remember {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
RuntimeShader(
|
||||||
|
"""
|
||||||
|
uniform float2 size;
|
||||||
|
layout(color) uniform half4 color;
|
||||||
|
uniform float radius;
|
||||||
|
uniform float2 offset;
|
||||||
|
|
||||||
|
half4 main(float2 coord) {
|
||||||
|
float2 center = offset;
|
||||||
|
float dist = distance(coord, center);
|
||||||
|
float intensity = smoothstep(radius, radius * 0.5, dist);
|
||||||
|
return color * intensity;
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.then(
|
||||||
|
if (!isInteractive) {
|
||||||
|
Modifier.drawBackdrop(
|
||||||
|
backdrop = backdrop,
|
||||||
|
shape = { RoundedCornerShape(28f.dp) },
|
||||||
|
effects = {
|
||||||
|
blur(16f.dp.toPx())
|
||||||
|
},
|
||||||
|
layer = null,
|
||||||
|
onDrawSurface = {
|
||||||
|
if (tint.isSpecified) {
|
||||||
|
drawRect(tint, blendMode = BlendMode.Hue)
|
||||||
|
drawRect(tint.copy(alpha = 0.75f))
|
||||||
|
} else {
|
||||||
|
drawRect(Color.White.copy(0.1f))
|
||||||
|
}
|
||||||
|
if (surfaceColor.isSpecified) {
|
||||||
|
val color = if (!isInteractive && isPressed) {
|
||||||
|
Color(
|
||||||
|
red = surfaceColor.red * 0.5f,
|
||||||
|
green = surfaceColor.green * 0.5f,
|
||||||
|
blue = surfaceColor.blue * 0.5f,
|
||||||
|
alpha = surfaceColor.alpha
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
surfaceColor
|
||||||
|
}
|
||||||
|
drawRect(color)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrawFront = null,
|
||||||
|
highlight = { Highlight.AmbientDefault.copy(alpha = 0f) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier.drawBackdrop(
|
||||||
|
backdrop = backdrop,
|
||||||
|
shape = { RoundedCornerShape(28f.dp) },
|
||||||
|
effects = {
|
||||||
|
vibrancy()
|
||||||
|
blur(2f.dp.toPx())
|
||||||
|
refraction(12f.dp.toPx(), 24f.dp.toPx())
|
||||||
|
},
|
||||||
|
layer = {
|
||||||
|
val width = size.width
|
||||||
|
val height = size.height
|
||||||
|
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
val maxScale = 0.1f
|
||||||
|
val scale = lerp(1f, 1f + maxScale, progress)
|
||||||
|
|
||||||
|
val maxOffset = size.minDimension
|
||||||
|
val initialDerivative = 0.05f
|
||||||
|
val offset = offsetAnimation.value
|
||||||
|
translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset)
|
||||||
|
translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset)
|
||||||
|
|
||||||
|
val maxDragScale = 0.1f
|
||||||
|
val offsetAngle = atan2(offset.y, offset.x)
|
||||||
|
scaleX =
|
||||||
|
scale +
|
||||||
|
maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
|
||||||
|
(width / height).fastCoerceAtMost(1f)
|
||||||
|
scaleY =
|
||||||
|
scale +
|
||||||
|
maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
|
||||||
|
(height / width).fastCoerceAtMost(1f)
|
||||||
|
},
|
||||||
|
onDrawSurface = {
|
||||||
|
if (tint.isSpecified) {
|
||||||
|
drawRect(tint, blendMode = BlendMode.Hue)
|
||||||
|
drawRect(tint.copy(alpha = 0.75f))
|
||||||
|
} else {
|
||||||
|
drawRect(Color.White.copy(0.1f))
|
||||||
|
}
|
||||||
|
if (surfaceColor.isSpecified) {
|
||||||
|
val color = if (!isInteractive && isPressed) {
|
||||||
|
Color(
|
||||||
|
red = surfaceColor.red * 0.5f,
|
||||||
|
green = surfaceColor.green * 0.5f,
|
||||||
|
blue = surfaceColor.blue * 0.5f,
|
||||||
|
alpha = surfaceColor.alpha
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
surfaceColor
|
||||||
|
}
|
||||||
|
drawRect(color)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrawFront = {
|
||||||
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
||||||
|
if (progress > 0f) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.1f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
interactiveHighlightShader.apply {
|
||||||
|
val offset = pressStartPosition + offsetAnimation.value
|
||||||
|
setFloatUniform("size", size.width, size.height)
|
||||||
|
setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
|
||||||
|
setFloatUniform("radius", size.maxDimension)
|
||||||
|
setFloatUniform(
|
||||||
|
"offset",
|
||||||
|
offset.x.fastCoerceIn(0f, size.width),
|
||||||
|
offset.y.fastCoerceIn(0f, size.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drawRect(
|
||||||
|
ShaderBrush(interactiveHighlightShader),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.25f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = null,
|
||||||
|
indication = null,
|
||||||
|
role = Role.Button,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (isInteractive) {
|
||||||
|
Modifier.pointerInput(animationScope) {
|
||||||
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
|
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
|
||||||
|
val onDragStop: () -> Unit = {
|
||||||
|
animationScope.launch {
|
||||||
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inspectDragGestures(
|
||||||
|
onDragStart = { down ->
|
||||||
|
pressStartPosition = down.position
|
||||||
|
animationScope.launch {
|
||||||
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = { onDragStop() },
|
||||||
|
onDragCancel = onDragStop
|
||||||
|
) { _, dragAmount ->
|
||||||
|
animationScope.launch {
|
||||||
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed = true
|
||||||
|
tryAwaitRelease()
|
||||||
|
isPressed = false
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.height(48f.dp)
|
||||||
|
.padding(horizontal = 16f.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8f.dp, Alignment.CenterHorizontally),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphics.RuntimeShader
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
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.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.BlurEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
|
import androidx.compose.ui.graphics.TileMode
|
||||||
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
|
import androidx.compose.ui.graphics.isSpecified
|
||||||
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.util.fastCoerceAtMost
|
||||||
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import com.kyant.backdrop.drawBackdrop
|
||||||
|
import com.kyant.backdrop.effects.refractionWithDispersion
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.utils.inspectDragGestures
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.tanh
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
icon: String,
|
||||||
|
darkMode: Boolean,
|
||||||
|
tint: Color = Color.Unspecified,
|
||||||
|
) {
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
|
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
|
||||||
|
val progressAnimation = remember { Animatable(0f) }
|
||||||
|
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
|
||||||
|
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val innerShadowLayer = rememberGraphicsLayer().apply {
|
||||||
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
|
}
|
||||||
|
|
||||||
|
val interactiveHighlightShader = remember {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
RuntimeShader(
|
||||||
|
"""
|
||||||
|
uniform float2 size;
|
||||||
|
layout(color) uniform half4 color;
|
||||||
|
uniform float radius;
|
||||||
|
uniform float2 offset;
|
||||||
|
|
||||||
|
half4 main(float2 coord) {
|
||||||
|
float2 center = offset;
|
||||||
|
float dist = distance(coord, center);
|
||||||
|
float intensity = smoothstep(radius, radius * 0.5, dist);
|
||||||
|
return color * intensity;
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
shape = RoundedCornerShape(56.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.drawBackdrop(
|
||||||
|
backdrop = rememberLayerBackdrop(),
|
||||||
|
shape = { RoundedCornerShape(56.dp) },
|
||||||
|
highlight = {
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
Highlight.AmbientDefault.copy(alpha = progress.coerceIn(0.45f, 1f))
|
||||||
|
},
|
||||||
|
shadow = {
|
||||||
|
Shadow(
|
||||||
|
radius = 4f.dp,
|
||||||
|
color = Color.Black.copy(0.08f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
layer = {
|
||||||
|
val width = size.width
|
||||||
|
val height = size.height
|
||||||
|
|
||||||
|
val progress = progressAnimation.value
|
||||||
|
val maxScale = 0.1f
|
||||||
|
val scale = lerp(1f, 1f + maxScale, progress)
|
||||||
|
|
||||||
|
val maxOffset = size.minDimension
|
||||||
|
val initialDerivative = 0.05f
|
||||||
|
val offset = offsetAnimation.value
|
||||||
|
translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset)
|
||||||
|
translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset)
|
||||||
|
|
||||||
|
val maxDragScale = 0.1f
|
||||||
|
val offsetAngle = atan2(offset.y, offset.x)
|
||||||
|
scaleX =
|
||||||
|
scale +
|
||||||
|
maxDragScale * abs(cos(offsetAngle) * offset.x / size.maxDimension) *
|
||||||
|
(width / height).fastCoerceAtMost(1f)
|
||||||
|
scaleY =
|
||||||
|
scale +
|
||||||
|
maxDragScale * abs(sin(offsetAngle) * offset.y / size.maxDimension) *
|
||||||
|
(height / width).fastCoerceAtMost(1f)
|
||||||
|
},
|
||||||
|
onDrawSurface = {
|
||||||
|
val progress = progressAnimation.value.coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
val shape = RoundedCornerShape(56.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(progress.coerceIn(0.15f, 0.35f))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDrawFront = {
|
||||||
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
||||||
|
if (progress > 0f) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.1f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
interactiveHighlightShader.apply {
|
||||||
|
val offset = pressStartPosition + offsetAnimation.value
|
||||||
|
setFloatUniform("size", size.width, size.height)
|
||||||
|
setColorUniform("color", Color.White.copy(0.15f * progress).toArgb())
|
||||||
|
setFloatUniform("radius", size.maxDimension)
|
||||||
|
setFloatUniform(
|
||||||
|
"offset",
|
||||||
|
offset.x.fastCoerceIn(0f, size.width),
|
||||||
|
offset.y.fastCoerceIn(0f, size.height)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drawRect(
|
||||||
|
ShaderBrush(interactiveHighlightShader),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.25f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
effects = {
|
||||||
|
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.pointerInput(animationScope) {
|
||||||
|
val onDragStop: () -> Unit = {
|
||||||
|
animationScope.launch {
|
||||||
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inspectDragGestures(
|
||||||
|
onDragStart = { down ->
|
||||||
|
pressStartPosition = down.position
|
||||||
|
animationScope.launch {
|
||||||
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
||||||
|
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = { onDragStop() },
|
||||||
|
onDragCancel = onDragStop
|
||||||
|
) { _, dragAmount ->
|
||||||
|
animationScope.launch {
|
||||||
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.size(48.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = icon,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = if (tint.isSpecified) tint else if (darkMode) Color.White else Color.Black,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* 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.isSystemInDarkTheme
|
||||||
|
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.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
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.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import dev.chrisbanes.haze.HazeProgressive
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.HazeTint
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.rememberHazeState
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
|
@Composable
|
||||||
|
fun StyledScaffold(
|
||||||
|
title: String,
|
||||||
|
navigationButton: @Composable () -> Unit = {},
|
||||||
|
actionButtons: List<@Composable () -> Unit> = emptyList(),
|
||||||
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
|
content: @Composable (spacerValue: Dp, hazeState: HazeState) -> Unit
|
||||||
|
) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val hazeState = rememberHazeState(blurEnabled = true)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7),
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
|
) { paddingValues ->
|
||||||
|
val topPadding = paddingValues.calculateTopPadding()
|
||||||
|
val bottomPadding = paddingValues.calculateBottomPadding()
|
||||||
|
val startPadding = paddingValues.calculateLeftPadding(LocalLayoutDirection.current)
|
||||||
|
val endPadding = paddingValues.calculateRightPadding(LocalLayoutDirection.current)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(start = startPadding, end = endPadding, bottom = bottomPadding)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(2f)
|
||||||
|
.height(64.dp + topPadding)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.hazeEffect(state = hazeState) {
|
||||||
|
tints = listOf(HazeTint(color = if (isDarkTheme) Color.Black else Color.White))
|
||||||
|
progressive = HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Spacer(modifier = Modifier.height(topPadding))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
navigationButton()
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (isDarkTheme) Color.White else Color.Black,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
|
) {
|
||||||
|
actionButtons.forEach { it() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
content(topPadding + 64.dp, hazeState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
|
@Composable
|
||||||
|
fun StyledScaffold(
|
||||||
|
title: String,
|
||||||
|
navigationButton: @Composable () -> Unit = {},
|
||||||
|
actionButtons: List<@Composable () -> Unit> = emptyList(),
|
||||||
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
StyledScaffold(
|
||||||
|
title = title,
|
||||||
|
navigationButton = navigationButton,
|
||||||
|
actionButtons = actionButtons,
|
||||||
|
snackbarHostState = snackbarHostState
|
||||||
|
) { _, _ ->
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
|
@Composable
|
||||||
|
fun StyledScaffold(
|
||||||
|
title: String,
|
||||||
|
navigationButton: @Composable () -> Unit = {},
|
||||||
|
actionButtons: List<@Composable () -> Unit> = emptyList(),
|
||||||
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
|
content: @Composable (spacerValue: Dp) -> Unit
|
||||||
|
) {
|
||||||
|
StyledScaffold(
|
||||||
|
title = title,
|
||||||
|
navigationButton = navigationButton,
|
||||||
|
actionButtons = actionButtons,
|
||||||
|
snackbarHostState = snackbarHostState
|
||||||
|
) { spacerValue, _ ->
|
||||||
|
content(spacerValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,12 +75,12 @@ import androidx.compose.ui.util.fastCoerceIn
|
|||||||
import androidx.compose.ui.util.fastRoundToInt
|
import androidx.compose.ui.util.fastRoundToInt
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
import com.kyant.backdrop.Backdrop
|
import com.kyant.backdrop.Backdrop
|
||||||
import com.kyant.backdrop.backdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import com.kyant.backdrop.drawBackdrop
|
import com.kyant.backdrop.drawBackdrop
|
||||||
import com.kyant.backdrop.effects.refractionWithDispersion
|
import com.kyant.backdrop.effects.refractionWithDispersion
|
||||||
import com.kyant.backdrop.highlight.Highlight
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import com.kyant.backdrop.rememberBackdrop
|
|
||||||
import com.kyant.backdrop.rememberCombinedBackdropDrawer
|
|
||||||
import com.kyant.backdrop.shadow.Shadow
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
@@ -88,18 +88,19 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSlider(
|
fun StyledSlider(
|
||||||
label: String? = null, // New optional parameter for the label
|
label: String? = null,
|
||||||
mutableFloatState: MutableFloatState,
|
mutableFloatState: MutableFloatState,
|
||||||
onValueChange: (Float) -> Unit,
|
onValueChange: (Float) -> Unit,
|
||||||
valueRange: ClosedFloatingPointRange<Float>,
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
backdrop: Backdrop = rememberBackdrop(),
|
backdrop: Backdrop = rememberLayerBackdrop(),
|
||||||
snapPoints: List<Float> = emptyList(),
|
snapPoints: List<Float> = emptyList(),
|
||||||
snapThreshold: Float = 0.05f,
|
snapThreshold: Float = 0.05f,
|
||||||
startIcon: String? = null,
|
startIcon: String? = null,
|
||||||
endIcon: String? = null,
|
endIcon: String? = null,
|
||||||
startLabel: String? = null,
|
startLabel: String? = null,
|
||||||
endLabel: String? = null,
|
endLabel: String? = null,
|
||||||
independent: Boolean = false
|
independent: Boolean = false,
|
||||||
|
description: String? = null
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val isLightTheme = !isSystemInDarkTheme()
|
val isLightTheme = !isSystemInDarkTheme()
|
||||||
@@ -126,7 +127,7 @@ fun StyledSlider(
|
|||||||
compositingStrategy = CompositingStrategy.Offscreen
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
val sliderBackdrop = rememberBackdrop()
|
val sliderBackdrop = rememberLayerBackdrop()
|
||||||
val trackWidthState = remember { mutableFloatStateOf(0f) }
|
val trackWidthState = remember { mutableFloatStateOf(0f) }
|
||||||
val trackPositionState = remember { mutableFloatStateOf(0f) }
|
val trackPositionState = remember { mutableFloatStateOf(0f) }
|
||||||
val startIconWidthState = remember { mutableFloatStateOf(0f) }
|
val startIconWidthState = remember { mutableFloatStateOf(0f) }
|
||||||
@@ -137,8 +138,9 @@ fun StyledSlider(
|
|||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxWidth(if (startIcon == null && endIcon == null) 0.95f else 1f)
|
Modifier.fillMaxWidth(if (startIcon == null && endIcon == null) 0.95f else 1f)
|
||||||
) {
|
) {
|
||||||
Box(Modifier
|
Box(
|
||||||
.backdrop(sliderBackdrop)
|
Modifier
|
||||||
|
.layerBackdrop(sliderBackdrop)
|
||||||
.fillMaxWidth()) {
|
.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -188,7 +190,7 @@ fun StyledSlider(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = labelTextColor,
|
color = accentColor,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -237,7 +239,7 @@ fun StyledSlider(
|
|||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = labelTextColor,
|
color = accentColor,
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -291,7 +293,7 @@ fun StyledSlider(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.drawBackdrop(
|
.drawBackdrop(
|
||||||
rememberCombinedBackdropDrawer(backdrop, sliderBackdrop),
|
rememberCombinedBackdrop(backdrop, sliderBackdrop),
|
||||||
{ RoundedCornerShape(28.dp) },
|
{ RoundedCornerShape(28.dp) },
|
||||||
highlight = {
|
highlight = {
|
||||||
val progress = progressAnimation.value
|
val progress = progressAnimation.value
|
||||||
@@ -299,8 +301,8 @@ fun StyledSlider(
|
|||||||
},
|
},
|
||||||
shadow = {
|
shadow = {
|
||||||
Shadow(
|
Shadow(
|
||||||
elevation = 4f.dp,
|
radius = 4f.dp,
|
||||||
color = Color.Black.copy(0.08f)
|
color = Color.Black.copy(0.05f)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
layer = {
|
layer = {
|
||||||
@@ -337,10 +339,11 @@ fun StyledSlider(
|
|||||||
drawLayer(innerShadowLayer)
|
drawLayer(innerShadowLayer)
|
||||||
|
|
||||||
drawRect(Color.White.copy(1f - progress))
|
drawRect(Color.White.copy(1f - progress))
|
||||||
|
},
|
||||||
|
effects = {
|
||||||
|
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
||||||
}
|
}
|
||||||
) {
|
)
|
||||||
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
|
||||||
}
|
|
||||||
.size(40f.dp, 24f.dp)
|
.size(40f.dp, 24f.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -365,7 +368,7 @@ fun StyledSlider(
|
|||||||
modifier = Modifier.padding(8.dp)
|
modifier = Modifier.padding(8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -376,9 +379,24 @@ fun StyledSlider(
|
|||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (label != null) Log.w("StyledSlider", "Label is ignored when independent is false")
|
if (label != null) Log.w("StyledSlider", "Label is ignored when independent is false")
|
||||||
|
if (description != null) Log.w("StyledSlider", "Description is ignored when independent is false")
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
|
package me.kavishdevar.librepods.composables
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
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.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.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.core.content.edit
|
||||||
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledToggle(
|
||||||
|
title: String? = null,
|
||||||
|
label: String,
|
||||||
|
description: String? = null,
|
||||||
|
checkedState: MutableState<Boolean> = remember { mutableStateOf(false) } ,
|
||||||
|
sharedPreferenceKey: String? = null,
|
||||||
|
sharedPreferences: SharedPreferences? = null,
|
||||||
|
independent: Boolean = true
|
||||||
|
) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
var checked by checkedState
|
||||||
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
|
fun cb() {
|
||||||
|
if (sharedPreferences != null) {
|
||||||
|
if (sharedPreferenceKey == null) {
|
||||||
|
Log.e("StyledToggle", "SharedPreferenceKey is null but SharedPreferences is provided.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sharedPreferences.edit { putBoolean(sharedPreferenceKey, checked) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (independent) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
if (title != null) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
checked = !checked
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(55.dp)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (description != null) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed.value = true
|
||||||
|
tryAwaitRelease()
|
||||||
|
isPressed.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
checked = !checked
|
||||||
|
cb()
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
if (description != null) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledToggle(
|
||||||
|
title: String? = null,
|
||||||
|
label: String,
|
||||||
|
description: String? = null,
|
||||||
|
controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers,
|
||||||
|
independent: Boolean = true
|
||||||
|
) {
|
||||||
|
val service = ServiceManager.getService() ?: return
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val checkedValue = service.aacpManager.controlCommandStatusList.find {
|
||||||
|
it.identifier == controlCommandIdentifier
|
||||||
|
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
|
var checked by remember { mutableStateOf(checkedValue == 1.toByte()) }
|
||||||
|
var backgroundColor by remember { mutableStateOf(if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
||||||
|
val animatedBackgroundColor by animateColorAsState(targetValue = backgroundColor, animationSpec = tween(durationMillis = 500))
|
||||||
|
|
||||||
|
fun cb() {
|
||||||
|
service.aacpManager.sendControlCommand(identifier = controlCommandIdentifier.value, value = checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
val listener = remember {
|
||||||
|
object : AACPManager.ControlCommandListener {
|
||||||
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
|
if (controlCommand.identifier == controlCommandIdentifier.value) {
|
||||||
|
Log.d("StyledToggle", "Received control command for $label: ${controlCommand.value}")
|
||||||
|
checked = controlCommand.value.takeIf { it.isNotEmpty() }?.get(0) == 1.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
service.aacpManager.registerControlCommandListener(controlCommandIdentifier, listener)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
service.aacpManager.unregisterControlCommandListener(controlCommandIdentifier, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (independent) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
if (title != null) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(animatedBackgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0x40888888) else Color(0x40D9D9D9)
|
||||||
|
tryAwaitRelease()
|
||||||
|
backgroundColor =
|
||||||
|
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
checked = !checked
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(55.dp)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (description != null) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed.value = true
|
||||||
|
tryAwaitRelease()
|
||||||
|
isPressed.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {
|
||||||
|
checked = !checked
|
||||||
|
cb()
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
if (description != null) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun StyledTogglePreview() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val sharedPrefs = context.getSharedPreferences("preview", 0)
|
||||||
|
StyledToggle(
|
||||||
|
label = "Example Toggle",
|
||||||
|
description = "This is an example description for the styled toggle.",
|
||||||
|
sharedPreferences = sharedPrefs
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -182,21 +182,31 @@ class AirPodsNotifications {
|
|||||||
if (data.size != 22) {
|
if (data.size != 22) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) {
|
// first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) {
|
||||||
Battery(first.component, first.level, data[10].toInt())
|
// Battery(first.component, first.level, data[10].toInt())
|
||||||
} else {
|
// } else {
|
||||||
Battery(data[7].toInt(), data[9].toInt(), data[10].toInt())
|
// Battery(data[7].toInt(), data[9].toInt(), data[10].toInt())
|
||||||
}
|
// }
|
||||||
second = if (data[15].toInt() == BatteryStatus.DISCONNECTED) {
|
// second = if (data[15].toInt() == BatteryStatus.DISCONNECTED) {
|
||||||
Battery(second.component, second.level, data[15].toInt())
|
// Battery(second.component, second.level, data[15].toInt())
|
||||||
} else {
|
// } else {
|
||||||
Battery(data[12].toInt(), data[14].toInt(), data[15].toInt())
|
// Battery(data[12].toInt(), data[14].toInt(), data[15].toInt())
|
||||||
}
|
// }
|
||||||
case = if (data[20].toInt() == BatteryStatus.DISCONNECTED && case.status != BatteryStatus.DISCONNECTED) {
|
// case = if (data[20].toInt() == BatteryStatus.DISCONNECTED && case.status != BatteryStatus.DISCONNECTED) {
|
||||||
Battery(case.component, case.level, data[20].toInt())
|
// Battery(case.component, case.level, data[20].toInt())
|
||||||
} else {
|
// } else {
|
||||||
Battery(data[17].toInt(), data[19].toInt(), data[20].toInt())
|
// Battery(data[17].toInt(), data[19].toInt(), data[20].toInt())
|
||||||
}
|
// }
|
||||||
|
// sometimes it shows battery as -1%, just skip all that and set it normally
|
||||||
|
first = Battery(
|
||||||
|
data[7].toInt(), data[9].toInt(), data[10].toInt()
|
||||||
|
)
|
||||||
|
second = Battery(
|
||||||
|
data[12].toInt(), data[14].toInt(), data[15].toInt()
|
||||||
|
)
|
||||||
|
case = Battery(
|
||||||
|
data[17].toInt(), data[19].toInt(), data[20].toInt()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBattery(): List<Battery> {
|
fun getBattery(): List<Battery> {
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
package me.kavishdevar.librepods.screens
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
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
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
@@ -44,35 +42,26 @@ 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.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
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.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
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.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.drawBehind
|
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -86,15 +75,11 @@ 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
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import dev.chrisbanes.haze.HazeEffectScope
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
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.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -105,18 +90,15 @@ import me.kavishdevar.librepods.R
|
|||||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.composables.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
|
||||||
import me.kavishdevar.librepods.composables.StyledDropdown
|
import me.kavishdevar.librepods.composables.StyledDropdown
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import 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
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
private var phoneMediaDebounceJob: Job? = null
|
private var phoneMediaDebounceJob: Job? = null
|
||||||
@@ -130,9 +112,6 @@ private const val TAG = "AccessibilitySettings"
|
|||||||
fun AccessibilitySettingsScreen(navController: NavController) {
|
fun AccessibilitySettingsScreen(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 hazeState = remember { HazeState() }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
val isSdpOffsetAvailable =
|
val isSdpOffsetAvailable =
|
||||||
remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
||||||
@@ -143,7 +122,7 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
val hearingAidEnabled = remember { mutableStateOf(
|
val hearingAidEnabled = remember { mutableStateOf(
|
||||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
|
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
|
||||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 0x01.toByte()
|
aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 0x01.toByte()
|
||||||
) }
|
) }
|
||||||
|
|
||||||
val hearingAidListener = remember {
|
val hearingAidListener = remember {
|
||||||
@@ -171,125 +150,30 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(
|
title = stringResource(R.string.accessibility),
|
||||||
0xFF000000
|
navigationButton = {
|
||||||
) else Color(
|
StyledIconButton(
|
||||||
0xFFF2F2F7
|
onClick = { navController.popBackStack() },
|
||||||
),
|
icon = "",
|
||||||
topBar = {
|
darkMode = isDarkTheme
|
||||||
val darkMode = isSystemInDarkTheme()
|
|
||||||
val mDensity = remember { mutableFloatStateOf(1f) }
|
|
||||||
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.accessibility),
|
|
||||||
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) }
|
) { spacerHeight, hazeState ->
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.hazeSource(hazeState)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.hazeSource(hazeState)
|
||||||
.padding(horizontal = 16.dp)
|
.verticalScroll(rememberScrollState()),
|
||||||
.verticalScroll(verticalScrollState),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
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 phoneMediaEQ = remember { mutableStateOf(FloatArray(8) { 0.5f }) }
|
||||||
val phoneEQEnabled = remember { mutableStateOf(false) }
|
val phoneEQEnabled = remember { mutableStateOf(false) }
|
||||||
val mediaEQEnabled = remember { mutableStateOf(false) }
|
val mediaEQEnabled = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val pressSpeedOptions = mapOf(
|
val pressSpeedOptions = mapOf(
|
||||||
0.toByte() to "Default",
|
0.toByte() to "Default",
|
||||||
1.toByte() to "Slower",
|
1.toByte() to "Slower",
|
||||||
@@ -402,7 +286,6 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounced write for phone/media EQ using AACP manager when values/toggles change
|
|
||||||
LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
|
LaunchedEffect(phoneMediaEQ.value, phoneEQEnabled.value, mediaEQEnabled.value) {
|
||||||
phoneMediaDebounceJob?.cancel()
|
phoneMediaDebounceJob?.cancel()
|
||||||
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
phoneMediaDebounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
@@ -541,7 +424,7 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenuComponent(
|
DropdownMenuComponent(
|
||||||
label = stringResource(R.string.volume_swipe_speed),
|
label = stringResource(R.string.volume_swipe_speed),
|
||||||
options = listOf(
|
options = listOf(
|
||||||
@@ -563,7 +446,7 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hearingAidEnabled.value) {
|
if (!hearingAidEnabled.value&& isSdpOffsetAvailable.value) {
|
||||||
NavigationButton(
|
NavigationButton(
|
||||||
to = "transparency_customization",
|
to = "transparency_customization",
|
||||||
name = stringResource(R.string.customize_transparency_mode),
|
name = stringResource(R.string.customize_transparency_mode),
|
||||||
@@ -881,22 +764,29 @@ fun AccessibilityToggle(
|
|||||||
}
|
}
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Box ( // for some reason, haze and backdrop don't work for uncontained text
|
||||||
text = description,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.fillMaxWidth()
|
||||||
)
|
.background(if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7), cornerShape)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
|
// modifier = Modifier
|
||||||
|
// .padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Composable
|
@Composable
|
||||||
private fun DropdownMenuComponent(
|
private fun DropdownMenuComponent(
|
||||||
label: String,
|
label: String,
|
||||||
@@ -904,7 +794,8 @@ private fun DropdownMenuComponent(
|
|||||||
selectedOption: String,
|
selectedOption: String,
|
||||||
onOptionSelected: (String) -> Unit,
|
onOptionSelected: (String) -> Unit,
|
||||||
textColor: Color,
|
textColor: Color,
|
||||||
hazeState: HazeState
|
hazeState: HazeState,
|
||||||
|
description: String? = null,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val itemHeightPx = with(density) { 48.dp.toPx() }
|
val itemHeightPx = with(density) { 48.dp.toPx() }
|
||||||
@@ -1020,5 +911,21 @@ private fun DropdownMenuComponent(
|
|||||||
hazeState = hazeState
|
hazeState = hazeState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7))
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
text = description ?: "",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,40 +16,51 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalEncodingApi::class)
|
package me.kavishdevar.librepods.screens
|
||||||
|
|
||||||
package me.kavishdevar.librepods.composables
|
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
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.fillMaxWidth
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.navigation.NavController
|
||||||
|
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.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
|
private var debounceJob: Job? = null
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveStrengthSlider() {
|
fun AdaptiveStrengthScreen(navController: NavController) {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
val sliderValue = remember { mutableFloatStateOf(0f) }
|
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||||||
val service = ServiceManager.getService()!!
|
val service = ServiceManager.getService()!!
|
||||||
|
|
||||||
LaunchedEffect(sliderValue) {
|
LaunchedEffect(sliderValue) {
|
||||||
val sliderValueFromAACP = service.aacpManager.controlCommandStatusList.find {
|
val sliderValueFromAACP = service.aacpManager.controlCommandStatusList.find {
|
||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH
|
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH
|
||||||
@@ -82,38 +93,44 @@ fun AdaptiveStrengthSlider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
StyledScaffold(
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
title = stringResource(R.string.customize_adaptive_audio),
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
navigationButton = {
|
||||||
|
StyledIconButton(
|
||||||
Column(
|
onClick = { navController.popBackStack() },
|
||||||
modifier = Modifier
|
icon = "",
|
||||||
.fillMaxWidth(),
|
darkMode = isDarkTheme
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
)
|
||||||
) {
|
}
|
||||||
StyledSlider(
|
) { spacerHeight ->
|
||||||
mutableFloatState = sliderValue,
|
Column(
|
||||||
onValueChange = {
|
modifier = Modifier
|
||||||
sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f))
|
.fillMaxSize(),
|
||||||
},
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
valueRange = 0f..100f,
|
) {
|
||||||
snapPoints = listOf(0f, 50f, 100f),
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
startLabel = stringResource(R.string.less_noise),
|
StyledSlider(
|
||||||
endLabel = stringResource(R.string.more_noise),
|
label = stringResource(R.string.customize_adaptive_audio).uppercase(),
|
||||||
independent = false
|
mutableFloatState = sliderValue,
|
||||||
)
|
onValueChange = {
|
||||||
|
sliderValue.floatValue = it
|
||||||
|
debounceJob?.cancel()
|
||||||
|
debounceJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
delay(300)
|
||||||
|
service.aacpManager.sendControlCommand(
|
||||||
|
AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value,
|
||||||
|
(100 - it).toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
snapPoints = listOf(0f, 50f, 100f),
|
||||||
|
startIcon = "",
|
||||||
|
endIcon = "",
|
||||||
|
independent = true,
|
||||||
|
description = stringResource(R.string.adaptive_audio_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun AdaptiveStrengthSliderPreview() {
|
|
||||||
AdaptiveStrengthSlider()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -41,34 +41,19 @@ import androidx.compose.foundation.layout.padding
|
|||||||
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.filled.Info
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
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.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.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.drawBehind
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -83,24 +68,26 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
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 com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import com.kyant.backdrop.drawBackdrop
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
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.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
|
||||||
import me.kavishdevar.librepods.composables.ConnectionSettings
|
import me.kavishdevar.librepods.composables.ConnectionSettings
|
||||||
import me.kavishdevar.librepods.composables.IndependentToggle
|
|
||||||
import me.kavishdevar.librepods.composables.MicrophoneSettings
|
import me.kavishdevar.librepods.composables.MicrophoneSettings
|
||||||
import me.kavishdevar.librepods.composables.NameField
|
import me.kavishdevar.librepods.composables.NameField
|
||||||
import me.kavishdevar.librepods.composables.NavigationButton
|
import me.kavishdevar.librepods.composables.NavigationButton
|
||||||
import me.kavishdevar.librepods.composables.NoiseControlSettings
|
import me.kavishdevar.librepods.composables.NoiseControlSettings
|
||||||
import me.kavishdevar.librepods.composables.PressAndHoldSettings
|
import me.kavishdevar.librepods.composables.PressAndHoldSettings
|
||||||
|
import me.kavishdevar.librepods.composables.StyledButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
import me.kavishdevar.librepods.constants.AirPodsNotifications
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
@@ -144,8 +131,6 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val verticalScrollState = rememberScrollState()
|
|
||||||
val hazeState = rememberHazeState( blurEnabled = true )
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -153,12 +138,6 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
isRemotelyConnected = connected
|
isRemotelyConnected = connected
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackbar(message: String) {
|
|
||||||
coroutineScope.launch {
|
|
||||||
snackbarHostState.showSnackbar(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val connectionReceiver = remember {
|
val connectionReceiver = remember {
|
||||||
@@ -219,118 +198,38 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val darkMode = isSystemInDarkTheme()
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
StyledScaffold(
|
||||||
Scaffold(
|
title = deviceName.text,
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(
|
actionButtons = listOf {
|
||||||
0xFF000000
|
StyledIconButton(
|
||||||
) else Color(
|
onClick = { navController.navigate("app_settings") },
|
||||||
0xFFF2F2F7
|
icon = "",
|
||||||
),
|
darkMode = darkMode
|
||||||
topBar = {
|
|
||||||
val darkMode = isSystemInDarkTheme()
|
|
||||||
val mDensity = remember { mutableFloatStateOf(1f) }
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = deviceName.text,
|
|
||||||
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
|
|
||||||
),
|
|
||||||
actions = {
|
|
||||||
if (isRemotelyConnected) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
showSnackbar("Connected remotely to AirPods via Linux.")
|
|
||||||
},
|
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Info,
|
|
||||||
contentDescription = "Info",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
navController.navigate("app_settings")
|
|
||||||
},
|
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Settings,
|
|
||||||
contentDescription = "Settings",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
snackbarHostState = snackbarHostState
|
||||||
) { paddingValues ->
|
) { spacerHeight, hazeState ->
|
||||||
if (isLocallyConnected || isRemotelyConnected) {
|
if (isLocallyConnected || isRemotelyConnected) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.hazeSource(hazeState)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp)
|
.hazeSource(hazeState)
|
||||||
.verticalScroll(
|
.verticalScroll(rememberScrollState())
|
||||||
state = verticalScrollState,
|
|
||||||
enabled = true,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(75.dp))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
LaunchedEffect(service) {
|
LaunchedEffect(service) {
|
||||||
service.let {
|
service.let {
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.Companion.BATTERY_DATA).apply {
|
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
||||||
})
|
})
|
||||||
it.sendBroadcast(Intent(AirPodsNotifications.Companion.ANC_DATA).apply {
|
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
putExtra("data", it.getANC())
|
putExtra("data", it.getANC())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(64.dp))
|
|
||||||
|
|
||||||
BatteryView(service = service)
|
BatteryView(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
// Show BLE-only mode indicator
|
// Show BLE-only mode indicator
|
||||||
@@ -372,7 +271,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
PressAndHoldSettings(navController = navController)
|
PressAndHoldSettings(navController = navController)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
AudioSettings()
|
AudioSettings(navController = navController)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
ConnectionSettings()
|
ConnectionSettings()
|
||||||
@@ -381,11 +280,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
MicrophoneSettings(hazeState)
|
MicrophoneSettings(hazeState)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IndependentToggle(
|
StyledToggle(
|
||||||
name = stringResource(R.string.sleep_detection),
|
label = stringResource(R.string.sleep_detection),
|
||||||
service = service,
|
|
||||||
sharedPreferences = sharedPreferences,
|
|
||||||
default = false,
|
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.SLEEP_DETECTION_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -396,11 +292,8 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
NavigationButton(to = "accessibility", "Accessibility", navController = navController)
|
NavigationButton(to = "accessibility", "Accessibility", navController = navController)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
IndependentToggle(
|
StyledToggle(
|
||||||
name = stringResource(R.string.off_listening_mode),
|
label = stringResource(R.string.off_listening_mode).uppercase(),
|
||||||
service = service,
|
|
||||||
sharedPreferences = sharedPreferences,
|
|
||||||
default = false,
|
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION,
|
||||||
description = stringResource(R.string.off_listening_mode_description)
|
description = stringResource(R.string.off_listening_mode_description)
|
||||||
)
|
)
|
||||||
@@ -415,19 +308,24 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
val backdrop = rememberLayerBackdrop()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 8.dp)
|
.drawBackdrop(
|
||||||
.verticalScroll(
|
backdrop = rememberLayerBackdrop(),
|
||||||
state = verticalScrollState,
|
exportedBackdrop = backdrop,
|
||||||
enabled = true,
|
shape = { RoundedCornerShape(0.dp) },
|
||||||
),
|
highlight = {
|
||||||
|
Highlight.AmbientDefault.copy(alpha = 0f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "AirPods not connected",
|
text = stringResource(R.string.airpods_not_connected),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -439,7 +337,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Please connect your AirPods to access settings.",
|
text = stringResource(R.string.airpods_not_connected_description),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -450,13 +348,9 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
Button(
|
StyledButton(
|
||||||
onClick = { navController.navigate("troubleshooting") },
|
onClick = { navController.navigate("troubleshooting") },
|
||||||
shape = RoundedCornerShape(10.dp),
|
backdrop = backdrop
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFF2F2F7),
|
|
||||||
contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Troubleshoot Connection",
|
text = "Troubleshoot Connection",
|
||||||
@@ -472,7 +366,6 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreenPreview() {
|
fun AirPodsSettingsScreenPreview() {
|
||||||
|
|||||||
@@ -42,38 +42,31 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
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.KeyboardArrowLeft
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.drawBehind
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
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
|
||||||
@@ -82,39 +75,32 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.navigation.NavController
|
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.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(navController: NavController) {
|
fun AppSettingsScreen(navController: NavController) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val name = remember { mutableStateOf(sharedPreferences.getString("name", "") ?: "") }
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
val hazeState = remember { HazeState() }
|
|
||||||
|
|
||||||
var showResetDialog by remember { mutableStateOf(false) }
|
var showResetDialog by remember { mutableStateOf(false) }
|
||||||
var showIrkDialog by remember { mutableStateOf(false) }
|
var showIrkDialog by remember { mutableStateOf(false) }
|
||||||
@@ -134,6 +120,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
irkValue = decoded.joinToString("") { "%02x".format(it) }
|
irkValue = decoded.joinToString("") { "%02x".format(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
irkValue = ""
|
irkValue = ""
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +130,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
encKeyValue = decoded.joinToString("") { "%02x".format(it) }
|
encKeyValue = decoded.joinToString("") { "%02x".format(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
encKeyValue = ""
|
encKeyValue = ""
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,8 +186,6 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mDensity by remember { mutableFloatStateOf(0f) }
|
|
||||||
|
|
||||||
fun validateHexInput(input: String): Boolean {
|
fun validateHexInput(input: String): Boolean {
|
||||||
val hexPattern = Regex("^[0-9a-fA-F]{32}$")
|
val hexPattern = Regex("^[0-9a-fA-F]{32}$")
|
||||||
return hexPattern.matches(input)
|
return hexPattern.matches(input)
|
||||||
@@ -210,84 +196,24 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
|
|
||||||
BackHandler(enabled = isProcessingSdp) {}
|
BackHandler(enabled = isProcessingSdp) {}
|
||||||
|
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
title = stringResource(R.string.app_settings),
|
||||||
topBar = {
|
navigationButton = {
|
||||||
CenterAlignedTopAppBar(
|
StyledIconButton(
|
||||||
modifier = Modifier.hazeEffect(
|
onClick = { navController.popBackStack() },
|
||||||
state = hazeState,
|
icon = "",
|
||||||
style = CupertinoMaterials.thick(),
|
darkMode = isDarkTheme
|
||||||
block = fun HazeEffectScope.() {
|
|
||||||
alpha =
|
|
||||||
if (scrollState.value > 60.dp.value * mDensity) 1f else 0f
|
|
||||||
})
|
|
||||||
.drawBehind {
|
|
||||||
mDensity = density
|
|
||||||
val strokeWidth = 0.7.dp.value * density
|
|
||||||
val y = size.height - strokeWidth / 2
|
|
||||||
if (scrollState.value > 60.dp.value * density) {
|
|
||||||
drawLine(
|
|
||||||
if (isDarkTheme) Color.DarkGray else Color.LightGray,
|
|
||||||
Offset(0f, y),
|
|
||||||
Offset(size.width, y),
|
|
||||||
strokeWidth
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.app_settings),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
if (!isProcessingSdp) {
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !isProcessingSdp,
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
modifier = Modifier.width(180.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = name.value,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
|
||||||
containerColor = Color.Transparent
|
|
||||||
),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
) { spacerHeight, hazeState ->
|
||||||
else Color(0xFFF2F2F7),
|
Column(
|
||||||
) { paddingValues ->
|
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.hazeSource(state = hazeState)
|
.hazeSource(state = hazeState)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
@@ -295,7 +221,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Widget".uppercase(),
|
text = stringResource(R.string.widget).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -335,13 +261,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Show phone battery in widget",
|
text = stringResource(R.string.show_phone_battery_in_widget),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Display your phone's battery level in the widget alongside AirPods battery",
|
text = stringResource(R.string.show_phone_battery_in_widget_description),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -359,7 +285,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Connection Mode".uppercase(),
|
text = stringResource(R.string.connection_mode).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -399,12 +325,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "BLE Only Mode",
|
text = stringResource(R.string.ble_only_mode),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Only use Bluetooth Low Energy for battery data and ear detection. Disables advanced features requiring L2CAP connection.",
|
text = stringResource(R.string.ble_only_mode_description),
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -422,7 +348,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Conversational Awareness".uppercase(),
|
text = stringResource(R.string.conversational_awareness).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -539,7 +465,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Conversational Awareness Volume",
|
text = stringResource(R.string.conversational_awareness_volume),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
|
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
|
||||||
@@ -628,7 +554,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Quick Settings Tile".uppercase(),
|
text = stringResource(R.string.quick_settings_tile).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -672,15 +598,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Open dialog for controlling",
|
text = stringResource(R.string.open_dialog_for_controlling),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (openDialogForControlling)
|
text = stringResource(R.string.open_dialog_for_controlling_description),
|
||||||
"If disabled, clicking on the QS will cycle through modes"
|
|
||||||
else "If enabled, it will show a dialog for controlling noise control mode and conversational awareness",
|
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -697,7 +621,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Ear Detection".uppercase(),
|
text = stringResource(R.string.ear_detection).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -741,13 +665,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Disconnect AirPods when not wearing",
|
text = stringResource(R.string.disconnect_when_not_wearing),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "You will still be able to control them with the app - this just disconnects the audio.",
|
text = stringResource(R.string.disconnect_when_not_wearing_description),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -1051,7 +975,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Advanced Options".uppercase(),
|
text = stringResource(R.string.advanced_options).uppercase(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
@@ -1087,13 +1011,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Set Identity Resolving Key (IRK)",
|
text = stringResource(R.string.set_identity_resolving_key),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Manually set the IRK value used for resolving BLE random addresses",
|
text = stringResource(R.string.set_identity_resolving_key_description),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -1116,13 +1040,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Set Encryption Key",
|
text = stringResource(R.string.set_encryption_key),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Manually set the ENC_KEY value used for decrypting BLE advertisements",
|
text = stringResource(R.string.set_encryption_key_description),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -1152,13 +1076,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Use alternate head tracking packets",
|
text = stringResource(R.string.use_alternate_head_tracking_packets),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Enable this if head tracking doesn't work for you. This sends different data to AirPods for requesting/stopping head tracking data.",
|
text = stringResource(R.string.use_alternate_head_tracking_packets_description),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -1206,6 +1130,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
actAsAppleDevice = RadareOffsetFinder.isSdpOffsetAvailable()
|
actAsAppleDevice = RadareOffsetFinder.isSdpOffsetAvailable()
|
||||||
}
|
}
|
||||||
|
val restartBluetoothText = stringResource(R.string.found_offset_restart_bluetooth)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1222,9 +1147,9 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
val radareOffsetFinder = RadareOffsetFinder(context)
|
val radareOffsetFinder = RadareOffsetFinder(context)
|
||||||
val success = radareOffsetFinder.findSdpOffset() ?: false
|
val success = radareOffsetFinder.findSdpOffset()
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.makeText(context, "Found offset please restart the Bluetooth process", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, restartBluetoothText, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RadareOffsetFinder.clearSdpOffset()
|
RadareOffsetFinder.clearSdpOffset()
|
||||||
@@ -1242,13 +1167,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Act as an Apple device",
|
text = stringResource(R.string.act_as_an_apple_device),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Enables multi-device connectivity and Accessibility features like customizing transparency mode (amplification, tone, ambient noise reduction, conversation boost, and EQ)",
|
text = stringResource(R.string.act_as_an_apple_device_description),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor.copy(0.6f),
|
color = textColor.copy(0.6f),
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
@@ -1256,14 +1181,13 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
if (actAsAppleDevice) {
|
if (actAsAppleDevice) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Might be unstable!! A maximum of two devices can be connected to your AirPods. If you are using with an Apple device like an iPad or Mac, then please connect that device first and then your Android.",
|
text = stringResource(R.string.act_as_an_apple_device_warning),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
lineHeight = 14.sp,
|
lineHeight = 14.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledSwitch(
|
StyledSwitch(
|
||||||
checked = actAsAppleDevice,
|
checked = actAsAppleDevice,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
@@ -1273,9 +1197,9 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
if (it) {
|
if (it) {
|
||||||
val radareOffsetFinder = RadareOffsetFinder(context)
|
val radareOffsetFinder = RadareOffsetFinder(context)
|
||||||
val success = radareOffsetFinder.findSdpOffset() ?: false
|
val success = radareOffsetFinder.findSdpOffset()
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.makeText(context, "Found offset please restart the Bluetooth process", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, restartBluetoothText, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RadareOffsetFinder.clearSdpOffset()
|
RadareOffsetFinder.clearSdpOffset()
|
||||||
@@ -1313,7 +1237,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Reset Hook Offset",
|
text = stringResource(R.string.reset_hook_offset),
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
@@ -1338,17 +1262,19 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
"This will clear the current hook offset and require you to go through the setup process again. Are you sure you want to continue?",
|
stringResource(R.string.reset_hook_offset_description),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
val successText = stringResource(R.string.hook_offset_reset_success)
|
||||||
|
val failureText = stringResource(R.string.hook_offset_reset_failure)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (RadareOffsetFinder.clearHookOffsets()) {
|
if (RadareOffsetFinder.clearHookOffsets()) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"Hook offset has been reset. Redirecting to setup...",
|
successText,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
|
|
||||||
@@ -1358,7 +1284,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"Failed to reset hook offset",
|
failureText,
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
@@ -1369,7 +1295,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Reset",
|
stringResource(R.string.reset),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
@@ -1394,7 +1320,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
onDismissRequest = { showIrkDialog = false },
|
onDismissRequest = { showIrkDialog = false },
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
"Set Identity Resolving Key (IRK)",
|
stringResource(R.string.set_identity_resolving_key),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
@@ -1402,7 +1328,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
"Enter 16-byte IRK as hex string (32 characters):",
|
stringResource(R.string.enter_irk_hex),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
)
|
)
|
||||||
@@ -1425,14 +1351,16 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
supportingText = {
|
supportingText = {
|
||||||
if (irkError != null) {
|
if (irkError != null) {
|
||||||
Text(irkError!!, color = MaterialTheme.colorScheme.error)
|
Text(stringResource(R.string.must_be_32_hex_chars), color = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = { Text("IRK Hex Value") }
|
label = { Text(stringResource(R.string.irk_hex_value)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
val successText = stringResource(R.string.irk_set_success)
|
||||||
|
val errorText = stringResource(R.string.error_converting_hex)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!validateHexInput(irkValue)) {
|
if (!validateHexInput(irkValue)) {
|
||||||
@@ -1450,10 +1378,10 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
val base64Value = Base64.encode(hexBytes)
|
val base64Value = Base64.encode(hexBytes)
|
||||||
sharedPreferences.edit { putString(AACPManager.Companion.ProximityKeyType.IRK.name, base64Value)}
|
sharedPreferences.edit { putString(AACPManager.Companion.ProximityKeyType.IRK.name, base64Value)}
|
||||||
|
|
||||||
Toast.makeText(context, "IRK has been set successfully", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
|
||||||
showIrkDialog = false
|
showIrkDialog = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
irkError = "Error converting hex: ${e.message}"
|
irkError = errorText + " " + (e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -1483,7 +1411,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
onDismissRequest = { showEncKeyDialog = false },
|
onDismissRequest = { showEncKeyDialog = false },
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
"Set Encryption Key",
|
stringResource(R.string.set_encryption_key),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
@@ -1491,7 +1419,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
"Enter 16-byte ENC_KEY as hex string (32 characters):",
|
stringResource(R.string.enter_enc_key_hex),
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
)
|
)
|
||||||
@@ -1514,14 +1442,16 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
supportingText = {
|
supportingText = {
|
||||||
if (encKeyError != null) {
|
if (encKeyError != null) {
|
||||||
Text(encKeyError!!, color = MaterialTheme.colorScheme.error)
|
Text(stringResource(R.string.must_be_32_hex_chars), color = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = { Text("ENC_KEY Hex Value") }
|
label = { Text(stringResource(R.string.enc_key_hex_value)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
val successText = stringResource(R.string.encryption_key_set_success)
|
||||||
|
val errorText = stringResource(R.string.error_converting_hex)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!validateHexInput(encKeyValue)) {
|
if (!validateHexInput(encKeyValue)) {
|
||||||
@@ -1539,10 +1469,10 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
val base64Value = Base64.encode(hexBytes)
|
val base64Value = Base64.encode(hexBytes)
|
||||||
sharedPreferences.edit { putString(AACPManager.Companion.ProximityKeyType.ENC_KEY.name, base64Value)}
|
sharedPreferences.edit { putString(AACPManager.Companion.ProximityKeyType.ENC_KEY.name, base64Value)}
|
||||||
|
|
||||||
Toast.makeText(context, "Encryption key has been set successfully", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
|
||||||
showEncKeyDialog = false
|
showEncKeyDialog = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
encKeyError = "Error converting hex: ${e.message}"
|
encKeyError = errorText + " " + (e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -29,10 +29,8 @@ import android.widget.Toast
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -50,37 +48,25 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
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.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
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.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
@@ -92,15 +78,13 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
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.navigation.NavController
|
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.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
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.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.constants.BatteryStatus
|
import me.kavishdevar.librepods.constants.BatteryStatus
|
||||||
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
@@ -304,52 +288,24 @@ fun parseOutgoingPacket(bytes: ByteArray, rawData: String): PacketInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun IOSCheckbox(
|
|
||||||
checked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.clickable { onCheckedChange(!checked) },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (checked) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = "Checked",
|
|
||||||
tint = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "UnspecifiedRegisterReceiverFlag")
|
||||||
@Composable
|
@Composable
|
||||||
fun DebugScreen(navController: NavController) {
|
fun DebugScreen(navController: NavController) {
|
||||||
val hazeState = remember { HazeState() }
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scrollOffset by remember { derivedStateOf { listState.firstVisibleItemScrollOffset } }
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val showMenu = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val airPodsService = remember { ServiceManager.getService() }
|
val airPodsService = remember { ServiceManager.getService() }
|
||||||
val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ?: emptySet()
|
val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ?: emptySet()
|
||||||
val shouldScrollToBottom = remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
val refreshTrigger = remember { mutableIntStateOf(0) }
|
val refreshTrigger = remember { mutableIntStateOf(0) }
|
||||||
LaunchedEffect(refreshTrigger.intValue) {
|
LaunchedEffect(refreshTrigger.intValue) {
|
||||||
while(true) {
|
while(true) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
refreshTrigger.intValue = refreshTrigger.intValue + 1
|
refreshTrigger.intValue += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,137 +319,42 @@ fun DebugScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(packetLogs.size, refreshTrigger.intValue) {
|
LaunchedEffect(packetLogs.size, refreshTrigger.intValue) {
|
||||||
if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
|
if (packetLogs.isNotEmpty()) {
|
||||||
listState.animateScrollToItem(packetLogs.size - 1)
|
listState.animateScrollToItem(packetLogs.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
topBar = {
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = { Text("Debug") },
|
|
||||||
navigationIcon = {
|
|
||||||
TextButton(
|
|
||||||
onClick = { navController.popBackStack() },
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
sharedPreferences.getString("name", "AirPods")!!,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = { showMenu.value = true }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = "More Options",
|
|
||||||
tint = if (isSystemInDarkTheme()) Color.White else Color.Black
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
StyledScaffold(
|
||||||
expanded = showMenu.value,
|
title = "Debug",
|
||||||
onDismissRequest = { showMenu.value = false },
|
navigationButton = {
|
||||||
modifier = Modifier
|
StyledIconButton(
|
||||||
.width(250.dp)
|
onClick = { navController.popBackStack() },
|
||||||
.background(
|
icon = "",
|
||||||
if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7)
|
darkMode = isDarkTheme
|
||||||
)
|
|
||||||
.padding(vertical = 4.dp)
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Auto-scroll",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
IOSCheckbox(
|
|
||||||
checked = shouldScrollToBottom.value,
|
|
||||||
onCheckedChange = { shouldScrollToBottom.value = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
shouldScrollToBottom.value = !shouldScrollToBottom.value
|
|
||||||
showMenu.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
color = if (isSystemInDarkTheme()) Color(0xFF3A3A3C) else Color(0xFFE5E5EA),
|
|
||||||
thickness = 0.5.dp
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Clear logs",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Normal
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = "Clear logs",
|
|
||||||
tint = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
ServiceManager.getService()?.clearLogs()
|
|
||||||
expandedItems.value = emptySet()
|
|
||||||
showMenu.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.hazeEffect(
|
|
||||||
state = hazeState,
|
|
||||||
style = CupertinoMaterials.thick(),
|
|
||||||
block = fun HazeEffectScope.() {
|
|
||||||
alpha = if (scrollOffset > 0) 1f else 0f
|
|
||||||
}),
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7),
|
actionButtons = listOf(
|
||||||
) { paddingValues ->
|
{
|
||||||
|
StyledIconButton(
|
||||||
|
onClick = {
|
||||||
|
airPodsService?.clearLogs()
|
||||||
|
expandedItems.value = emptySet()
|
||||||
|
},
|
||||||
|
icon = "",
|
||||||
|
darkMode = isDarkTheme,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) { spacerHeight, hazeState ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.padding(top = paddingValues.calculateTopPadding())
|
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -509,7 +370,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
.padding(vertical = 2.dp)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
expandedItems.value = if (isExpanded) {
|
expandedItems.value = if (isExpanded) {
|
||||||
@@ -528,67 +389,65 @@ fun DebugScreen(navController: NavController) {
|
|||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7),
|
containerColor = if (isSystemInDarkTheme()) Color(0xFF1C1B20) else Color(0xFFF2F2F7),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Icon(
|
||||||
Icon(
|
imageVector = if (isSent) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
imageVector = if (isSent) Icons.AutoMirrored.Filled.KeyboardArrowLeft else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
contentDescription = null,
|
||||||
contentDescription = null,
|
tint = if (isSent) Color.Green else Color.Red,
|
||||||
tint = if (isSent) Color.Green else Color.Red,
|
modifier = Modifier.size(24.dp)
|
||||||
modifier = Modifier.size(24.dp)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = if (packetInfo.isUnknown) {
|
||||||
|
val shortenedData = packetInfo.rawData.take(60) +
|
||||||
|
(if (packetInfo.rawData.length > 60) "..." else "")
|
||||||
|
shortenedData
|
||||||
|
} else {
|
||||||
|
"${packetInfo.type}: ${packetInfo.description}"
|
||||||
|
},
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.hack))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
if (isExpanded) {
|
||||||
Column {
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
if (packetInfo.parsedData.isNotEmpty()) {
|
||||||
|
packetInfo.parsedData.forEach { (key, value) ->
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = "$key: ",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily(Font(R.font.hack))
|
||||||
|
),
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.hack))
|
||||||
|
),
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = if (packetInfo.isUnknown) {
|
text = "Raw: ${packetInfo.rawData}",
|
||||||
val shortenedData = packetInfo.rawData.take(60) +
|
|
||||||
(if (packetInfo.rawData.length > 60) "..." else "")
|
|
||||||
shortenedData
|
|
||||||
} else {
|
|
||||||
"${packetInfo.type}: ${packetInfo.description}"
|
|
||||||
},
|
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontFamily = FontFamily(Font(R.font.hack))
|
fontFamily = FontFamily(Font(R.font.hack))
|
||||||
)
|
),
|
||||||
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
if (isExpanded) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
if (packetInfo.parsedData.isNotEmpty()) {
|
|
||||||
packetInfo.parsedData.forEach { (key, value) ->
|
|
||||||
Row {
|
|
||||||
Text(
|
|
||||||
text = "$key: ",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontFamily = FontFamily(Font(R.font.hack))
|
|
||||||
),
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.hack))
|
|
||||||
),
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Raw: ${packetInfo.rawData}",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.hack))
|
|
||||||
),
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -627,7 +486,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
packet.value = TextFieldValue("")
|
packet.value = TextFieldValue("")
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
|
|
||||||
if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
|
if (packetLogs.isNotEmpty()) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
try {
|
try {
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|||||||
@@ -41,25 +41,15 @@ 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.width
|
|
||||||
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.automirrored.filled.KeyboardArrowLeft
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
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
|
||||||
@@ -74,22 +64,16 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
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.drawBehind
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.asAndroidPath
|
import androidx.compose.ui.graphics.asAndroidPath
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.graphics.vector.path
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
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
|
||||||
@@ -99,22 +83,19 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
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 androidx.navigation.NavController
|
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.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
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.IndependentToggle
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.HeadTracking
|
import me.kavishdevar.librepods.utils.HeadTracking
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
@@ -134,147 +115,59 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
ServiceManager.getService()?.stopHeadTracking()
|
ServiceManager.getService()?.stopHeadTracking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
val hazeState = remember { HazeState() }
|
|
||||||
|
|
||||||
var mDensity by remember { mutableFloatStateOf(0f) }
|
StyledScaffold (
|
||||||
Scaffold(
|
title = stringResource(R.string.head_tracking),
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
navigationButton = {
|
||||||
topBar = {
|
StyledIconButton(
|
||||||
CenterAlignedTopAppBar(
|
onClick = { navController.popBackStack() },
|
||||||
modifier = Modifier.hazeEffect(
|
icon = "",
|
||||||
state = hazeState,
|
darkMode = isDarkTheme
|
||||||
style = CupertinoMaterials.thick(),
|
|
||||||
block = fun HazeEffectScope.() {
|
|
||||||
alpha =
|
|
||||||
if (scrollState.value > 60.dp.value * mDensity) 1f else 0f
|
|
||||||
})
|
|
||||||
.drawBehind {
|
|
||||||
mDensity = density
|
|
||||||
val strokeWidth = 0.7.dp.value * density
|
|
||||||
val y = size.height - strokeWidth / 2
|
|
||||||
if (scrollState.value > 60.dp.value * density) {
|
|
||||||
drawLine(
|
|
||||||
if (isDarkTheme) Color.DarkGray else Color.LightGray,
|
|
||||||
Offset(0f, y),
|
|
||||||
Offset(size.width, y),
|
|
||||||
strokeWidth
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.head_tracking),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
navController.popBackStack()
|
|
||||||
if (ServiceManager.getService()?.isHeadTrackingActive == true) ServiceManager.getService()?.stopHeadTracking()
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
modifier = Modifier.width(180.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
sharedPreferences.getString("name", "AirPods")!!,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
|
||||||
containerColor = Color.Transparent
|
|
||||||
),
|
|
||||||
actions = {
|
|
||||||
var isActive by remember { mutableStateOf(ServiceManager.getService()?.isHeadTrackingActive == true) }
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (ServiceManager.getService()?.isHeadTrackingActive == false) {
|
|
||||||
ServiceManager.getService()?.startHeadTracking()
|
|
||||||
Log.d("HeadTrackingScreen", "Head tracking started")
|
|
||||||
isActive = true
|
|
||||||
} else {
|
|
||||||
ServiceManager.getService()?.stopHeadTracking()
|
|
||||||
Log.d("HeadTrackingScreen", "Head tracking stopped")
|
|
||||||
isActive = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
if (isActive) {
|
|
||||||
ImageVector.Builder(
|
|
||||||
name = "Pause",
|
|
||||||
defaultWidth = 24.dp,
|
|
||||||
defaultHeight = 24.dp,
|
|
||||||
viewportWidth = 24f,
|
|
||||||
viewportHeight = 24f
|
|
||||||
).apply {
|
|
||||||
path(
|
|
||||||
fill = SolidColor(Color.Black),
|
|
||||||
pathBuilder = {
|
|
||||||
moveTo(6f, 5f)
|
|
||||||
lineTo(10f, 5f)
|
|
||||||
lineTo(10f, 19f)
|
|
||||||
lineTo(6f, 19f)
|
|
||||||
lineTo(6f, 5f)
|
|
||||||
moveTo(14f, 5f)
|
|
||||||
lineTo(18f, 5f)
|
|
||||||
lineTo(18f, 19f)
|
|
||||||
lineTo(14f, 19f)
|
|
||||||
lineTo(14f, 5f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}.build()
|
|
||||||
} else Icons.Filled.PlayArrow,
|
|
||||||
contentDescription = "Start",
|
|
||||||
tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
actionButtons = listOf(
|
||||||
else Color(0xFFF2F2F7),
|
{
|
||||||
) { paddingValues ->
|
var isActive by remember { mutableStateOf(ServiceManager.getService()?.isHeadTrackingActive == true) }
|
||||||
|
StyledIconButton(
|
||||||
|
onClick = {
|
||||||
|
if (ServiceManager.getService()?.isHeadTrackingActive == false) {
|
||||||
|
ServiceManager.getService()?.startHeadTracking()
|
||||||
|
Log.d("HeadTrackingScreen", "Head tracking started")
|
||||||
|
} else {
|
||||||
|
ServiceManager.getService()?.stopHeadTracking()
|
||||||
|
Log.d("HeadTrackingScreen", "Head tracking stopped")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = if (isActive) "" else "",
|
||||||
|
darkMode = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) { spacerHeight, hazeState ->
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues = paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.hazeSource(state = hazeState)
|
.hazeSource(state = hazeState)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val sharedPreferences =
|
val sharedPreferences =
|
||||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
var gestureText by remember { mutableStateOf("") }
|
var gestureText by remember { mutableStateOf("") }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
IndependentToggle(name = "Head Gestures", sharedPreferences = sharedPreferences)
|
StyledToggle(
|
||||||
|
label = "Head Gestures",
|
||||||
|
sharedPreferences = sharedPreferences,
|
||||||
|
sharedPreferenceKey = "head_gestures",
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.head_gestures_details),
|
stringResource(R.string.head_gestures_details),
|
||||||
@@ -302,7 +195,7 @@ fun HeadTrackingScreen(navController: NavController) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
"Acceleration",
|
"Velocity",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -441,14 +334,13 @@ private fun ParticleText(
|
|||||||
|
|
||||||
if (particles.isEmpty()) {
|
if (particles.isEmpty()) {
|
||||||
val random = Random(System.currentTimeMillis())
|
val random = Random(System.currentTimeMillis())
|
||||||
for (i in 0..100) {
|
for (@Suppress("Unused")i in 0..100) {
|
||||||
val x = centerX + random.nextFloat() * textBounds.width
|
val x = centerX + random.nextFloat() * textBounds.width
|
||||||
val y = centerY - textBounds.height / 2 + random.nextFloat() * textBounds.height
|
val y = centerY - textBounds.height / 2 + random.nextFloat() * textBounds.height
|
||||||
val vx = (random.nextFloat() - 0.5f) * 20
|
val vx = (random.nextFloat() - 0.5f) * 20
|
||||||
val vy = (random.nextFloat() - 0.5f) * 20
|
val vy = (random.nextFloat() - 0.5f) * 20
|
||||||
particles.add(Particle(Offset(x, y), Offset(vx, vy)))
|
particles.add(Particle(Offset(x, y), Offset(vx, vy)))
|
||||||
}
|
}
|
||||||
textVisible = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
particles.forEach { particle ->
|
particles.forEach { particle ->
|
||||||
@@ -518,14 +410,12 @@ private fun HeadVisualization() {
|
|||||||
fun rotate3D(point: Triple<Float, Float, Float>): Triple<Float, Float, Float> {
|
fun rotate3D(point: Triple<Float, Float, Float>): Triple<Float, Float, Float> {
|
||||||
val (x, y, z) = point
|
val (x, y, z) = point
|
||||||
val x1 = x * cosY - z * sinY
|
val x1 = x * cosY - z * sinY
|
||||||
val y1 = y
|
|
||||||
val z1 = x * sinY + z * cosY
|
val z1 = x * sinY + z * cosY
|
||||||
|
|
||||||
val x2 = x1
|
val y2 = y * cosP - z1 * sinP
|
||||||
val y2 = y1 * cosP - z1 * sinP
|
val z2 = y * sinP + z1 * cosP
|
||||||
val z2 = y1 * sinP + z1 * cosP
|
|
||||||
|
|
||||||
return Triple(x2, y2, z2)
|
return Triple(x1, y2, z2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun project(point: Triple<Float, Float, Float>): Pair<Float, Float> {
|
fun project(point: Triple<Float, Float, Float>): Pair<Float, Float> {
|
||||||
|
|||||||
@@ -19,22 +19,16 @@
|
|||||||
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.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -43,23 +37,11 @@ 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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
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.font.Font
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import dev.chrisbanes.haze.HazeEffectScope
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
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.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -67,13 +49,14 @@ 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.IndependentToggle
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
import me.kavishdevar.librepods.utils.ATTManager
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -88,78 +71,31 @@ private const val TAG = "HearingAidAdjustments"
|
|||||||
@Composable
|
@Composable
|
||||||
fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController) {
|
fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController) {
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
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
|
|
||||||
remember { RadareOffsetFinder(context) }
|
|
||||||
remember { mutableStateOf(RadareOffsetFinder.isSdpOffsetAvailable()) }
|
|
||||||
val service = ServiceManager.getService()
|
|
||||||
|
|
||||||
if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFF929491)
|
StyledScaffold(
|
||||||
if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
title = stringResource(R.string.adjustments),
|
||||||
if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
navigationButton = {
|
||||||
if (isDarkTheme) Color.White else Color.Black
|
StyledIconButton(
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
Scaffold(
|
icon = "",
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7),
|
darkMode = isDarkTheme
|
||||||
topBar = {
|
|
||||||
val darkMode = isSystemInDarkTheme()
|
|
||||||
val mDensity = remember { mutableFloatStateOf(1f) }
|
|
||||||
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.adjustments),
|
|
||||||
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) }
|
) { spacerHeight ->
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.verticalScroll(verticalScrollState),
|
.verticalScroll(verticalScrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
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) }
|
||||||
@@ -355,12 +291,9 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
|||||||
independent = true,
|
independent = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
IndependentToggle(
|
StyledToggle(
|
||||||
name = stringResource(R.string.swipe_to_control_amplification),
|
label = stringResource(R.string.swipe_to_control_amplification),
|
||||||
service = service,
|
|
||||||
sharedPreferences = sharedPreferences,
|
|
||||||
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE,
|
controlCommandIdentifier = AACPManager.Companion.ControlCommandIdentifiers.HPS_GAIN_SWIPE,
|
||||||
description = stringResource(R.string.swipe_amplification_description)
|
description = stringResource(R.string.swipe_amplification_description)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,27 +39,21 @@ 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.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.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.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.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.drawBehind
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -70,20 +64,20 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
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.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.kyant.backdrop.backdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.rememberBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.HazeEffectScope
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
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.ExperimentalHazeMaterialsApi
|
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.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
import me.kavishdevar.librepods.composables.ConfirmationDialog
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||||
|
import me.kavishdevar.librepods.composables.StyledToggle
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.AACPManager
|
import me.kavishdevar.librepods.utils.AACPManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
@@ -108,7 +102,8 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
|
|
||||||
val showDialog = remember { mutableStateOf(false) }
|
val showDialog = remember { mutableStateOf(false) }
|
||||||
val backdrop = rememberBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
|
val initialLoad = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
val hearingAidEnabled = remember {
|
val hearingAidEnabled = remember {
|
||||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
||||||
@@ -116,67 +111,28 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
|
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(
|
title = stringResource(R.string.hearing_aid),
|
||||||
0xFF000000
|
navigationButton = {
|
||||||
) else Color(
|
StyledIconButton(
|
||||||
0xFFF2F2F7
|
onClick = { navController.popBackStack() },
|
||||||
),
|
icon = "",
|
||||||
topBar = {
|
darkMode = isDarkTheme
|
||||||
val darkMode = isSystemInDarkTheme()
|
|
||||||
val mDensity = remember { mutableFloatStateOf(1f) }
|
|
||||||
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.hearing_aid),
|
|
||||||
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) },
|
actionButtons = emptyList(),
|
||||||
modifier = Modifier
|
snackbarHostState = snackbarHostState,
|
||||||
.backdrop(backdrop)
|
) { spacerHeight ->
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.layerBackdrop(backdrop)
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.verticalScroll(verticalScrollState),
|
.verticalScroll(verticalScrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
val hearingAidListener = remember {
|
val hearingAidListener = remember {
|
||||||
object : AACPManager.ControlCommandListener {
|
object : AACPManager.ControlCommandListener {
|
||||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||||
@@ -206,14 +162,15 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChange(value: Boolean) {
|
LaunchedEffect(hearingAidEnabled.value) {
|
||||||
if (value) {
|
if (hearingAidEnabled.value && !initialLoad.value) {
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
} else {
|
} else if (!hearingAidEnabled.value) {
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
|
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value, byteArrayOf(0x01, 0x02))
|
||||||
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
|
aacpManager?.sendControlCommand(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value, 0x02.toByte())
|
||||||
hearingAidEnabled.value = false
|
hearingAidEnabled.value = false
|
||||||
}
|
}
|
||||||
|
initialLoad.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAdjustPhoneChange(value: Boolean) {
|
fun onAdjustPhoneChange(value: Boolean) {
|
||||||
@@ -241,37 +198,15 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
) {
|
.clip(
|
||||||
val isDarkThemeLocal = isSystemInDarkTheme()
|
RoundedCornerShape(14.dp)
|
||||||
var backgroundColorHA by remember { mutableStateOf(if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)) }
|
|
||||||
val animatedBackgroundColorHA by animateColorAsState(targetValue = backgroundColorHA, animationSpec = tween(durationMillis = 500))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp)
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
backgroundColorHA = if (isDarkThemeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
|
||||||
tryAwaitRelease()
|
|
||||||
backgroundColorHA = if (isDarkThemeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
|
||||||
},
|
|
||||||
onTap = {
|
|
||||||
onChange(value = !hearingAidEnabled.value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.hearing_aid), modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
|
||||||
StyledSwitch(
|
|
||||||
checked = hearingAidEnabled.value,
|
|
||||||
onCheckedChange = {
|
|
||||||
onChange(value = it)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
) {
|
||||||
|
StyledToggle(
|
||||||
|
label = stringResource(R.string.hearing_aid),
|
||||||
|
checkedState = hearingAidEnabled,
|
||||||
|
independent = false
|
||||||
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
color = Color(0x40888888),
|
color = Color(0x40888888),
|
||||||
@@ -299,7 +234,6 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.hearing_aid_description),
|
text = stringResource(R.string.hearing_aid_description),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -310,7 +244,6 @@ fun HearingAidScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
AccessibilityToggle(
|
AccessibilityToggle(
|
||||||
|
|||||||
@@ -39,25 +39,18 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -78,14 +71,18 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
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 androidx.core.content.edit
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Onboarding(navController: NavController, activityContext: Context) {
|
fun Onboarding(navController: NavController, activityContext: Context) {
|
||||||
@@ -104,7 +101,6 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
var moduleEnabled by remember { mutableStateOf(false) }
|
var moduleEnabled by remember { mutableStateOf(false) }
|
||||||
var bluetoothToggled by remember { mutableStateOf(false) }
|
var bluetoothToggled by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
|
||||||
var showSkipDialog by remember { mutableStateOf(false) }
|
var showSkipDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun checkRootAccess() {
|
fun checkRootAccess() {
|
||||||
@@ -155,55 +151,27 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
isComplete = true
|
isComplete = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
StyledScaffold(
|
||||||
Scaffold(
|
title = "Setting Up",
|
||||||
topBar = {
|
actionButtons = listOf(
|
||||||
CenterAlignedTopAppBar(
|
{
|
||||||
title = {
|
StyledIconButton(
|
||||||
Text(
|
onClick = {
|
||||||
"Setting Up",
|
showSkipDialog = true
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
},
|
||||||
fontWeight = FontWeight.Medium
|
icon = "",
|
||||||
)
|
darkMode = isDarkTheme
|
||||||
},
|
)
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
}
|
||||||
containerColor = Color.Transparent
|
)
|
||||||
),
|
) { spacerHeight ->
|
||||||
actions = {
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = { showMenu = true }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = "More Options"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showMenu,
|
|
||||||
onDismissRequest = { showMenu = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("Skip Setup") },
|
|
||||||
onClick = {
|
|
||||||
showMenu = false
|
|
||||||
showSkipDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
containerColor = if (isDarkTheme) Color(0xFF000000) else Color(0xFFF2F2F7)
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -300,7 +268,8 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = if (hasStarted) getStatusTitle(progressState, isComplete, moduleEnabled, bluetoothToggled) else "Setup Required",
|
targetState = if (hasStarted) getStatusTitle(progressState,
|
||||||
|
moduleEnabled, bluetoothToggled) else "Setup Required",
|
||||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||||
) { text ->
|
) { text ->
|
||||||
Text(
|
Text(
|
||||||
@@ -319,7 +288,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
|
|||||||
|
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = if (hasStarted)
|
targetState = if (hasStarted)
|
||||||
getStatusDescription(progressState, isComplete, moduleEnabled, bluetoothToggled)
|
getStatusDescription(progressState, moduleEnabled, bluetoothToggled)
|
||||||
else
|
else
|
||||||
"AirPods functionality requires one-time setup for hooking into Bluetooth library",
|
"AirPods functionality requires one-time setup for hooking into Bluetooth library",
|
||||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||||
@@ -608,7 +577,6 @@ private fun StatusIcon(
|
|||||||
|
|
||||||
private fun getStatusTitle(
|
private fun getStatusTitle(
|
||||||
state: RadareOffsetFinder.ProgressState,
|
state: RadareOffsetFinder.ProgressState,
|
||||||
isComplete: Boolean,
|
|
||||||
moduleEnabled: Boolean,
|
moduleEnabled: Boolean,
|
||||||
bluetoothToggled: Boolean
|
bluetoothToggled: Boolean
|
||||||
): String {
|
): String {
|
||||||
@@ -635,7 +603,6 @@ private fun getStatusTitle(
|
|||||||
|
|
||||||
private fun getStatusDescription(
|
private fun getStatusDescription(
|
||||||
state: RadareOffsetFinder.ProgressState,
|
state: RadareOffsetFinder.ProgressState,
|
||||||
isComplete: Boolean,
|
|
||||||
moduleEnabled: Boolean,
|
moduleEnabled: Boolean,
|
||||||
bluetoothToggled: Boolean
|
bluetoothToggled: Boolean
|
||||||
): String {
|
): String {
|
||||||
@@ -660,6 +627,7 @@ private fun getStatusDescription(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingPreview() {
|
fun OnboardingPreview() {
|
||||||
|
|||||||
@@ -30,24 +30,19 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
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.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.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
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.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -68,14 +63,17 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.constants.StemAction
|
import me.kavishdevar.librepods.constants.StemAction
|
||||||
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.experimental.and
|
import kotlin.experimental.and
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
@Composable()
|
@Composable
|
||||||
fun RightDivider() {
|
fun RightDivider() {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
@@ -85,7 +83,7 @@ fun RightDivider() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable()
|
@Composable
|
||||||
fun RightDividerNoIcon() {
|
fun RightDividerNoIcon() {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = 1.5.dp,
|
thickness = 1.5.dp,
|
||||||
@@ -95,6 +93,7 @@ fun RightDividerNoIcon() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalHazeMaterialsApi
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPress(navController: NavController, name: String) {
|
fun LongPress(navController: NavController, name: String) {
|
||||||
@@ -114,60 +113,27 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
}
|
}
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val deviceName = sharedPreferences.getString("name", "AirPods Pro")
|
|
||||||
val prefKey = if (name.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
val prefKey = if (name.lowercase() == "left") "left_long_press_action" else "right_long_press_action"
|
||||||
val longPressActionPref = sharedPreferences.getString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
val longPressActionPref = sharedPreferences.getString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name)
|
||||||
Log.d("PressAndHoldSettingsScreen", "Long press action preference ($prefKey): $longPressActionPref")
|
Log.d("PressAndHoldSettingsScreen", "Long press action preference ($prefKey): $longPressActionPref")
|
||||||
var longPressAction by remember { mutableStateOf(StemAction.valueOf(longPressActionPref ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) }
|
var longPressAction by remember { mutableStateOf(StemAction.valueOf(longPressActionPref ?: StemAction.CYCLE_NOISE_CONTROL_MODES.name)) }
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
topBar = {
|
title = name,
|
||||||
CenterAlignedTopAppBar(
|
navigationButton = {
|
||||||
title = {
|
StyledIconButton(
|
||||||
Text(
|
onClick = { navController.popBackStack() },
|
||||||
name,
|
icon = "",
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
darkMode = isDarkTheme
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
deviceName?: "AirPods Pro",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
) { spacerHeight ->
|
||||||
else Color(0xFFF2F2F7),
|
|
||||||
) { paddingValues ->
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues = paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -221,33 +187,37 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION
|
||||||
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
}?.value?.takeIf { it.isNotEmpty() }?.get(0)
|
||||||
val offListeningMode = offListeningModeValue == 1.toByte()
|
val offListeningMode = offListeningModeValue == 1.toByte()
|
||||||
LongPressElement(
|
ListeningModeElement(
|
||||||
name = "Off",
|
name = "Off",
|
||||||
enabled = offListeningMode,
|
enabled = offListeningMode,
|
||||||
resourceId = R.drawable.noise_cancellation,
|
resourceId = R.drawable.noise_cancellation,
|
||||||
isFirst = true)
|
isFirst = true)
|
||||||
if (offListeningMode) RightDivider()
|
if (offListeningMode) RightDivider()
|
||||||
LongPressElement(
|
ListeningModeElement(
|
||||||
name = "Transparency",
|
name = "Transparency",
|
||||||
resourceId = R.drawable.transparency,
|
resourceId = R.drawable.transparency,
|
||||||
isFirst = !offListeningMode)
|
isFirst = !offListeningMode)
|
||||||
RightDivider()
|
RightDivider()
|
||||||
LongPressElement(
|
ListeningModeElement(
|
||||||
name = "Adaptive",
|
name = "Adaptive",
|
||||||
resourceId = R.drawable.adaptive)
|
resourceId = R.drawable.adaptive)
|
||||||
RightDivider()
|
RightDivider()
|
||||||
LongPressElement(
|
ListeningModeElement(
|
||||||
name = "Noise Cancellation",
|
name = "Noise Cancellation",
|
||||||
resourceId = R.drawable.noise_cancellation,
|
resourceId = R.drawable.noise_cancellation,
|
||||||
isLast = true)
|
isLast = true)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
"Press and hold the stem to cycle between the selected noise control modes.",
|
text = "Press and hold the stem to cycle between the selected noise control modes.",
|
||||||
fontSize = 16.sp,
|
style = TextStyle(
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
fontSize = 12.sp,
|
||||||
color = textColor.copy(alpha = 0.6f),
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f),
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp, top = 4.dp)
|
.padding(horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +228,7 @@ fun LongPress(navController: NavController, name: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isFirst: Boolean = false, isLast: Boolean = false) {
|
fun ListeningModeElement(name: String, enabled: Boolean = true, resourceId: Int, isFirst: Boolean = false, isLast: Boolean = false) {
|
||||||
val bit = when (name) {
|
val bit = when (name) {
|
||||||
"Off" -> 0x01
|
"Off" -> 0x01
|
||||||
"Transparency" -> 0x02
|
"Transparency" -> 0x02
|
||||||
@@ -280,7 +250,7 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF
|
|||||||
val isChecked = (byteValue.toInt() and bit) != 0
|
val isChecked = (byteValue.toInt() and bit) != 0
|
||||||
val checked = remember { mutableStateOf(isChecked) }
|
val checked = remember { mutableStateOf(isChecked) }
|
||||||
|
|
||||||
Log.d("PressAndHoldSettingsScreen", "LongPressElement: $name, checked: ${checked.value}, byteValue: ${byteValue.toInt()}, in bits: ${byteValue.toInt().toString(2)}")
|
Log.d("PressAndHoldSettingsScreen", "ListeningModeElement: $name, checked: ${checked.value}, byteValue: ${byteValue.toInt()}, in bits: ${byteValue.toInt().toString(2)}")
|
||||||
val darkMode = isSystemInDarkTheme()
|
val darkMode = isSystemInDarkTheme()
|
||||||
val textColor = if (darkMode) Color.White else Color.Black
|
val textColor = if (darkMode) Color.White else Color.Black
|
||||||
val desc = when (name) {
|
val desc = when (name) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
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.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
|
||||||
@@ -32,23 +33,16 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -58,21 +52,21 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
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.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
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 androidx.core.content.edit
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RenameScreen(navController: NavController) {
|
fun RenameScreen(navController: NavController) {
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
@@ -87,54 +81,21 @@ fun RenameScreen(navController: NavController) {
|
|||||||
name.value = name.value.copy(selection = TextRange(name.value.text.length))
|
name.value = name.value.copy(selection = TextRange(name.value.text.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
topBar = {
|
title = stringResource(R.string.name),
|
||||||
CenterAlignedTopAppBar(
|
navigationButton = {
|
||||||
title = {
|
StyledIconButton(
|
||||||
Text(
|
onClick = { navController.popBackStack() },
|
||||||
text = stringResource(R.string.name),
|
icon = "",
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
darkMode = isDarkTheme
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = name.value.text,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000)
|
) { spacerHeight ->
|
||||||
else Color(0xFFF2F2F7),
|
Column(
|
||||||
) { paddingValues ->
|
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues = paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ package me.kavishdevar.librepods.screens
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
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.background
|
||||||
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.Box
|
||||||
@@ -39,61 +36,38 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
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.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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.draw.shadow
|
||||||
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.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
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
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.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
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.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.composables.StyledSlider
|
import me.kavishdevar.librepods.composables.StyledSlider
|
||||||
import me.kavishdevar.librepods.services.ServiceManager
|
import me.kavishdevar.librepods.services.ServiceManager
|
||||||
import me.kavishdevar.librepods.utils.ATTHandles
|
import me.kavishdevar.librepods.utils.ATTHandles
|
||||||
import me.kavishdevar.librepods.utils.ATTManager
|
|
||||||
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
import me.kavishdevar.librepods.utils.RadareOffsetFinder
|
||||||
import me.kavishdevar.librepods.utils.TransparencySettings
|
import me.kavishdevar.librepods.utils.TransparencySettings
|
||||||
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
import me.kavishdevar.librepods.utils.parseTransparencySettingsResponse
|
||||||
@@ -111,8 +85,6 @@ fun TransparencySettingsScreen(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()
|
||||||
val hazeState = remember { HazeState() }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
val attManager = ServiceManager.getService()?.attManager ?: return
|
val attManager = ServiceManager.getService()?.attManager ?: return
|
||||||
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
val aacpManager = remember { ServiceManager.getService()?.aacpManager }
|
||||||
val isSdpOffsetAvailable =
|
val isSdpOffsetAvailable =
|
||||||
@@ -122,65 +94,24 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(
|
title = stringResource(R.string.customize_transparency_mode),
|
||||||
0xFF000000
|
navigationButton = {
|
||||||
) else Color(
|
StyledIconButton(
|
||||||
0xFFF2F2F7
|
onClick = { navController.popBackStack() },
|
||||||
),
|
icon = "",
|
||||||
topBar = {
|
darkMode = isDarkTheme
|
||||||
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) }
|
){ spacerHeight, hazeState ->
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.verticalScroll(verticalScrollState),
|
.verticalScroll(verticalScrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
val enabled = remember { mutableStateOf(false) }
|
val enabled = remember { mutableStateOf(false) }
|
||||||
@@ -523,4 +454,4 @@ fun TransparencySettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple's ecosystem
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
@@ -23,9 +23,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -49,29 +47,23 @@ 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.KeyboardArrowLeft
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
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.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -80,11 +72,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
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.drawBehind
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -97,17 +85,15 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.navigation.NavController
|
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.hazeSource
|
||||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.kavishdevar.librepods.R
|
import me.kavishdevar.librepods.R
|
||||||
|
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||||
|
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||||
import me.kavishdevar.librepods.utils.LogCollector
|
import me.kavishdevar.librepods.utils.LogCollector
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -134,8 +120,6 @@ fun CustomIconButton(
|
|||||||
fun TroubleshootingScreen(navController: NavController) {
|
fun TroubleshootingScreen(navController: NavController) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
val hazeState = remember { HazeState() }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val logCollector = remember { LogCollector(context) }
|
val logCollector = remember { LogCollector(context) }
|
||||||
@@ -161,27 +145,6 @@ fun TroubleshootingScreen(navController: NavController) {
|
|||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val sheetProgress by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
if (!showBottomSheet) 0f else sheetState.targetValue.ordinal.toFloat() / 2f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentScaleFactor by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
1.0f - (0.12f * sheetProgress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentScale by animateFloatAsState(
|
|
||||||
targetValue = contentScaleFactor,
|
|
||||||
animationSpec = spring(
|
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
||||||
stiffness = Spring.StiffnessMedium
|
|
||||||
),
|
|
||||||
label = "contentScale"
|
|
||||||
)
|
|
||||||
|
|
||||||
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
val textColor = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||||
val accentColor = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
val accentColor = if (isSystemInDarkTheme()) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||||
@@ -189,7 +152,6 @@ fun TroubleshootingScreen(navController: NavController) {
|
|||||||
|
|
||||||
var instructionText by remember { mutableStateOf("") }
|
var instructionText by remember { mutableStateOf("") }
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
var mDensity by remember { mutableFloatStateOf(0f) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -249,75 +211,23 @@ fun TroubleshootingScreen(navController: NavController) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
StyledScaffold(
|
||||||
modifier = Modifier
|
title = stringResource(R.string.troubleshooting),
|
||||||
.fillMaxSize()
|
navigationButton = {
|
||||||
.graphicsLayer {
|
StyledIconButton(
|
||||||
scaleX = contentScale
|
onClick = { navController.popBackStack() },
|
||||||
scaleY = contentScale
|
icon = "",
|
||||||
transformOrigin = androidx.compose.ui.graphics.TransformOrigin(0.5f, 0.3f)
|
darkMode = isDarkTheme
|
||||||
},
|
|
||||||
topBar = {
|
|
||||||
CenterAlignedTopAppBar(
|
|
||||||
modifier = Modifier.hazeEffect(
|
|
||||||
state = hazeState,
|
|
||||||
style = CupertinoMaterials.thick(),
|
|
||||||
block = fun HazeEffectScope.() {
|
|
||||||
alpha = if (scrollState.value > 60.dp.value * mDensity) 1f else 0f
|
|
||||||
})
|
|
||||||
.drawBehind {
|
|
||||||
mDensity = density
|
|
||||||
val strokeWidth = 0.7.dp.value * density
|
|
||||||
val y = size.height - strokeWidth / 2
|
|
||||||
if (scrollState.value > 60.dp.value * density) {
|
|
||||||
drawLine(
|
|
||||||
if (isDarkTheme) Color.DarkGray else Color.LightGray,
|
|
||||||
Offset(0f, y),
|
|
||||||
Offset(size.width, y),
|
|
||||||
strokeWidth
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.troubleshooting),
|
|
||||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "Back",
|
|
||||||
tint = accentColor,
|
|
||||||
modifier = Modifier.scale(1.5f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
|
||||||
containerColor = Color.Transparent
|
|
||||||
),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
containerColor = if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF2F2F7),
|
){ spacerHeight, hazeState ->
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.hazeSource(state = hazeState)
|
.hazeSource(state = hazeState)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.saved_logs).uppercase(),
|
text = stringResource(R.string.saved_logs).uppercase(),
|
||||||
@@ -706,7 +616,9 @@ fun TroubleshootingScreen(navController: NavController) {
|
|||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedLogFile?.let { file ->
|
selectedLogFile?.let { file ->
|
||||||
saveLauncher.launch("airpods_log_${System.currentTimeMillis()}.txt")
|
saveLauncher.launch(
|
||||||
|
file.absolutePath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
@@ -977,7 +889,7 @@ fun TroubleshootingScreen(navController: NavController) {
|
|||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedLogFile?.let { file ->
|
selectedLogFile?.let { file ->
|
||||||
saveLauncher.launch("airpods_log_${System.currentTimeMillis()}.txt")
|
saveLauncher.launch(file.absolutePath)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple's ecosystem
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
@@ -202,10 +202,10 @@ class AACPManager {
|
|||||||
|
|
||||||
var eqData = FloatArray(8) { 0.0f }
|
var eqData = FloatArray(8) { 0.0f }
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var eqOnPhone: Boolean = false
|
var eqOnPhone: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var eqOnMedia: Boolean = false
|
var eqOnMedia: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -528,12 +528,23 @@ class AACPManager {
|
|||||||
val packetString = packet.decodeToString()
|
val packetString = packet.decodeToString()
|
||||||
val sender = packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
|
val sender = packet.sliceArray(6..11).reversedArray().joinToString(":") { "%02X".format(it) }
|
||||||
|
|
||||||
if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
|
// if (connectedDevices.find { it.mac == sender }?.type == null && packetString.contains("btName")) {
|
||||||
val nameStartIndex = packetString.indexOf("btName") + 7
|
// val nameStartIndex = packetString.indexOf("btName") + 8
|
||||||
val nameEndIndex = if (packetString.contains("other")) (packetString.indexOf("otherDevice") - 2) else (packetString.indexOf("nearbyAudio") - 2)
|
// val nameEndIndex = if (packetString.contains("other")) (packetString.indexOf("otherDevice") - 1) else (packetString.indexOf("nearbyAudio") - 1)
|
||||||
val name = packet.sliceArray(nameStartIndex..nameEndIndex).decodeToString()
|
// val name = packet.sliceArray(nameStartIndex..nameEndIndex).decodeToString()
|
||||||
connectedDevices.find { it.mac == sender }?.type = name
|
// connectedDevices.find { it.mac == sender }?.type = name
|
||||||
Log.d(TAG, "Device $sender is named $name")
|
// Log.d(TAG, "Device $sender is named $name")
|
||||||
|
// } // doesn't work, it's different for Mac and iPad. just hardcoding for now
|
||||||
|
if ("iPad" in packetString) {
|
||||||
|
connectedDevices.find { it.mac == sender }?.type = "iPad"
|
||||||
|
} else if ("Mac" in packetString) {
|
||||||
|
connectedDevices.find { it.mac == sender }?.type = "Mac"
|
||||||
|
} else if ("iPhone" in packetString) { // not sure if this is it - don't have an iphone
|
||||||
|
connectedDevices.find { it.mac == sender }?.type = "iPhone"
|
||||||
|
} else if ("Linux" in packetString) {
|
||||||
|
connectedDevices.find { it.mac == sender }?.type = "Linux"
|
||||||
|
} else if ("Android" in packetString) {
|
||||||
|
connectedDevices.find { it.mac == sender }?.type = "Android"
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}")
|
Log.d(TAG, "Smart Routing Response from $sender: $packetString, type: ${connectedDevices.find { it.mac == sender }?.type}")
|
||||||
if (packetString.contains("SetOwnershipToFalse")) {
|
if (packetString.contains("SetOwnershipToFalse")) {
|
||||||
@@ -568,7 +579,7 @@ class AACPManager {
|
|||||||
val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
val eq2 = ByteBuffer.wrap(packet, 44, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
val eq3 = ByteBuffer.wrap(packet, 76, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
val eq4 = ByteBuffer.wrap(packet, 108, 32).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
|
||||||
|
|
||||||
// for now, taking just the first EQ
|
// for now, taking just the first EQ
|
||||||
eqData = FloatArray(8) { i -> eq1.get(i) }
|
eqData = FloatArray(8) { i -> eq1.get(i) }
|
||||||
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
|
Log.d(TAG, "EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia")
|
||||||
@@ -580,17 +591,6 @@ class AACPManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createEqualizerDataPacket(eqData: FloatArray, eqOnPhone: Boolean, eqOnMedia: Boolean): ByteArray {
|
|
||||||
val opcode = byteArrayOf(Opcodes.EQ_DATA, 0x00)
|
|
||||||
val identifier = byteArrayOf(0x84.toByte(), 0x00)
|
|
||||||
val something = byteArrayOf(0x02, 0x02)
|
|
||||||
val phoneFlag = if (eqOnPhone) 0x01.toByte() else 0x00.toByte()
|
|
||||||
val mediaFlag = if (eqOnMedia) 0x01.toByte() else 0x00.toByte()
|
|
||||||
val buffer = ByteBuffer.allocate(32).order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
eqData.forEach { buffer.putFloat(it) }
|
|
||||||
return opcode + identifier + something + byteArrayOf(phoneFlag, mediaFlag) + buffer.array() + buffer.array() + buffer.array() + buffer.array()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendNotificationRequest(): Boolean {
|
fun sendNotificationRequest(): Boolean {
|
||||||
return sendDataPacket(createRequestNotificationPacket())
|
return sendDataPacket(createRequestNotificationPacket())
|
||||||
}
|
}
|
||||||
@@ -853,11 +853,11 @@ class AACPManager {
|
|||||||
Log.w(TAG, "Cannot send Media Information packet: No connected device found")
|
Log.w(TAG, "Cannot send Media Information packet: No connected device found")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Sending Media Information packet to ${targetMac ?: "unknown device"}")
|
Log.d(TAG, "Sending Media Information packet to $targetMac")
|
||||||
return sendDataPacket(
|
return sendDataPacket(
|
||||||
createMediaInformationPacket(
|
createMediaInformationPacket(
|
||||||
selfMacAddress,
|
selfMacAddress,
|
||||||
targetMac ?: return false,
|
targetMac,
|
||||||
streamingState
|
streamingState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -186,9 +186,7 @@ class ATTManager(private val device: BluetoothDevice) {
|
|||||||
private fun readResponse(timeoutMs: Long = 2000): ByteArray {
|
private fun readResponse(timeoutMs: Long = 2000): ByteArray {
|
||||||
try {
|
try {
|
||||||
val resp = responses.poll(timeoutMs, TimeUnit.MILLISECONDS)
|
val resp = responses.poll(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
if (resp == null) {
|
?: throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
||||||
throw IllegalStateException("No response read from ATT socket within $timeoutMs ms")
|
|
||||||
}
|
|
||||||
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
Log.d(TAG, "readResponse: ${resp.joinToString(" ") { String.format("%02X", it) }}")
|
||||||
return resp.copyOfRange(1, resp.size)
|
return resp.copyOfRange(1, resp.size)
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* 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
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.PointerId
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||||
|
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
|
||||||
|
import androidx.compose.ui.input.pointer.positionChange
|
||||||
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
|
|
||||||
|
suspend fun PointerInputScope.inspectDragGestures(
|
||||||
|
onDragStart: (down: PointerInputChange) -> Unit = {},
|
||||||
|
onDragEnd: (change: PointerInputChange) -> Unit = {},
|
||||||
|
onDragCancel: () -> Unit = {},
|
||||||
|
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
|
||||||
|
) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val initialDown = awaitFirstDown(false, PointerEventPass.Initial)
|
||||||
|
|
||||||
|
val down = awaitFirstDown(false)
|
||||||
|
|
||||||
|
onDragStart(down)
|
||||||
|
onDrag(initialDown, Offset.Zero)
|
||||||
|
val upEvent =
|
||||||
|
drag(
|
||||||
|
pointerId = initialDown.id,
|
||||||
|
onDrag = { onDrag(it, it.positionChange()) }
|
||||||
|
)
|
||||||
|
if (upEvent == null) {
|
||||||
|
onDragCancel()
|
||||||
|
} else {
|
||||||
|
onDragEnd(upEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun AwaitPointerEventScope.drag(
|
||||||
|
pointerId: PointerId,
|
||||||
|
onDrag: (PointerInputChange) -> Unit
|
||||||
|
): PointerInputChange? {
|
||||||
|
val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
|
||||||
|
if (isPointerUp) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var pointer = pointerId
|
||||||
|
while (true) {
|
||||||
|
val change = awaitDragOrUp(pointer) ?: return null
|
||||||
|
if (change.isConsumed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (change.changedToUpIgnoreConsumed()) {
|
||||||
|
return change
|
||||||
|
}
|
||||||
|
onDrag(change)
|
||||||
|
pointer = change.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
|
||||||
|
pointerId: PointerId
|
||||||
|
): PointerInputChange? {
|
||||||
|
var pointer = pointerId
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
|
||||||
|
if (dragEvent.changedToUpIgnoreConsumed()) {
|
||||||
|
val otherDown = event.changes.fastFirstOrNull { it.pressed }
|
||||||
|
if (otherDown == null) {
|
||||||
|
return dragEvent
|
||||||
|
} else {
|
||||||
|
pointer = otherDown.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val hasDragged = dragEvent.previousPosition != dragEvent.position
|
||||||
|
if (hasDragged) {
|
||||||
|
return dragEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
@file:OptIn(ExperimentalEncodingApi::class)
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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:Suppress("PrivatePropertyName")
|
@file:Suppress("PrivatePropertyName")
|
||||||
|
|
||||||
package me.kavishdevar.librepods.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.VideoView
|
import android.widget.VideoView
|
||||||
import androidx.core.content.ContextCompat.getString
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.dynamicanimation.animation.DynamicAnimation
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||||
import androidx.dynamicanimation.animation.SpringAnimation
|
import androidx.dynamicanimation.animation.SpringAnimation
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* LibrePods - AirPods liberated from Apple's ecosystem
|
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 LibrePods contributors
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -462,7 +462,8 @@ class RadareOffsetFinder(context: Context) {
|
|||||||
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
// findAndSaveL2cuProcessCfgReqOffset(libraryPath, envSetup)
|
||||||
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
// findAndSaveL2cCsmConfigOffset(libraryPath, envSetup)
|
||||||
// findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
// findAndSaveL2cuSendPeerInfoReqOffset(libraryPath, envSetup)
|
||||||
findAndSaveSdpOffset(libraryPath, envSetup)
|
|
||||||
|
// findAndSaveSdpOffset(libraryPath, envSetup) Should not be run by default, only when user asks for it.
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to find function offset", e)
|
Log.e(TAG, "Failed to find function offset", e)
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils
|
package me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -10,8 +27,6 @@ import java.io.IOException
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
private const val TAG = "TransparencyUtils"
|
|
||||||
|
|
||||||
data class TransparencySettings(
|
data class TransparencySettings(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val leftEQ: FloatArray,
|
val leftEQ: FloatArray,
|
||||||
@@ -67,9 +82,8 @@ data class TransparencySettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
|
fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings {
|
||||||
val settingsData = data
|
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
val buffer = ByteBuffer.wrap(settingsData).order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
val enabled = buffer.float
|
val enabled = buffer.float
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 52 KiB |
@@ -23,8 +23,6 @@
|
|||||||
<string name="conversational_awareness_description">当你开始与他人交谈时,会降低媒体音量并减少背景噪音。</string>
|
<string name="conversational_awareness_description">当你开始与他人交谈时,会降低媒体音量并减少背景噪音。</string>
|
||||||
<string name="personalized_volume">个性化音量</string>
|
<string name="personalized_volume">个性化音量</string>
|
||||||
<string name="personalized_volume_description">根据环境自动调整媒体音量。</string>
|
<string name="personalized_volume_description">根据环境自动调整媒体音量。</string>
|
||||||
<string name="less_noise">减少噪音</string>
|
|
||||||
<string name="more_noise">增加噪音</string>
|
|
||||||
<string name="noise_cancellation_single_airpod">单只 AirPod 主动降噪</string>
|
<string name="noise_cancellation_single_airpod">单只 AirPod 主动降噪</string>
|
||||||
<string name="noise_cancellation_single_airpod_description">仅佩戴一只 AirPod 时也能开启主动降噪。</string>
|
<string name="noise_cancellation_single_airpod_description">仅佩戴一只 AirPod 时也能开启主动降噪。</string>
|
||||||
<string name="volume_control">音量控制</string>
|
<string name="volume_control">音量控制</string>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<string name="tone_volume">Tone Volume</string>
|
<string name="tone_volume">Tone Volume</string>
|
||||||
<string name="audio">Audio</string>
|
<string name="audio">Audio</string>
|
||||||
<string name="adaptive_audio">Adaptive Audio</string>
|
<string name="adaptive_audio">Adaptive Audio</string>
|
||||||
|
<string name="customize_adaptive_audio">Customize Adaptive Audio</string>
|
||||||
<string name="adaptive_audio_description">Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.</string>
|
<string name="adaptive_audio_description">Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.</string>
|
||||||
<string name="buds">Buds</string>
|
<string name="buds">Buds</string>
|
||||||
<string name="case_alt">Case</string>
|
<string name="case_alt">Case</string>
|
||||||
@@ -26,14 +27,12 @@
|
|||||||
<string name="conversational_awareness_description">Lowers media volume and reduces background noise when you start speaking to other people.</string>
|
<string name="conversational_awareness_description">Lowers media volume and reduces background noise when you start speaking to other people.</string>
|
||||||
<string name="personalized_volume">Personalized Volume</string>
|
<string name="personalized_volume">Personalized Volume</string>
|
||||||
<string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string>
|
<string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string>
|
||||||
<string name="less_noise">Less Noise</string>
|
|
||||||
<string name="more_noise">More Noise</string>
|
|
||||||
<string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string>
|
<string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string>
|
||||||
<string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.</string>
|
<string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.</string>
|
||||||
<string name="volume_control">Volume Control</string>
|
<string name="volume_control">Volume Control</string>
|
||||||
<string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string>
|
<string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string>
|
||||||
<string name="airpods_not_connected">AirPods not connected</string>
|
<string name="airpods_not_connected">AirPods not connected</string>
|
||||||
<string name="airpods_not_connected_description">Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)</string>
|
<string name="airpods_not_connected_description">Please connect your AirPods to access settings.</string>
|
||||||
<string name="back">Back</string>
|
<string name="back">Back</string>
|
||||||
<string name="app_settings">Customizations</string>
|
<string name="app_settings">Customizations</string>
|
||||||
<string name="relative_conversational_awareness_volume">Relative volume</string>
|
<string name="relative_conversational_awareness_volume">Relative volume</string>
|
||||||
@@ -135,4 +134,40 @@
|
|||||||
<string name="media_assist_description">AirPods Pro can use the results of a hearing test to make adjustments that improve the clarity of music, video, and calls.</string>
|
<string name="media_assist_description">AirPods Pro can use the results of a hearing test to make adjustments that improve the clarity of music, video, and calls.</string>
|
||||||
<string name="adjust_media">Adjust Music and Video</string>
|
<string name="adjust_media">Adjust Music and Video</string>
|
||||||
<string name="adjust_calls">Adjust Calls</string>
|
<string name="adjust_calls">Adjust Calls</string>
|
||||||
|
<string name="widget">Widget</string>
|
||||||
|
<string name="show_phone_battery_in_widget">Show phone battery in widget</string>
|
||||||
|
<string name="show_phone_battery_in_widget_description">Display your phone\'s battery level in the widget alongside AirPods battery</string>
|
||||||
|
<string name="connection_mode">Connection Mode</string>
|
||||||
|
<string name="ble_only_mode">BLE Only Mode</string>
|
||||||
|
<string name="ble_only_mode_description">Only use Bluetooth Low Energy for battery data and ear detection. Disables advanced features requiring L2CAP connection.</string>
|
||||||
|
<string name="conversational_awareness_volume">Conversational Awareness Volume</string>
|
||||||
|
<string name="quick_settings_tile">Quick Settings Tile</string>
|
||||||
|
<string name="open_dialog_for_controlling">Open dialog for controlling</string>
|
||||||
|
<string name="open_dialog_for_controlling_description">If disabled, clicking on the QS will cycle through modes. If enabled, it will show a dialog for controlling noise control mode and conversational awareness</string>
|
||||||
|
<string name="disconnect_when_not_wearing">Disconnect AirPods when not wearing</string>
|
||||||
|
<string name="disconnect_when_not_wearing_description">You will still be able to control them with the app - this just disconnects the audio.</string>
|
||||||
|
<string name="advanced_options">Advanced Options</string>
|
||||||
|
<string name="set_identity_resolving_key">Set Identity Resolving Key (IRK)</string>
|
||||||
|
<string name="set_identity_resolving_key_description">Manually set the IRK value used for resolving BLE random addresses</string>
|
||||||
|
<string name="set_encryption_key">Set Encryption Key</string>
|
||||||
|
<string name="set_encryption_key_description">Manually set the ENC_KEY value used for decrypting BLE advertisements</string>
|
||||||
|
<string name="use_alternate_head_tracking_packets">Use alternate head tracking packets</string>
|
||||||
|
<string name="use_alternate_head_tracking_packets_description">Enable this if head tracking doesn\'t work for you. This sends different data to AirPods for requesting/stopping head tracking data.</string>
|
||||||
|
<string name="act_as_an_apple_device">Act as an Apple device</string>
|
||||||
|
<string name="act_as_an_apple_device_description">Enables multi-device connectivity and Accessibility features like customizing transparency mode (amplification, tone, ambient noise reduction, conversation boost, and EQ)</string>
|
||||||
|
<string name="act_as_an_apple_device_warning">Might be unstable!! A maximum of two devices can be connected to your AirPods. If you are using with an Apple device like an iPad or Mac, then please connect that device first and then your Android.</string>
|
||||||
|
<string name="reset_hook_offset">Reset Hook Offset</string>
|
||||||
|
<string name="reset_hook_offset_description">This will clear the current hook offset and require you to go through the setup process again. Are you sure you want to continue?</string>
|
||||||
|
<string name="reset">Reset</string>
|
||||||
|
<string name="hook_offset_reset_success">Hook offset has been reset. Redirecting to setup...</string>
|
||||||
|
<string name="hook_offset_reset_failure">Failed to reset hook offset</string>
|
||||||
|
<string name="irk_set_success">IRK has been set successfully</string>
|
||||||
|
<string name="encryption_key_set_success">Encryption key has been set successfully</string>
|
||||||
|
<string name="irk_hex_value">IRK Hex Value</string>
|
||||||
|
<string name="enc_key_hex_value">ENC_KEY Hex Value</string>
|
||||||
|
<string name="enter_irk_hex">Enter 16-byte IRK as hex string (32 characters):</string>
|
||||||
|
<string name="enter_enc_key_hex">Enter 16-byte ENC_KEY as hex string (32 characters):</string>
|
||||||
|
<string name="must_be_32_hex_chars">Must be exactly 32 hex characters</string>
|
||||||
|
<string name="error_converting_hex">Error converting hex:</string>
|
||||||
|
<string name="found_offset_restart_bluetooth">Found offset please restart the Bluetooth process</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user