feat(settings): Group device settings into card containers

Wrap setting sections in SettingsSection cards using Material 3 surfaceContainerLow surfaces with rounded corners. Reorder Controls by usage frequency, move Microphone Mode to Other section, and replace ear detection info row with a contextual info box that only appears for BLE-only connections.
This commit is contained in:
darken
2026-04-14 11:28:41 +02:00
committed by Matthias Urhahn
parent 609423b27d
commit 65a74e9bc1
4 changed files with 362 additions and 258 deletions
@@ -9,18 +9,19 @@ import androidx.compose.foundation.layout.fillMaxWidth
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.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.Preview2
@@ -95,7 +96,10 @@ fun SettingsBaseItem(
text = title, text = title,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha) color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false),
) )
if (requiresUpgrade) { if (requiresUpgrade) {
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
@@ -0,0 +1,57 @@
package eu.darken.capod.common.settings
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.unit.dp
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
@Composable
fun SettingsInfoBox(
text: String,
modifier: Modifier = Modifier,
) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp),
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.Top,
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 10.dp, top = 2.dp)
.size(20.dp),
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
)
}
}
}
@Preview2
@Composable
private fun SettingsInfoBoxPreview() = PreviewWrapper {
SettingsInfoBox(
text = "If ear detection only works for one pod, this is an Apple limitation. Only the \"primary pod\" (used for microphone) is detected.",
)
}
@@ -0,0 +1,56 @@
package eu.darken.capod.common.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Notifications
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
@Composable
fun SettingsSection(
modifier: Modifier = Modifier,
title: String? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerLow,
shape = RoundedCornerShape(16.dp),
modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Column {
if (title != null) {
SettingsCategoryHeader(text = title)
}
content()
}
}
}
@Preview2
@Composable
private fun SettingsSectionPreview() = PreviewWrapper {
SettingsSection(title = "Reactions") {
SettingsSwitchItem(
icon = Icons.TwoTone.Notifications,
title = "Auto Play",
subtitle = "Resume playback when AirPods are inserted",
checked = true,
onCheckedChange = {},
)
SettingsSwitchItem(
icon = Icons.TwoTone.Notifications,
title = "Auto Pause",
subtitle = "Pause when AirPods are removed",
checked = false,
onCheckedChange = {},
)
}
}
@@ -86,7 +86,9 @@ import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsBaseItem import eu.darken.capod.common.settings.SettingsBaseItem
import eu.darken.capod.common.settings.SettingsCategoryHeader import eu.darken.capod.common.settings.SettingsCategoryHeader
import eu.darken.capod.common.settings.SettingsInfoBox
import eu.darken.capod.common.settings.SettingsPreferenceItem import eu.darken.capod.common.settings.SettingsPreferenceItem
import eu.darken.capod.common.settings.SettingsSection
import eu.darken.capod.common.settings.SettingsSliderItem import eu.darken.capod.common.settings.SettingsSliderItem
import eu.darken.capod.common.settings.SettingsSwitchItem import eu.darken.capod.common.settings.SettingsSwitchItem
import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.monitor.core.PodDevice
@@ -282,101 +284,85 @@ fun DeviceSettingsScreen(
// ── Reactions (per-profile, not gated on AAP) ───────────────── // ── Reactions (per-profile, not gated on AAP) ─────────────────
if (device != null && features != null) { if (device != null && features != null) {
item("reactions_header") { item("reactions_section") {
SettingsCategoryHeader(text = stringResource(R.string.settings_reaction_label)) SettingsSection(title = stringResource(R.string.settings_reaction_label)) {
} if (features.hasEarDetection) {
SettingsSwitchItem(
if (features.hasEarDetection) { icon = Icons.TwoTone.PlayCircle,
item("reaction_auto_play") { title = stringResource(R.string.settings_autopplay_label),
subtitle = stringResource(R.string.settings_autoplay_description),
checked = reactions.autoPlay,
onCheckedChange = onAutoPlayChange,
requiresUpgrade = !isPro,
)
SettingsSwitchItem(
icon = Icons.TwoTone.PauseCircle,
title = stringResource(R.string.settings_autopause_label),
subtitle = stringResource(R.string.settings_autopause_description),
checked = reactions.autoPause,
onCheckedChange = onAutoPauseChange,
requiresUpgrade = !isPro,
)
if (features.hasDualPods) {
val earDetectionActive = reactions.autoPlay || reactions.autoPause
SettingsBaseItem(
title = stringResource(R.string.settings_onepod_mode_label),
subtitle = stringResource(R.string.settings_onepod_mode_description),
icon = Icons.TwoTone.LooksOne,
onClick = { if (earDetectionActive) onOnePodModeChange(!reactions.onePodMode) },
enabled = earDetectionActive,
trailingContent = {
Switch(
checked = reactions.onePodMode,
onCheckedChange = onOnePodModeChange,
enabled = earDetectionActive,
modifier = Modifier.padding(start = 16.dp),
)
},
)
}
val earDetectionWarningVisible =
(reactions.autoPlay || reactions.autoPause) && !device.isAapConnected
if (earDetectionWarningVisible) {
SettingsInfoBox(
text = stringResource(R.string.settings_eardetection_info_description),
)
}
}
SettingsSwitchItem( SettingsSwitchItem(
icon = Icons.TwoTone.PlayCircle, icon = Icons.TwoTone.BluetoothConnected,
title = stringResource(R.string.settings_autopplay_label), title = stringResource(R.string.settings_autoconnect_label),
subtitle = stringResource(R.string.settings_autoplay_description), subtitle = stringResource(R.string.settings_autoconnect_description),
checked = reactions.autoPlay, checked = reactions.autoConnect,
onCheckedChange = onAutoPlayChange, onCheckedChange = onAutoConnectChange,
requiresUpgrade = !isPro,
) )
} SettingsBaseItem(
item("reaction_auto_pause") { title = stringResource(R.string.settings_autoconnect_condition_label),
SettingsSwitchItem( subtitle = stringResource(reactions.autoConnectCondition.labelRes),
icon = Icons.TwoTone.PauseCircle, icon = Icons.TwoTone.Workspaces,
title = stringResource(R.string.settings_autopause_label), onClick = { if (reactions.autoConnect) showAutoConnectConditionDialog = true },
subtitle = stringResource(R.string.settings_autopause_description), enabled = reactions.autoConnect,
checked = reactions.autoPause,
onCheckedChange = onAutoPauseChange,
requiresUpgrade = !isPro,
) )
} if (features.hasCase) {
if (features.hasDualPods) { SettingsSwitchItem(
val earDetectionActive = reactions.autoPlay || reactions.autoPause icon = Icons.AutoMirrored.TwoTone.Message,
item("reaction_one_pod_mode") { title = stringResource(R.string.settings_popup_caseopen_label),
SettingsBaseItem( subtitle = stringResource(R.string.settings_popup_caseopen_description),
title = stringResource(R.string.settings_onepod_mode_label), checked = reactions.showPopUpOnCaseOpen,
subtitle = stringResource(R.string.settings_onepod_mode_description), onCheckedChange = onShowPopUpOnCaseOpenChange,
icon = Icons.TwoTone.LooksOne, requiresUpgrade = !isPro,
onClick = { if (earDetectionActive) onOnePodModeChange(!reactions.onePodMode) },
enabled = earDetectionActive,
trailingContent = {
Switch(
checked = reactions.onePodMode,
onCheckedChange = onOnePodModeChange,
enabled = earDetectionActive,
modifier = Modifier.padding(start = 16.dp),
)
},
) )
} }
}
item("reaction_ear_detection_info") {
SettingsBaseItem(
title = stringResource(R.string.settings_eardetection_info_label),
subtitle = stringResource(R.string.settings_eardetection_info_description),
icon = Icons.TwoTone.QuestionMark,
onClick = {},
enabled = false,
)
}
}
item("reaction_auto_connect") {
SettingsSwitchItem(
icon = Icons.TwoTone.BluetoothConnected,
title = stringResource(R.string.settings_autoconnect_label),
subtitle = stringResource(R.string.settings_autoconnect_description),
checked = reactions.autoConnect,
onCheckedChange = onAutoConnectChange,
)
}
item("reaction_auto_connect_condition") {
SettingsBaseItem(
title = stringResource(R.string.settings_autoconnect_condition_label),
subtitle = stringResource(reactions.autoConnectCondition.labelRes),
icon = Icons.TwoTone.Workspaces,
onClick = { if (reactions.autoConnect) showAutoConnectConditionDialog = true },
enabled = reactions.autoConnect,
)
}
if (features.hasCase) {
item("reaction_popup_caseopen") {
SettingsSwitchItem( SettingsSwitchItem(
icon = Icons.AutoMirrored.TwoTone.Message, icon = Icons.AutoMirrored.TwoTone.Message,
title = stringResource(R.string.settings_popup_caseopen_label), title = stringResource(R.string.settings_popup_connected_label),
subtitle = stringResource(R.string.settings_popup_caseopen_description), subtitle = stringResource(R.string.settings_popup_connected_description),
checked = reactions.showPopUpOnCaseOpen, checked = reactions.showPopUpOnConnection,
onCheckedChange = onShowPopUpOnCaseOpenChange, onCheckedChange = onShowPopUpOnConnectionChange,
requiresUpgrade = !isPro, requiresUpgrade = !isPro,
) )
} }
} }
item("reaction_popup_connection") {
SettingsSwitchItem(
icon = Icons.AutoMirrored.TwoTone.Message,
title = stringResource(R.string.settings_popup_connected_label),
subtitle = stringResource(R.string.settings_popup_connected_description),
checked = reactions.showPopUpOnConnection,
onCheckedChange = onShowPopUpOnConnectionChange,
requiresUpgrade = !isPro,
)
}
} }
// Settings — only show when AAP is connected // Settings — only show when AAP is connected
@@ -461,201 +447,185 @@ fun DeviceSettingsScreen(
// ── Sound ──────────────────────────────────── // ── Sound ────────────────────────────────────
val personalizedVol = device.personalizedVolume val personalizedVol = device.personalizedVolume
val toneVol = device.toneVolume val toneVol = device.toneVolume
if ((features.hasPersonalizedVolume && personalizedVol != null) || (features.hasToneVolume && toneVol != null)) { val showSoundSection =
item("sound_header") { (features.hasPersonalizedVolume && personalizedVol != null) || (features.hasToneVolume && toneVol != null)
SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_sound_label)) if (showSoundSection) {
} item("sound_section") {
} SettingsSection(title = stringResource(R.string.device_settings_category_sound_label)) {
if (features.hasPersonalizedVolume && personalizedVol != null) {
if (features.hasPersonalizedVolume && personalizedVol != null) { SettingsSwitchItem(
item("personalized_volume") { icon = Icons.AutoMirrored.TwoTone.VolumeUp,
SettingsSwitchItem( title = stringResource(R.string.device_settings_personalized_volume_label),
icon = Icons.AutoMirrored.TwoTone.VolumeUp, subtitle = stringResource(R.string.device_settings_personalized_volume_description),
title = stringResource(R.string.device_settings_personalized_volume_label), checked = personalizedVol.enabled,
subtitle = stringResource(R.string.device_settings_personalized_volume_description), onCheckedChange = onPersonalizedVolumeChange,
checked = personalizedVol.enabled, enabled = enabled,
onCheckedChange = onPersonalizedVolumeChange, )
enabled = enabled, }
) if (features.hasToneVolume && toneVol != null) {
} ToneVolumeSlider(
} level = toneVol.level,
onLevelChange = onToneVolumeChange,
if (features.hasToneVolume && toneVol != null) { enabled = enabled,
item("tone_volume") { )
ToneVolumeSlider( }
level = toneVol.level, }
onLevelChange = onToneVolumeChange,
enabled = enabled,
)
} }
} }
// ── Controls ───────────────────────────────── // ── Controls ─────────────────────────────────
item("controls_header") {
SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_controls_label))
}
val pressSpd = device.pressSpeed val pressSpd = device.pressSpeed
if (features.hasPressSpeed && pressSpd != null) {
item("press_speed") {
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 = pressSpd.value,
onSelected = onPressSpeedChange,
enabled = enabled,
)
}
}
val pressHold = device.pressHoldDuration val pressHold = device.pressHoldDuration
if (features.hasPressHoldDuration && pressHold != null) {
item("press_hold") {
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 = pressHold.value,
onSelected = onPressHoldDurationChange,
enabled = enabled,
)
}
}
val volSwipe = device.volumeSwipe val volSwipe = device.volumeSwipe
if (features.hasVolumeSwipe && volSwipe != null) {
item("volume_swipe") {
SettingsSwitchItem(
icon = Icons.TwoTone.Swipe,
title = stringResource(R.string.device_settings_volume_swipe_label),
subtitle = stringResource(R.string.device_settings_volume_swipe_description),
checked = volSwipe.enabled,
onCheckedChange = onVolumeSwipeChange,
enabled = enabled,
)
}
}
val volSwipeLen = device.volumeSwipeLength val volSwipeLen = device.volumeSwipeLength
if (features.hasVolumeSwipeLength && volSwipeLen != null) {
item("volume_swipe_length") {
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 = volSwipeLen.value,
onSelected = onVolumeSwipeLengthChange,
enabled = enabled,
)
}
}
val endCallMuteMic = device.endCallMuteMic val endCallMuteMic = device.endCallMuteMic
if (features.hasEndCallMuteMic && endCallMuteMic != null) { val showControlsSection = features.hasStemConfig ||
item("end_call_mute_mic") { (features.hasEndCallMuteMic && endCallMuteMic != null) ||
EndCallMuteMicControl( (features.hasPressSpeed && pressSpd != null) ||
current = endCallMuteMic, (features.hasPressHoldDuration && pressHold != null) ||
onChange = onEndCallMuteMicChange, (features.hasVolumeSwipe && volSwipe != null) ||
enabled = enabled, (features.hasVolumeSwipeLength && volSwipeLen != null)
) if (showControlsSection) {
item("controls_section") {
SettingsSection(title = stringResource(R.string.device_settings_category_controls_label)) {
if (features.hasStemConfig) {
SettingsPreferenceItem(
icon = Icons.TwoTone.TouchApp,
title = stringResource(R.string.stem_actions_title),
subtitle = stringResource(R.string.stem_actions_nav_description),
onClick = onStemActionsClick,
enabled = enabled,
requiresUpgrade = !isPro,
)
}
if (features.hasEndCallMuteMic && endCallMuteMic != null) {
EndCallMuteMicControl(
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,
),
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,
),
selected = pressHold.value,
onSelected = onPressHoldDurationChange,
enabled = enabled,
)
}
if (features.hasVolumeSwipe && volSwipe != null) {
SettingsSwitchItem(
icon = Icons.TwoTone.Swipe,
title = stringResource(R.string.device_settings_volume_swipe_label),
subtitle = stringResource(R.string.device_settings_volume_swipe_description),
checked = volSwipe.enabled,
onCheckedChange = onVolumeSwipeChange,
enabled = enabled,
)
}
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,
),
selected = volSwipeLen.value,
onSelected = onVolumeSwipeLengthChange,
enabled = enabled,
)
}
}
} }
} }
if (features.hasMicrophoneMode) { // ── Other ───────────────────────────────────
val micMode = device.microphoneMode val showOtherSection = features.hasMicrophoneMode || features.hasSleepDetection
?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO) if (showOtherSection) {
item("microphone_mode") { item("other_section") {
SegmentedSettingRow( SettingsSection(title = stringResource(R.string.settings_category_other_label)) {
icon = Icons.TwoTone.Mic, if (features.hasMicrophoneMode) {
title = stringResource(R.string.device_settings_microphone_mode_label), val micMode = device.microphoneMode
subtitle = stringResource(R.string.device_settings_microphone_mode_description), ?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO)
options = listOf( SegmentedSettingRow(
stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO, icon = Icons.TwoTone.Mic,
stringResource(R.string.device_settings_microphone_mode_left) to AapSetting.MicrophoneMode.Mode.ALWAYS_LEFT, title = stringResource(R.string.device_settings_microphone_mode_label),
stringResource(R.string.device_settings_microphone_mode_right) to AapSetting.MicrophoneMode.Mode.ALWAYS_RIGHT, subtitle = stringResource(R.string.device_settings_microphone_mode_description),
), options = listOf(
selected = micMode.mode, stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO,
onSelected = onMicrophoneModeChange, stringResource(R.string.device_settings_microphone_mode_left) to AapSetting.MicrophoneMode.Mode.ALWAYS_LEFT,
enabled = enabled, stringResource(R.string.device_settings_microphone_mode_right) to AapSetting.MicrophoneMode.Mode.ALWAYS_RIGHT,
) ),
} selected = micMode.mode,
} onSelected = onMicrophoneModeChange,
enabled = enabled,
if (features.hasStemConfig) { )
item("stem_actions") { }
SettingsPreferenceItem( if (features.hasSleepDetection) {
icon = Icons.TwoTone.TouchApp, val sleepDet = device.sleepDetection
title = stringResource(R.string.stem_actions_title), ?: AapSetting.SleepDetection(enabled = true)
subtitle = stringResource(R.string.stem_actions_nav_description), SettingsSwitchItem(
onClick = onStemActionsClick, icon = Icons.TwoTone.Nightlight,
enabled = enabled, title = stringResource(R.string.device_settings_sleep_detection_label),
requiresUpgrade = !isPro, subtitle = stringResource(R.string.device_settings_sleep_detection_description),
) checked = sleepDet.enabled,
} onCheckedChange = onSleepDetectionChange,
} enabled = enabled,
requiresUpgrade = !isPro,
// ── General ────────────────────────────────── )
item("general_header") { }
SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_general_label)) }
}
if (features.hasSleepDetection) {
val sleepDet = device.sleepDetection
?: AapSetting.SleepDetection(enabled = true)
item("sleep_detection") {
SettingsSwitchItem(
icon = Icons.TwoTone.Nightlight,
title = stringResource(R.string.device_settings_sleep_detection_label),
subtitle = stringResource(R.string.device_settings_sleep_detection_description),
checked = sleepDet.enabled,
onCheckedChange = onSleepDetectionChange,
enabled = enabled,
requiresUpgrade = !isPro,
)
} }
} }
// ── Connections ─────────────────────────────── // ── Connections ───────────────────────────────
val connectedDevices = device.connectedDevices val connectedDevices = device.connectedDevices
if (connectedDevices != null && connectedDevices.devices.isNotEmpty()) { if (connectedDevices != null && connectedDevices.devices.isNotEmpty()) {
item("connections_header") { item("connections_section") {
SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_connections_label)) SettingsSection(title = stringResource(R.string.device_settings_category_connections_label)) {
} ConnectedDevicesList(
item("connected_devices") { devices = connectedDevices.devices,
ConnectedDevicesList( audioSource = device.audioSource,
devices = connectedDevices.devices, )
audioSource = device.audioSource, }
)
} }
} }
// EQ visualization (debug only) // EQ visualization (debug only)
val eqBands = device.eqBands val eqBands = device.eqBands
if (eu.darken.capod.BuildConfig.DEBUG && eqBands != null && eqBands.sets.isNotEmpty()) { if (eu.darken.capod.BuildConfig.DEBUG && eqBands != null && eqBands.sets.isNotEmpty()) {
item("eq_header") { item("eq_section") {
SettingsCategoryHeader(text = stringResource(R.string.device_settings_eq_label)) SettingsSection(title = stringResource(R.string.device_settings_eq_label)) {
} EqBarsChart(
item("eq_bars") { sets = eqBands.sets,
EqBarsChart( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
sets = eqBands.sets, )
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), }
)
} }
} }
} }
@@ -1423,6 +1393,23 @@ private fun DeviceSettingsInfoOnlyPreview() = PreviewWrapper {
) )
} }
@Preview2
@Composable
private fun DeviceSettingsLowFeaturePreview() = PreviewWrapper {
DeviceSettingsScreen(
state = DeviceSettingsViewModel.State(
device = PodDevice(
profileId = "preview-low-feature",
label = "Beats Solo 3",
ble = MockPodDataProvider.beatsSolo3(),
aap = null,
),
now = MOCK_NOW,
),
onNavigateUp = {},
)
}
@Preview2 @Preview2
@Composable @Composable
private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper { private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper {