From 5b187c6d5b42861e1126e2ad25c5300425da648c Mon Sep 17 00:00:00 2001 From: darken Date: Thu, 16 Apr 2026 17:40:35 +0200 Subject: [PATCH] refactor(device-settings): Extract setting controls into own files --- .../ui/devicesettings/DeviceSettingsScreen.kt | 267 +++--------------- .../components/CallControlSettings.kt | 135 +++++++++ .../components/ConnectedDevicesList.kt | 87 ++++++ .../devicesettings/components/EqBarsChart.kt | 67 +++++ .../components/PressHoldDurationSetting.kt | 42 +++ .../components/PressSpeedSetting.kt | 42 +++ .../components/VolumeSwipeLengthSetting.kt | 42 +++ 7 files changed, 450 insertions(+), 232 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/CallControlSettings.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/ConnectedDevicesList.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/EqBarsChart.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressHoldDurationSetting.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressSpeedSetting.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/VolumeSwipeLengthSetting.kt diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index 7ceebe5b..3cefa3db 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -3,7 +3,6 @@ package eu.darken.capod.main.ui.devicesettings import android.content.Intent import android.os.Build import android.provider.Settings -import androidx.compose.foundation.Canvas import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.layout.Column @@ -19,7 +18,6 @@ import androidx.compose.material.icons.automirrored.twotone.ArrowBack import androidx.compose.material.icons.automirrored.twotone.Message import androidx.compose.material.icons.automirrored.twotone.VolumeUp import androidx.compose.material.icons.twotone.BluetoothConnected -import androidx.compose.material.icons.twotone.DevicesOther import androidx.compose.material.icons.twotone.GraphicEq import androidx.compose.material.icons.twotone.Headphones import androidx.compose.material.icons.twotone.Hearing @@ -29,9 +27,7 @@ import androidx.compose.material.icons.twotone.Mic import androidx.compose.material.icons.twotone.Nightlight import androidx.compose.material.icons.twotone.PauseCircle import androidx.compose.material.icons.twotone.PlayCircle -import androidx.compose.material.icons.twotone.Speed import androidx.compose.material.icons.twotone.Swipe -import androidx.compose.material.icons.twotone.Timer import androidx.compose.material.icons.twotone.TouchApp import androidx.compose.material.icons.twotone.Workspaces import androidx.compose.material3.AlertDialog @@ -41,13 +37,13 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -91,6 +87,12 @@ import eu.darken.capod.main.ui.devicesettings.cards.DeviceDetailItem import eu.darken.capod.main.ui.devicesettings.cards.DeviceInfoCard import eu.darken.capod.main.ui.devicesettings.cards.buildModelLabel import eu.darken.capod.main.ui.devicesettings.cards.NotConnectedCard +import eu.darken.capod.main.ui.devicesettings.components.CallControlSettings +import eu.darken.capod.main.ui.devicesettings.components.ConnectedDevicesList +import eu.darken.capod.main.ui.devicesettings.components.EqBarsChart +import eu.darken.capod.main.ui.devicesettings.components.PressHoldDurationSetting +import eu.darken.capod.main.ui.devicesettings.components.PressSpeedSetting +import eu.darken.capod.main.ui.devicesettings.components.VolumeSwipeLengthSetting import eu.darken.capod.main.ui.components.icon import eu.darken.capod.main.ui.components.shortLabel import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector @@ -121,6 +123,11 @@ fun DeviceSettingsScreenHost( val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } var showRenameUnavailableDialog by rememberSaveable { mutableStateOf(false) } + var showListeningModeCycleDialog by rememberSaveable { mutableStateOf(false) } + val state by vm.state.collectAsStateWithLifecycle(initialValue = null) + val offRejectedMessage = stringResource(R.string.device_settings_anc_off_rejected_message) + val offRejectedAction = stringResource(R.string.device_settings_anc_off_rejected_action) + LaunchedEffect(Unit) { vm.events.collect { event -> when (event) { @@ -135,6 +142,17 @@ fun DeviceSettingsScreenHost( DeviceSettingsViewModel.Event.SystemRenameUnavailable -> { showRenameUnavailableDialog = true } + DeviceSettingsViewModel.Event.OffModeRejectedByDevice -> { + val isPro = state?.isPro == true + val result = snackbarHostState.showSnackbar( + message = offRejectedMessage, + actionLabel = if (isPro) offRejectedAction else null, + withDismissAction = true, + ) + if (result == SnackbarResult.ActionPerformed && isPro) { + showListeningModeCycleDialog = true + } + } } } } @@ -149,12 +167,13 @@ fun DeviceSettingsScreenHost( ) } - val state by vm.state.collectAsStateWithLifecycle(initialValue = null) val currentState = state ?: return DeviceSettingsScreen( state = currentState, snackbarHostState = snackbarHostState, + showListeningModeCycleDialog = showListeningModeCycleDialog, + onShowListeningModeCycleDialogChange = { showListeningModeCycleDialog = it }, onNavigateUp = { vm.navUp() }, onAncModeChange = { vm.setAncMode(it) }, onConversationalAwarenessChange = { vm.setConversationalAwareness(it) }, @@ -192,6 +211,8 @@ fun DeviceSettingsScreenHost( fun DeviceSettingsScreen( state: DeviceSettingsViewModel.State, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + showListeningModeCycleDialog: Boolean = false, + onShowListeningModeCycleDialogChange: (Boolean) -> Unit = {}, onNavigateUp: () -> Unit, onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {}, onConversationalAwarenessChange: (Boolean) -> Unit = {}, @@ -230,7 +251,6 @@ fun DeviceSettingsScreen( val reactions = state.reactions var showAutoConnectConditionDialog by remember { mutableStateOf(false) } - var showListeningModeCycleDialog by rememberSaveable { mutableStateOf(false) } Scaffold( topBar = { @@ -584,7 +604,7 @@ fun DeviceSettingsScreen( ), onClick = { if (isPro) { - showListeningModeCycleDialog = true + onShowListeningModeCycleDialogChange(true) } else { onUpgrade() } @@ -723,37 +743,21 @@ fun DeviceSettingsScreen( ) } if (features.hasEndCallMuteMic && endCallMuteMic != null) { - EndCallMuteMicControl( + CallControlSettings( current = endCallMuteMic, onChange = onEndCallMuteMicChange, enabled = enabled, ) } if (features.hasPressSpeed && pressSpd != null) { - SegmentedSettingRow( - icon = Icons.TwoTone.Speed, - title = stringResource(R.string.device_settings_press_speed_label), - subtitle = stringResource(R.string.device_settings_press_speed_description), - options = listOf( - stringResource(R.string.device_settings_press_speed_default) to AapSetting.PressSpeed.Value.DEFAULT, - stringResource(R.string.device_settings_press_speed_slower) to AapSetting.PressSpeed.Value.SLOWER, - stringResource(R.string.device_settings_press_speed_slowest) to AapSetting.PressSpeed.Value.SLOWEST, - ), + PressSpeedSetting( selected = pressSpd.value, onSelected = onPressSpeedChange, enabled = enabled, ) } if (features.hasPressHoldDuration && pressHold != null) { - SegmentedSettingRow( - icon = Icons.TwoTone.Timer, - title = stringResource(R.string.device_settings_press_hold_label), - subtitle = stringResource(R.string.device_settings_press_hold_description), - options = listOf( - stringResource(R.string.device_settings_press_hold_default) to AapSetting.PressHoldDuration.Value.DEFAULT, - stringResource(R.string.device_settings_press_hold_shorter) to AapSetting.PressHoldDuration.Value.SHORTER, - stringResource(R.string.device_settings_press_hold_shortest) to AapSetting.PressHoldDuration.Value.SHORTEST, - ), + PressHoldDurationSetting( selected = pressHold.value, onSelected = onPressHoldDurationChange, enabled = enabled, @@ -770,15 +774,7 @@ fun DeviceSettingsScreen( ) } if (features.hasVolumeSwipeLength && volSwipeLen != null) { - SegmentedSettingRow( - icon = Icons.TwoTone.Swipe, - title = stringResource(R.string.device_settings_volume_swipe_length_label), - subtitle = stringResource(R.string.device_settings_volume_swipe_length_description), - options = listOf( - stringResource(R.string.device_settings_volume_swipe_length_default) to AapSetting.VolumeSwipeLength.Value.DEFAULT, - stringResource(R.string.device_settings_volume_swipe_length_longer) to AapSetting.VolumeSwipeLength.Value.LONGER, - stringResource(R.string.device_settings_volume_swipe_length_longest) to AapSetting.VolumeSwipeLength.Value.LONGEST, - ), + VolumeSwipeLengthSetting( selected = volSwipeLen.value, onSelected = onVolumeSwipeLengthChange, enabled = enabled, @@ -841,7 +837,7 @@ fun DeviceSettingsScreen( onListeningModeCycleChange(newMask) } }, - onDismiss = { showListeningModeCycleDialog = false }, + onDismiss = { onShowListeningModeCycleDialogChange(false) }, ) } } @@ -1047,7 +1043,7 @@ private fun AdaptiveNoiseSlider( } @Composable -private fun SettingsCompoundHeader( +internal fun SettingsCompoundHeader( icon: ImageVector, title: String, subtitle: String?, @@ -1078,7 +1074,7 @@ private fun SettingsCompoundHeader( } @Composable -private fun SegmentedSettingRow( +internal fun SegmentedSettingRow( icon: ImageVector, title: String, options: List>, @@ -1122,199 +1118,6 @@ private fun SegmentedSettingRow( } } -@Composable -private fun EndCallMuteMicControl( - current: AapSetting.EndCallMuteMic, - onChange: (AapSetting.EndCallMuteMic.MuteMicMode, AapSetting.EndCallMuteMic.EndCallMode) -> Unit, - enabled: Boolean, -) { - val optionASelected = current.endCall == AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS - - Surface( - color = MaterialTheme.colorScheme.surfaceContainerLow, - shape = RoundedCornerShape(16.dp), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - ) { - Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp)) { - SettingsCompoundHeader( - icon = Icons.TwoTone.TouchApp, - title = stringResource(R.string.device_settings_end_call_mute_mic_label), - subtitle = stringResource(R.string.device_settings_end_call_mute_mic_description), - enabled = enabled, - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Column(modifier = Modifier.selectableGroup()) { - CallControlOption( - title = stringResource(R.string.device_settings_end_call_mute_mic_option_a_title), - subtitle = stringResource(R.string.device_settings_end_call_mute_mic_option_a_subtitle), - selected = optionASelected, - enabled = enabled, - onClick = { - onChange( - AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS, - AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS, - ) - }, - ) - - CallControlOption( - title = stringResource(R.string.device_settings_end_call_mute_mic_option_b_title), - subtitle = stringResource(R.string.device_settings_end_call_mute_mic_option_b_subtitle), - selected = !optionASelected, - enabled = enabled, - onClick = { - onChange( - AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS, - AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS, - ) - }, - ) - } - } - } -} - -@Composable -private fun CallControlOption( - title: String, - subtitle: String, - selected: Boolean, - enabled: Boolean, - onClick: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = selected, - enabled = enabled, - role = Role.RadioButton, - onClick = { if (!selected) onClick() }, - ) - .padding(vertical = 4.dp), - ) { - RadioButton( - selected = selected, - onClick = null, - enabled = enabled, - ) - Column(modifier = Modifier.padding(start = 4.dp)) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f), - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = if (enabled) 1f else 0.5f), - ) - } - } -} - -@Composable -private fun EqBarsChart( - sets: List>, - modifier: Modifier = Modifier, -) { - val primary = MaterialTheme.colorScheme.primary - val outline = MaterialTheme.colorScheme.outlineVariant - - // Use the first EQ set (main) - val bands = sets.firstOrNull() ?: return - if (bands.size != 8) return - - Canvas( - modifier = modifier - .fillMaxWidth() - .height(80.dp), - ) { - val barCount = bands.size - val spacing = 4.dp.toPx() - val cornerRadius = 4.dp.toPx() - val barWidth = (size.width - spacing * (barCount - 1)) / barCount - - for (i in bands.indices) { - val x = i * (barWidth + spacing) - // Fixed 0-100 scale — values are typically 0-100 from the device - val normalized = (bands[i] / 100f).coerceIn(0f, 1f) - val barHeight = (normalized * size.height).coerceAtLeast(2.dp.toPx()) - - // Background bar (rounded rect) - drawRoundRect( - color = outline, - topLeft = androidx.compose.ui.geometry.Offset(x, 0f), - size = androidx.compose.ui.geometry.Size(barWidth, size.height), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(cornerRadius), - ) - - // Filled bar from bottom (rounded rect) - drawRoundRect( - color = primary, - topLeft = androidx.compose.ui.geometry.Offset(x, size.height - barHeight), - size = androidx.compose.ui.geometry.Size(barWidth, barHeight), - cornerRadius = androidx.compose.ui.geometry.CornerRadius(cornerRadius), - ) - } - } -} - -@Composable -private fun ConnectedDevicesList( - devices: List, - audioSource: AapSetting.AudioSource?, -) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) { - Text( - text = stringResource(R.string.device_settings_connected_devices_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 8.dp), - ) - - for ((index, device) in devices.withIndex()) { - val isSource = audioSource?.sourceMac == device.mac - val statusLabel = if (isSource) { - when (audioSource?.type) { - AapSetting.AudioSource.AudioSourceType.CALL -> stringResource(R.string.device_settings_connected_device_call) - AapSetting.AudioSource.AudioSourceType.MEDIA -> stringResource(R.string.device_settings_connected_device_media) - else -> "" - } - } else "" - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.TwoTone.DevicesOther, - contentDescription = null, - tint = if (isSource) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(end = 16.dp), - ) - Column { - Text( - text = stringResource(R.string.device_settings_connected_device_label, index + 1), - style = MaterialTheme.typography.bodyMedium, - color = if (isSource) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, - ) - Text( - text = if (statusLabel.isNotEmpty()) statusLabel else device.mac, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - } -} - private fun cycleBit(mode: AapSetting.AncMode.Value): Int = when (mode) { AapSetting.AncMode.Value.OFF -> 0x01 AapSetting.AncMode.Value.ON -> 0x02 diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/CallControlSettings.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/CallControlSettings.kt new file mode 100644 index 00000000..84a44b6e --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/CallControlSettings.kt @@ -0,0 +1,135 @@ +package eu.darken.capod.main.ui.devicesettings.components + +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.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.TouchApp +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.main.ui.devicesettings.SettingsCompoundHeader +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Composable +fun CallControlSettings( + current: AapSetting.EndCallMuteMic, + onChange: (AapSetting.EndCallMuteMic.MuteMicMode, AapSetting.EndCallMuteMic.EndCallMode) -> Unit, + enabled: Boolean, +) { + val optionASelected = current.endCall == AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS + + Surface( + color = MaterialTheme.colorScheme.surfaceContainerLow, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp)) { + SettingsCompoundHeader( + icon = Icons.TwoTone.TouchApp, + title = stringResource(R.string.device_settings_end_call_mute_mic_label), + subtitle = stringResource(R.string.device_settings_end_call_mute_mic_description), + enabled = enabled, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Column(modifier = Modifier.selectableGroup()) { + CallControlOption( + title = stringResource(R.string.device_settings_end_call_mute_mic_option_a_title), + subtitle = stringResource(R.string.device_settings_end_call_mute_mic_option_a_subtitle), + selected = optionASelected, + enabled = enabled, + onClick = { + onChange( + AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS, + AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS, + ) + }, + ) + + CallControlOption( + title = stringResource(R.string.device_settings_end_call_mute_mic_option_b_title), + subtitle = stringResource(R.string.device_settings_end_call_mute_mic_option_b_subtitle), + selected = !optionASelected, + enabled = enabled, + onClick = { + onChange( + AapSetting.EndCallMuteMic.MuteMicMode.SINGLE_PRESS, + AapSetting.EndCallMuteMic.EndCallMode.DOUBLE_PRESS, + ) + }, + ) + } + } + } +} + +@Composable +private fun CallControlOption( + title: String, + subtitle: String, + selected: Boolean, + enabled: Boolean, + onClick: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selected, + enabled = enabled, + role = Role.RadioButton, + onClick = { if (!selected) onClick() }, + ) + .padding(vertical = 4.dp), + ) { + RadioButton( + selected = selected, + onClick = null, + enabled = enabled, + ) + Column(modifier = Modifier.padding(start = 4.dp)) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f), + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = if (enabled) 1f else 0.5f), + ) + } + } +} + +@Preview2 +@Composable +private fun CallControlSettingsPreview() = PreviewWrapper { + CallControlSettings( + current = AapSetting.EndCallMuteMic( + muteMic = AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS, + endCall = AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS, + ), + onChange = { _, _ -> }, + enabled = true, + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/ConnectedDevicesList.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/ConnectedDevicesList.kt new file mode 100644 index 00000000..1c911295 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/ConnectedDevicesList.kt @@ -0,0 +1,87 @@ +package eu.darken.capod.main.ui.devicesettings.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.DevicesOther +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Composable +fun ConnectedDevicesList( + devices: List, + audioSource: AapSetting.AudioSource?, +) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) { + Text( + text = stringResource(R.string.device_settings_connected_devices_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 8.dp), + ) + + for ((index, device) in devices.withIndex()) { + val isSource = audioSource?.sourceMac == device.mac + val statusLabel = if (isSource) { + when (audioSource?.type) { + AapSetting.AudioSource.AudioSourceType.CALL -> stringResource(R.string.device_settings_connected_device_call) + AapSetting.AudioSource.AudioSourceType.MEDIA -> stringResource(R.string.device_settings_connected_device_media) + else -> "" + } + } else "" + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.TwoTone.DevicesOther, + contentDescription = null, + tint = if (isSource) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(end = 16.dp), + ) + Column { + Text( + text = stringResource(R.string.device_settings_connected_device_label, index + 1), + style = MaterialTheme.typography.bodyMedium, + color = if (isSource) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + ) + Text( + text = if (statusLabel.isNotEmpty()) statusLabel else device.mac, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} + +@Preview2 +@Composable +private fun ConnectedDevicesListPreview() = PreviewWrapper { + ConnectedDevicesList( + devices = listOf( + AapSetting.ConnectedDevices.ConnectedDevice(mac = "AA:BB:CC:DD:EE:01", type = 2), + AapSetting.ConnectedDevices.ConnectedDevice(mac = "AA:BB:CC:DD:EE:02", type = 2), + ), + audioSource = AapSetting.AudioSource( + sourceMac = "AA:BB:CC:DD:EE:01", + type = AapSetting.AudioSource.AudioSourceType.MEDIA, + ), + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/EqBarsChart.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/EqBarsChart.kt new file mode 100644 index 00000000..501a2545 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/EqBarsChart.kt @@ -0,0 +1,67 @@ +package eu.darken.capod.main.ui.devicesettings.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.dp +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper + +@Composable +fun EqBarsChart( + sets: List>, + modifier: Modifier = Modifier, +) { + val primary = MaterialTheme.colorScheme.primary + val outline = MaterialTheme.colorScheme.outlineVariant + + val bands = sets.firstOrNull() ?: return + if (bands.size != 8) return + + Canvas( + modifier = modifier + .fillMaxWidth() + .height(80.dp), + ) { + val barCount = bands.size + val spacing = 4.dp.toPx() + val cornerRadius = 4.dp.toPx() + val barWidth = (size.width - spacing * (barCount - 1)) / barCount + + for (i in bands.indices) { + val x = i * (barWidth + spacing) + val normalized = (bands[i] / 100f).coerceIn(0f, 1f) + val barHeight = (normalized * size.height).coerceAtLeast(2.dp.toPx()) + + drawRoundRect( + color = outline, + topLeft = Offset(x, 0f), + size = Size(barWidth, size.height), + cornerRadius = CornerRadius(cornerRadius), + ) + + drawRoundRect( + color = primary, + topLeft = Offset(x, size.height - barHeight), + size = Size(barWidth, barHeight), + cornerRadius = CornerRadius(cornerRadius), + ) + } + } +} + +@Preview2 +@Composable +private fun EqBarsChartPreview() = PreviewWrapper { + EqBarsChart( + sets = listOf(listOf(10f, 25f, 40f, 60f, 80f, 70f, 50f, 30f)), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressHoldDurationSetting.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressHoldDurationSetting.kt new file mode 100644 index 00000000..d204e198 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressHoldDurationSetting.kt @@ -0,0 +1,42 @@ +package eu.darken.capod.main.ui.devicesettings.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Timer +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.main.ui.devicesettings.SegmentedSettingRow +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Composable +fun PressHoldDurationSetting( + selected: AapSetting.PressHoldDuration.Value, + onSelected: (AapSetting.PressHoldDuration.Value) -> Unit, + enabled: Boolean, +) { + SegmentedSettingRow( + icon = Icons.TwoTone.Timer, + title = stringResource(R.string.device_settings_press_hold_label), + subtitle = stringResource(R.string.device_settings_press_hold_description), + options = listOf( + stringResource(R.string.device_settings_press_hold_default) to AapSetting.PressHoldDuration.Value.DEFAULT, + stringResource(R.string.device_settings_press_hold_shorter) to AapSetting.PressHoldDuration.Value.SHORTER, + stringResource(R.string.device_settings_press_hold_shortest) to AapSetting.PressHoldDuration.Value.SHORTEST, + ), + selected = selected, + onSelected = onSelected, + enabled = enabled, + ) +} + +@Preview2 +@Composable +private fun PressHoldDurationSettingPreview() = PreviewWrapper { + PressHoldDurationSetting( + selected = AapSetting.PressHoldDuration.Value.DEFAULT, + onSelected = {}, + enabled = true, + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressSpeedSetting.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressSpeedSetting.kt new file mode 100644 index 00000000..1038bb54 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/PressSpeedSetting.kt @@ -0,0 +1,42 @@ +package eu.darken.capod.main.ui.devicesettings.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Speed +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.main.ui.devicesettings.SegmentedSettingRow +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Composable +fun PressSpeedSetting( + selected: AapSetting.PressSpeed.Value, + onSelected: (AapSetting.PressSpeed.Value) -> Unit, + enabled: Boolean, +) { + SegmentedSettingRow( + icon = Icons.TwoTone.Speed, + title = stringResource(R.string.device_settings_press_speed_label), + subtitle = stringResource(R.string.device_settings_press_speed_description), + options = listOf( + stringResource(R.string.device_settings_press_speed_default) to AapSetting.PressSpeed.Value.DEFAULT, + stringResource(R.string.device_settings_press_speed_slower) to AapSetting.PressSpeed.Value.SLOWER, + stringResource(R.string.device_settings_press_speed_slowest) to AapSetting.PressSpeed.Value.SLOWEST, + ), + selected = selected, + onSelected = onSelected, + enabled = enabled, + ) +} + +@Preview2 +@Composable +private fun PressSpeedSettingPreview() = PreviewWrapper { + PressSpeedSetting( + selected = AapSetting.PressSpeed.Value.DEFAULT, + onSelected = {}, + enabled = true, + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/VolumeSwipeLengthSetting.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/VolumeSwipeLengthSetting.kt new file mode 100644 index 00000000..f20c65cf --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/components/VolumeSwipeLengthSetting.kt @@ -0,0 +1,42 @@ +package eu.darken.capod.main.ui.devicesettings.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Swipe +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.main.ui.devicesettings.SegmentedSettingRow +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Composable +fun VolumeSwipeLengthSetting( + selected: AapSetting.VolumeSwipeLength.Value, + onSelected: (AapSetting.VolumeSwipeLength.Value) -> Unit, + enabled: Boolean, +) { + SegmentedSettingRow( + icon = Icons.TwoTone.Swipe, + title = stringResource(R.string.device_settings_volume_swipe_length_label), + subtitle = stringResource(R.string.device_settings_volume_swipe_length_description), + options = listOf( + stringResource(R.string.device_settings_volume_swipe_length_default) to AapSetting.VolumeSwipeLength.Value.DEFAULT, + stringResource(R.string.device_settings_volume_swipe_length_longer) to AapSetting.VolumeSwipeLength.Value.LONGER, + stringResource(R.string.device_settings_volume_swipe_length_longest) to AapSetting.VolumeSwipeLength.Value.LONGEST, + ), + selected = selected, + onSelected = onSelected, + enabled = enabled, + ) +} + +@Preview2 +@Composable +private fun VolumeSwipeLengthSettingPreview() = PreviewWrapper { + VolumeSwipeLengthSetting( + selected = AapSetting.VolumeSwipeLength.Value.DEFAULT, + onSelected = {}, + enabled = true, + ) +}