mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
android: small ui tweaks
This commit is contained in:
1
android/.gitignore
vendored
1
android/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
crowdin.yml
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
|
||||
@@ -20,22 +20,12 @@
|
||||
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -43,19 +33,19 @@ import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AdaptiveStrengthSlider() {
|
||||
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||||
@@ -100,80 +90,20 @@ fun AdaptiveStrengthSlider() {
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Slider(
|
||||
value = sliderValue.floatValue,
|
||||
StyledSlider(
|
||||
mutableFloatState = sliderValue,
|
||||
onValueChange = {
|
||||
sliderValue.floatValue = snapIfClose(it, listOf(0f, 50f, 100f))
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
onValueChangeFinished = {
|
||||
sliderValue.floatValue = snapIfClose(sliderValue.floatValue.roundToInt().toFloat(), listOf(0f, 50f, 100f))
|
||||
service.aacpManager.sendControlCommand(
|
||||
identifier = AACPManager.Companion.ControlCommandIdentifiers.AUTO_ANC_STRENGTH.value,
|
||||
value = (100 - sliderValue.floatValue).toInt()
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(36.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
inactiveTrackColor = trackColor
|
||||
),
|
||||
thumb = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.shadow(4.dp, CircleShape)
|
||||
.background(thumbColor, CircleShape)
|
||||
)
|
||||
},
|
||||
track = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
)
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(trackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
snapPoints = listOf(0f, 50f, 100f),
|
||||
startLabel = stringResource(R.string.less_noise),
|
||||
endLabel = stringResource(R.string.more_noise),
|
||||
independent = false
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Less Noise",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = labelTextColor
|
||||
),
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = "More Noise",
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = labelTextColor
|
||||
),
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ fun CallControlSettings(hazeState: HazeState) {
|
||||
)
|
||||
}
|
||||
|
||||
DragSelectableDropdown(
|
||||
StyledDropdown(
|
||||
expanded = showSinglePressDropdown,
|
||||
onDismissRequest = {
|
||||
showSinglePressDropdown = false
|
||||
@@ -415,7 +415,7 @@ fun CallControlSettings(hazeState: HazeState) {
|
||||
)
|
||||
}
|
||||
|
||||
DragSelectableDropdown(
|
||||
StyledDropdown(
|
||||
expanded = showDoublePressDropdown,
|
||||
onDismissRequest = {
|
||||
showDoublePressDropdown = false
|
||||
|
||||
@@ -269,7 +269,7 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
||||
val microphoneAlwaysRightText = stringResource(R.string.microphone_always_right)
|
||||
val microphoneAlwaysLeftText = stringResource(R.string.microphone_always_left)
|
||||
|
||||
DragSelectableDropdown(
|
||||
StyledDropdown(
|
||||
expanded = showDropdown,
|
||||
onDismissRequest = {
|
||||
showDropdown = false
|
||||
@@ -312,173 +312,3 @@ fun MicrophoneSettings(hazeState: HazeState) {
|
||||
fun MicrophoneSettingsPreview() {
|
||||
MicrophoneSettings(HazeState())
|
||||
}
|
||||
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@Composable
|
||||
fun DragSelectableDropdown(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
options: List<String>,
|
||||
selectedOption: String,
|
||||
touchOffset: Offset?,
|
||||
boxPosition: Offset,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
externalHoveredIndex: Int? = null,
|
||||
externalDragActive: Boolean = false,
|
||||
hazeState: HazeState,
|
||||
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||
) {
|
||||
if (expanded) {
|
||||
val relativeOffset = touchOffset?.let { it - boxPosition } ?: Offset.Zero
|
||||
Popup(
|
||||
offset = IntOffset(relativeOffset.x.toInt(), relativeOffset.y.toInt()),
|
||||
onDismissRequest = onDismissRequest
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
.width(300.dp)
|
||||
.background(Color.Transparent)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
var hoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||
val itemHeight = 48.dp
|
||||
|
||||
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
|
||||
var lastDragPosition by remember { mutableStateOf<Offset?>(null) }
|
||||
|
||||
LaunchedEffect(externalHoveredIndex, externalDragActive) {
|
||||
if (externalDragActive) {
|
||||
hoveredIndex = externalHoveredIndex
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
popupSize = coordinates.size
|
||||
}
|
||||
.pointerInput(popupSize) {
|
||||
detectDragGestures(
|
||||
onDragStart = { offset ->
|
||||
hoveredIndex = (offset.y / itemHeight.toPx()).toInt()
|
||||
lastDragPosition = offset
|
||||
},
|
||||
onDrag = { change, _ ->
|
||||
val y = change.position.y
|
||||
hoveredIndex = (y / itemHeight.toPx()).toInt()
|
||||
lastDragPosition = change.position
|
||||
},
|
||||
onDragEnd = {
|
||||
val pos = lastDragPosition
|
||||
val withinBounds = pos != null &&
|
||||
pos.x >= 0f && pos.y >= 0f &&
|
||||
pos.x <= popupSize.width.toFloat() && pos.y <= popupSize.height.toFloat()
|
||||
|
||||
if (withinBounds) {
|
||||
hoveredIndex?.let { idx ->
|
||||
if (idx in options.indices) {
|
||||
onOptionSelected(options[idx])
|
||||
}
|
||||
}
|
||||
onDismissRequest()
|
||||
} else {
|
||||
hoveredIndex = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
options.forEachIndexed { index, text ->
|
||||
val isHovered =
|
||||
if (externalDragActive && externalHoveredIndex != null) {
|
||||
index == externalHoveredIndex
|
||||
} else {
|
||||
index == hoveredIndex
|
||||
}
|
||||
val isSystemInDarkTheme = isSystemInDarkTheme()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(itemHeight)
|
||||
.background(
|
||||
Color.Transparent
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
onOptionSelected(text)
|
||||
onDismissRequest()
|
||||
}
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.regular(),
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha = 1f
|
||||
backgroundColor = if (isSystemInDarkTheme) {
|
||||
Color(0xB02C2C2E)
|
||||
} else {
|
||||
Color(0xB0FFFFFF)
|
||||
}
|
||||
tints = if (isHovered) listOf(
|
||||
HazeTint(
|
||||
color = if (isSystemInDarkTheme) Color(0x338A8A8A) else Color(0x40D9D9D9)
|
||||
)
|
||||
) else listOf()
|
||||
})
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.75f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Checkbox(
|
||||
checked = text == selectedOption,
|
||||
onCheckedChange = { onOptionSelected(text) },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent,
|
||||
disabledBorderColor = Color.Transparent,
|
||||
disabledCheckedBoxColor = Color.Transparent,
|
||||
disabledUncheckedBoxColor = Color.Transparent,
|
||||
disabledUncheckedBorderColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (index != options.lastIndex) {
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
*
|
||||
* Copyright (C) 2025 LibrePods contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import dev.chrisbanes.haze.HazeEffectScope
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.HazeTint
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import me.kavishdevar.librepods.R
|
||||
|
||||
@ExperimentalHazeMaterialsApi
|
||||
@Composable
|
||||
fun StyledDropdown(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
options: List<String>,
|
||||
selectedOption: String,
|
||||
touchOffset: Offset?,
|
||||
boxPosition: Offset,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
externalHoveredIndex: Int? = null,
|
||||
externalDragActive: Boolean = false,
|
||||
hazeState: HazeState,
|
||||
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||
) {
|
||||
if (expanded) {
|
||||
val relativeOffset = touchOffset?.let { it - boxPosition } ?: Offset.Zero
|
||||
Popup(
|
||||
offset = IntOffset(relativeOffset.x.toInt(), relativeOffset.y.toInt()),
|
||||
onDismissRequest = onDismissRequest
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
.width(300.dp)
|
||||
.background(Color.Transparent)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
) {
|
||||
var hoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||
val itemHeight = 48.dp
|
||||
|
||||
var popupSize by remember { mutableStateOf(IntSize(0, 0)) }
|
||||
var lastDragPosition by remember { mutableStateOf<Offset?>(null) }
|
||||
|
||||
LaunchedEffect(externalHoveredIndex, externalDragActive) {
|
||||
if (externalDragActive) {
|
||||
hoveredIndex = externalHoveredIndex
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
popupSize = coordinates.size
|
||||
}
|
||||
.pointerInput(popupSize) {
|
||||
detectDragGestures(
|
||||
onDragStart = { offset ->
|
||||
hoveredIndex = (offset.y / itemHeight.toPx()).toInt()
|
||||
lastDragPosition = offset
|
||||
},
|
||||
onDrag = { change, _ ->
|
||||
val y = change.position.y
|
||||
hoveredIndex = (y / itemHeight.toPx()).toInt()
|
||||
lastDragPosition = change.position
|
||||
},
|
||||
onDragEnd = {
|
||||
val pos = lastDragPosition
|
||||
val withinBounds = pos != null &&
|
||||
pos.x >= 0f && pos.y >= 0f &&
|
||||
pos.x <= popupSize.width.toFloat() && pos.y <= popupSize.height.toFloat()
|
||||
|
||||
if (withinBounds) {
|
||||
hoveredIndex?.let { idx ->
|
||||
if (idx in options.indices) {
|
||||
onOptionSelected(options[idx])
|
||||
}
|
||||
}
|
||||
onDismissRequest()
|
||||
} else {
|
||||
hoveredIndex = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
options.forEachIndexed { index, text ->
|
||||
val isHovered =
|
||||
if (externalDragActive && externalHoveredIndex != null) {
|
||||
index == externalHoveredIndex
|
||||
} else {
|
||||
index == hoveredIndex
|
||||
}
|
||||
val isSystemInDarkTheme = isSystemInDarkTheme()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(itemHeight)
|
||||
.background(
|
||||
Color.Transparent
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
onOptionSelected(text)
|
||||
onDismissRequest()
|
||||
}
|
||||
.hazeEffect(
|
||||
state = hazeState,
|
||||
style = CupertinoMaterials.regular(),
|
||||
block = fun HazeEffectScope.() {
|
||||
alpha = 1f
|
||||
backgroundColor = if (isSystemInDarkTheme) {
|
||||
Color(0xB02C2C2E)
|
||||
} else {
|
||||
Color(0xB0FFFFFF)
|
||||
}
|
||||
tints = if (isHovered) listOf(
|
||||
HazeTint(
|
||||
color = if (isSystemInDarkTheme) Color(0x338A8A8A) else Color(0x40D9D9D9)
|
||||
)
|
||||
) else listOf()
|
||||
})
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black.copy(alpha = 0.75f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
)
|
||||
)
|
||||
Checkbox(
|
||||
checked = text == selectedOption,
|
||||
onCheckedChange = { onOptionSelected(text) },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent,
|
||||
disabledBorderColor = Color.Transparent,
|
||||
disabledCheckedBoxColor = Color.Transparent,
|
||||
disabledUncheckedBoxColor = Color.Transparent,
|
||||
disabledUncheckedBorderColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (index != options.lastIndex) {
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
@@ -87,7 +88,7 @@ import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun StyledSlider(
|
||||
label: String? = null,
|
||||
label: String? = null, // New optional parameter for the label
|
||||
mutableFloatState: MutableFloatState,
|
||||
onValueChange: (Float) -> Unit,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
@@ -146,18 +147,6 @@ fun StyledSlider(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = labelTextColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (startLabel != null || endLabel != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -358,17 +347,38 @@ fun StyledSlider(
|
||||
}
|
||||
|
||||
if (independent) {
|
||||
Box(
|
||||
|
||||
Column (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
.heightIn(min = 55.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
content()
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = labelTextColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 0.dp)
|
||||
.heightIn(min = 55.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (label != null) Log.w("StyledSlider", "Label is ignored when independent is false")
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -41,6 +42,8 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
@@ -48,6 +51,7 @@ import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
@@ -62,6 +66,7 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -73,6 +78,9 @@ import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInParent
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
@@ -94,10 +102,11 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.StyledSlider
|
||||
import me.kavishdevar.librepods.composables.LoudSoundReductionSwitch
|
||||
import me.kavishdevar.librepods.composables.NavigationButton
|
||||
import me.kavishdevar.librepods.composables.SinglePodANCSwitch
|
||||
import me.kavishdevar.librepods.composables.StyledSlider
|
||||
import me.kavishdevar.librepods.composables.StyledDropdown
|
||||
import me.kavishdevar.librepods.composables.StyledSwitch
|
||||
import me.kavishdevar.librepods.composables.VolumeControlSwitch
|
||||
import me.kavishdevar.librepods.services.ServiceManager
|
||||
@@ -132,6 +141,36 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
||||
val activeTrackColor = if (isDarkTheme) Color(0xFF007AFF) else Color(0xFF3C6DF5)
|
||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||
|
||||
val hearingAidEnabled = remember { mutableStateOf(
|
||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }?.value?.getOrNull(1) == 0x01.toByte() &&
|
||||
aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }?.value?.getOrNull(0) == 0x01.toByte()
|
||||
) }
|
||||
|
||||
val hearingAidListener = remember {
|
||||
object : AACPManager.ControlCommandListener {
|
||||
override fun onControlCommandReceived(controlCommand: AACPManager.ControlCommand) {
|
||||
if (controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID.value ||
|
||||
controlCommand.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG.value) {
|
||||
val aidStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID }
|
||||
val assistStatus = aacpManager?.controlCommandStatusList?.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG }
|
||||
hearingAidEnabled.value = (aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.registerControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_AID, hearingAidListener)
|
||||
aacpManager?.unregisterControlCommandListener(AACPManager.Companion.ControlCommandIdentifiers.HEARING_ASSIST_CONFIG, hearingAidListener)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = if (isSystemInDarkTheme()) Color(
|
||||
0xFF000000
|
||||
@@ -411,23 +450,14 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.tone_volume).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.tone_volume).uppercase(),
|
||||
mutableFloatState = toneVolumeValue,
|
||||
onValueChange = {
|
||||
toneVolumeValue.floatValue = it
|
||||
},
|
||||
valueRange = 0f..125f,
|
||||
snapPoints = listOf(100f),
|
||||
valueRange = 0f..100f,
|
||||
snapPoints = listOf(75f),
|
||||
startIcon = "\uDBC0\uDEA1",
|
||||
endIcon = "\uDBC0\uDEA9",
|
||||
independent = true
|
||||
@@ -442,8 +472,25 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
SinglePodANCSwitch()
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
VolumeControlSwitch()
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
LoudSoundReductionSwitch()
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
DropdownMenuComponent(
|
||||
label = stringResource(R.string.press_speed),
|
||||
@@ -461,8 +508,15 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
||||
?: 0.toByte()
|
||||
)
|
||||
},
|
||||
textColor = textColor
|
||||
textColor = textColor,
|
||||
hazeState = hazeState
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
DropdownMenuComponent(
|
||||
label = stringResource(R.string.press_and_hold_duration),
|
||||
options = listOf(
|
||||
@@ -479,8 +533,15 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
||||
?: 0.toByte()
|
||||
)
|
||||
},
|
||||
textColor = textColor
|
||||
textColor = textColor,
|
||||
hazeState = hazeState
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
|
||||
DropdownMenuComponent(
|
||||
label = stringResource(R.string.volume_swipe_speed),
|
||||
options = listOf(
|
||||
@@ -497,234 +558,237 @@ fun AccessibilitySettingsScreen(navController: NavController) {
|
||||
?: 1.toByte()
|
||||
)
|
||||
},
|
||||
textColor = textColor
|
||||
textColor = textColor,
|
||||
hazeState = hazeState
|
||||
)
|
||||
}
|
||||
|
||||
NavigationButton(
|
||||
to = "transparency_customization",
|
||||
name = stringResource(R.string.customize_transparency_mode),
|
||||
navController = navController
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.apply_eq_to).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(vertical = 0.dp)
|
||||
) {
|
||||
val darkModeLocal = isSystemInDarkTheme()
|
||||
|
||||
val phoneShape = RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
|
||||
var phoneBackgroundColor by remember {
|
||||
mutableStateOf(
|
||||
if (darkModeLocal) Color(
|
||||
0xFF1C1C1E
|
||||
) else Color(0xFFFFFFFF)
|
||||
)
|
||||
}
|
||||
val phoneAnimatedBackgroundColor by animateColorAsState(
|
||||
targetValue = phoneBackgroundColor,
|
||||
animationSpec = tween(durationMillis = 500)
|
||||
if (!hearingAidEnabled.value) {
|
||||
NavigationButton(
|
||||
to = "transparency_customization",
|
||||
name = stringResource(R.string.customize_transparency_mode),
|
||||
navController = navController
|
||||
)
|
||||
|
||||
Row(
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.apply_eq_to).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.background(phoneAnimatedBackgroundColor, phoneShape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
phoneBackgroundColor =
|
||||
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
phoneBackgroundColor =
|
||||
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
phoneEQEnabled.value = !phoneEQEnabled.value
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(vertical = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.phone),
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Checkbox(
|
||||
checked = phoneEQEnabled.value,
|
||||
onCheckedChange = { phoneEQEnabled.value = it },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.scale(1.5f)
|
||||
)
|
||||
}
|
||||
val darkModeLocal = isSystemInDarkTheme()
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888)
|
||||
)
|
||||
|
||||
val mediaShape = RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
|
||||
var mediaBackgroundColor by remember {
|
||||
mutableStateOf(
|
||||
if (darkModeLocal) Color(
|
||||
0xFF1C1C1E
|
||||
) else Color(0xFFFFFFFF)
|
||||
val phoneShape = RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)
|
||||
var phoneBackgroundColor by remember {
|
||||
mutableStateOf(
|
||||
if (darkModeLocal) Color(
|
||||
0xFF1C1C1E
|
||||
) else Color(0xFFFFFFFF)
|
||||
)
|
||||
}
|
||||
val phoneAnimatedBackgroundColor by animateColorAsState(
|
||||
targetValue = phoneBackgroundColor,
|
||||
animationSpec = tween(durationMillis = 500)
|
||||
)
|
||||
}
|
||||
val mediaAnimatedBackgroundColor by animateColorAsState(
|
||||
targetValue = mediaBackgroundColor,
|
||||
animationSpec = tween(durationMillis = 500)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.background(mediaAnimatedBackgroundColor, mediaShape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
mediaBackgroundColor =
|
||||
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
mediaBackgroundColor =
|
||||
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
mediaEQEnabled.value = !mediaEQEnabled.value
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.media),
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Checkbox(
|
||||
checked = mediaEQEnabled.value,
|
||||
onCheckedChange = { mediaEQEnabled.value = it },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.scale(1.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
for (i in 0 until 8) {
|
||||
val eqPhoneValue =
|
||||
remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.height(38.dp)
|
||||
.background(phoneAnimatedBackgroundColor, phoneShape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
phoneBackgroundColor =
|
||||
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
phoneBackgroundColor =
|
||||
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
phoneEQEnabled.value = !phoneEQEnabled.value
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = String.format("%.2f", eqPhoneValue.floatValue),
|
||||
fontSize = 12.sp,
|
||||
stringResource(R.string.phone),
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Slider(
|
||||
value = eqPhoneValue.floatValue,
|
||||
onValueChange = { newVal ->
|
||||
eqPhoneValue.floatValue = newVal
|
||||
val newEQ = phoneMediaEQ.value.copyOf()
|
||||
newEQ[i] = eqPhoneValue.floatValue
|
||||
phoneMediaEQ.value = newEQ
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(36.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
activeTrackColor = activeTrackColor,
|
||||
inactiveTrackColor = trackColor
|
||||
Checkbox(
|
||||
checked = phoneEQEnabled.value,
|
||||
onCheckedChange = { phoneEQEnabled.value = it },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent
|
||||
),
|
||||
thumb = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.shadow(4.dp, CircleShape)
|
||||
.background(thumbColor, CircleShape)
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.scale(1.5f)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = Color(0x40888888)
|
||||
)
|
||||
|
||||
val mediaShape = RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)
|
||||
var mediaBackgroundColor by remember {
|
||||
mutableStateOf(
|
||||
if (darkModeLocal) Color(
|
||||
0xFF1C1C1E
|
||||
) else Color(0xFFFFFFFF)
|
||||
)
|
||||
}
|
||||
val mediaAnimatedBackgroundColor by animateColorAsState(
|
||||
targetValue = mediaBackgroundColor,
|
||||
animationSpec = tween(durationMillis = 500)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.background(mediaAnimatedBackgroundColor, mediaShape)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
mediaBackgroundColor =
|
||||
if (darkModeLocal) Color(0x40888888) else Color(0x40D9D9D9)
|
||||
tryAwaitRelease()
|
||||
mediaBackgroundColor =
|
||||
if (darkModeLocal) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
mediaEQEnabled.value = !mediaEQEnabled.value
|
||||
}
|
||||
)
|
||||
},
|
||||
track = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
)
|
||||
{
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.media),
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Checkbox(
|
||||
checked = mediaEQEnabled.value,
|
||||
onCheckedChange = { mediaEQEnabled.value = it },
|
||||
colors = CheckboxDefaults.colors().copy(
|
||||
checkedCheckmarkColor = Color(0xFF007AFF),
|
||||
uncheckedCheckmarkColor = Color.Transparent,
|
||||
checkedBoxColor = Color.Transparent,
|
||||
uncheckedBoxColor = Color.Transparent,
|
||||
checkedBorderColor = Color.Transparent,
|
||||
uncheckedBorderColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.scale(1.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
for (i in 0 until 8) {
|
||||
val eqPhoneValue =
|
||||
remember(phoneMediaEQ.value[i]) { mutableFloatStateOf(phoneMediaEQ.value[i]) }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(38.dp)
|
||||
) {
|
||||
Text(
|
||||
text = String.format("%.2f", eqPhoneValue.floatValue),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
|
||||
Slider(
|
||||
value = eqPhoneValue.floatValue,
|
||||
onValueChange = { newVal ->
|
||||
eqPhoneValue.floatValue = newVal
|
||||
val newEQ = phoneMediaEQ.value.copyOf()
|
||||
newEQ[i] = eqPhoneValue.floatValue
|
||||
phoneMediaEQ.value = newEQ
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(36.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
activeTrackColor = activeTrackColor,
|
||||
inactiveTrackColor = trackColor
|
||||
),
|
||||
thumb = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.shadow(4.dp, CircleShape)
|
||||
.background(thumbColor, CircleShape)
|
||||
)
|
||||
},
|
||||
track = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(trackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(eqPhoneValue.floatValue / 100f)
|
||||
.height(4.dp)
|
||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||||
.height(12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
)
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(trackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(eqPhoneValue.floatValue / 100f)
|
||||
.height(4.dp)
|
||||
.background(activeTrackColor, RoundedCornerShape(4.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.band_label, i + 1),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.band_label, i + 1),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -832,55 +896,129 @@ fun AccessibilityToggle(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun DropdownMenuComponent(
|
||||
label: String,
|
||||
options: List<String>,
|
||||
selectedOption: String,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
textColor: Color
|
||||
textColor: Color,
|
||||
hazeState: HazeState
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val density = LocalDensity.current
|
||||
val itemHeightPx = with(density) { 48.dp.toPx() }
|
||||
|
||||
Column(
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchOffset by remember { mutableStateOf<Offset?>(null) }
|
||||
var boxPosition by remember { mutableStateOf(Offset.Zero) }
|
||||
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
||||
var parentHoveredIndex by remember { mutableStateOf<Int?>(null) }
|
||||
var parentDragActive by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(start = 12.dp, end = 12.dp)
|
||||
.height(55.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { offset ->
|
||||
val now = System.currentTimeMillis()
|
||||
if (expanded) {
|
||||
expanded = false
|
||||
lastDismissTime = now
|
||||
} else {
|
||||
if (now - lastDismissTime > 250L) {
|
||||
touchOffset = offset
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { offset ->
|
||||
val now = System.currentTimeMillis()
|
||||
touchOffset = offset
|
||||
if (!expanded && now - lastDismissTime > 250L) {
|
||||
expanded = true
|
||||
}
|
||||
lastDismissTime = now
|
||||
parentDragActive = true
|
||||
parentHoveredIndex = 0
|
||||
},
|
||||
onDrag = { change, _ ->
|
||||
val current = change.position
|
||||
val touch = touchOffset ?: current
|
||||
val posInPopupY = current.y - touch.y
|
||||
val idx = (posInPopupY / itemHeightPx).toInt()
|
||||
parentHoveredIndex = idx
|
||||
},
|
||||
onDragEnd = {
|
||||
parentDragActive = false
|
||||
parentHoveredIndex?.let { idx ->
|
||||
if (idx in options.indices) {
|
||||
onOptionSelected(options[idx])
|
||||
expanded = false
|
||||
lastDismissTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
parentHoveredIndex = null
|
||||
},
|
||||
onDragCancel = {
|
||||
parentDragActive = false
|
||||
parentHoveredIndex = null
|
||||
}
|
||||
)
|
||||
},
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = textColor
|
||||
)
|
||||
fontSize = 16.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = true }
|
||||
.padding(8.dp)
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
boxPosition = coordinates.positionInParent()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = selectedOption,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
options.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onOptionSelected(option)
|
||||
expanded = false
|
||||
},
|
||||
text = { Text(text = option) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = selectedOption,
|
||||
fontSize = 16.sp,
|
||||
color = textColor.copy(alpha = 0.8f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
StyledDropdown(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
lastDismissTime = System.currentTimeMillis()
|
||||
},
|
||||
options = options,
|
||||
selectedOption = selectedOption,
|
||||
touchOffset = touchOffset,
|
||||
boxPosition = boxPosition,
|
||||
externalHoveredIndex = parentHoveredIndex,
|
||||
externalDragActive = parentDragActive,
|
||||
onOptionSelected = { option ->
|
||||
onOptionSelected(option)
|
||||
expanded = false
|
||||
},
|
||||
hazeState = hazeState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,17 +343,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.amplification).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.amplification).uppercase(),
|
||||
valueRange = -1f..1f,
|
||||
mutableFloatState = amplificationSliderValue,
|
||||
onValueChange = {
|
||||
@@ -374,17 +365,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
||||
description = stringResource(R.string.swipe_amplification_description)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.balance).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.balance).uppercase(),
|
||||
valueRange = -1f..1f,
|
||||
mutableFloatState = balanceSliderValue,
|
||||
onValueChange = {
|
||||
@@ -396,17 +378,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
||||
independent = true,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.tone).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.tone).uppercase(),
|
||||
valueRange = -1f..1f,
|
||||
mutableFloatState = toneSliderValue,
|
||||
onValueChange = {
|
||||
@@ -417,18 +390,8 @@ fun HearingAidAdjustmentsScreen(@Suppress("unused") navController: NavController
|
||||
independent = true,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||
valueRange = 0f..1f,
|
||||
mutableFloatState = ambientNoiseReductionSliderValue,
|
||||
onValueChange = {
|
||||
|
||||
@@ -363,17 +363,8 @@ fun TransparencySettingsScreen(navController: NavController) {
|
||||
description = stringResource(R.string.customize_transparency_mode_description)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.amplification).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.amplification).uppercase(),
|
||||
valueRange = -1f..1f,
|
||||
mutableFloatState = amplificationSliderValue,
|
||||
onValueChange = {
|
||||
@@ -384,17 +375,8 @@ fun TransparencySettingsScreen(navController: NavController) {
|
||||
independent = true
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.balance).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.balance).uppercase(),
|
||||
valueRange = -1f..1f,
|
||||
mutableFloatState = balanceSliderValue,
|
||||
onValueChange = {
|
||||
@@ -406,17 +388,8 @@ fun TransparencySettingsScreen(navController: NavController) {
|
||||
independent = true,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.tone).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.tone).uppercase(),
|
||||
valueRange = -1f..1f,
|
||||
mutableFloatState = toneSliderValue,
|
||||
onValueChange = {
|
||||
@@ -427,18 +400,8 @@ fun TransparencySettingsScreen(navController: NavController) {
|
||||
independent = true,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
color = textColor.copy(alpha = 0.6f),
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||
),
|
||||
modifier = Modifier.padding(8.dp, bottom = 0.dp)
|
||||
)
|
||||
|
||||
StyledSlider(
|
||||
label = stringResource(R.string.ambient_noise_reduction).uppercase(),
|
||||
valueRange = 0f..1f,
|
||||
mutableFloatState = ambientNoiseReductionSliderValue,
|
||||
onValueChange = {
|
||||
|
||||
@@ -25,7 +25,8 @@ data class TransparencySettings(
|
||||
val leftAmbientNoiseReduction: Float,
|
||||
val rightAmbientNoiseReduction: Float,
|
||||
val netAmplification: Float,
|
||||
val balance: Float
|
||||
val balance: Float,
|
||||
val ownVoiceAmplification: Float? = null
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -44,6 +45,7 @@ data class TransparencySettings(
|
||||
if (rightAmbientNoiseReduction != other.rightAmbientNoiseReduction) return false
|
||||
if (!leftEQ.contentEquals(other.leftEQ)) return false
|
||||
if (!rightEQ.contentEquals(other.rightEQ)) return false
|
||||
if (ownVoiceAmplification != other.ownVoiceAmplification) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -60,6 +62,7 @@ data class TransparencySettings(
|
||||
result = 31 * result + rightAmbientNoiseReduction.hashCode()
|
||||
result = 31 * result + leftEQ.contentHashCode()
|
||||
result = 31 * result + rightEQ.contentHashCode()
|
||||
result = 31 * result + (ownVoiceAmplification?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -91,6 +94,12 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
|
||||
val rightConversationBoost = rightConvFloat > 0.5f
|
||||
val rightAmbientNoiseReduction = buffer.float
|
||||
|
||||
val ownVoiceAmplification = if (buffer.remaining() >= 4) {
|
||||
buffer.float
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val avg = (leftAmplification + rightAmplification) / 2
|
||||
val amplification = avg.coerceIn(-1f, 1f)
|
||||
val diff = rightAmplification - leftAmplification
|
||||
@@ -109,7 +118,8 @@ fun parseTransparencySettingsResponse(data: ByteArray): TransparencySettings? {
|
||||
leftAmbientNoiseReduction = leftAmbientNoiseReduction,
|
||||
rightAmbientNoiseReduction = rightAmbientNoiseReduction,
|
||||
netAmplification = amplification,
|
||||
balance = balance
|
||||
balance = balance,
|
||||
ownVoiceAmplification = ownVoiceAmplification
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,7 +130,9 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans
|
||||
debounceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(100)
|
||||
try {
|
||||
val buffer = ByteBuffer.allocate(100).order(ByteOrder.LITTLE_ENDIAN)
|
||||
val buffer = ByteBuffer.allocate(
|
||||
if (transparencySettings.ownVoiceAmplification != null) 104 else 100
|
||||
).order(ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
buffer.putFloat(if (transparencySettings.enabled) 1.0f else 0.0f)
|
||||
|
||||
@@ -140,6 +152,10 @@ fun sendTransparencySettings(attManager: ATTManager, transparencySettings: Trans
|
||||
buffer.putFloat(if (transparencySettings.rightConversationBoost) 1.0f else 0.0f)
|
||||
buffer.putFloat(transparencySettings.rightAmbientNoiseReduction)
|
||||
|
||||
if (transparencySettings.ownVoiceAmplification != null) {
|
||||
buffer.putFloat(transparencySettings.ownVoiceAmplification)
|
||||
}
|
||||
|
||||
val data = buffer.array()
|
||||
attManager.write(ATTHandles.TRANSPARENCY, value = data)
|
||||
} catch (e: IOException) {
|
||||
|
||||
Reference in New Issue
Block a user