From 65a74e9bc12a0f230396146a62d9491f9b9a6064 Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 13 Apr 2026 14:06:37 +0200 Subject: [PATCH] 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. --- .../capod/common/settings/SettingsBaseItem.kt | 10 +- .../capod/common/settings/SettingsInfoBox.kt | 57 ++ .../capod/common/settings/SettingsSection.kt | 56 ++ .../ui/devicesettings/DeviceSettingsScreen.kt | 497 +++++++++--------- 4 files changed, 362 insertions(+), 258 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt create mode 100644 app/src/main/java/eu/darken/capod/common/settings/SettingsSection.kt diff --git a/app/src/main/java/eu/darken/capod/common/settings/SettingsBaseItem.kt b/app/src/main/java/eu/darken/capod/common/settings/SettingsBaseItem.kt index 9b9fa091..6c1486e7 100644 --- a/app/src/main/java/eu/darken/capod/common/settings/SettingsBaseItem.kt +++ b/app/src/main/java/eu/darken/capod/common/settings/SettingsBaseItem.kt @@ -9,18 +9,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector 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 eu.darken.capod.common.compose.Preview2 @@ -95,7 +96,10 @@ fun SettingsBaseItem( text = title, style = MaterialTheme.typography.bodyLarge, 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) { Spacer(modifier = Modifier.width(6.dp)) diff --git a/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt b/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt new file mode 100644 index 00000000..e91a7893 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt @@ -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.", + ) +} diff --git a/app/src/main/java/eu/darken/capod/common/settings/SettingsSection.kt b/app/src/main/java/eu/darken/capod/common/settings/SettingsSection.kt new file mode 100644 index 00000000..2735781c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/settings/SettingsSection.kt @@ -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 = {}, + ) + } +} 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 9b1793ac..6abada80 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 @@ -86,7 +86,9 @@ import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsBaseItem 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.SettingsSection import eu.darken.capod.common.settings.SettingsSliderItem import eu.darken.capod.common.settings.SettingsSwitchItem import eu.darken.capod.monitor.core.PodDevice @@ -282,101 +284,85 @@ fun DeviceSettingsScreen( // ── Reactions (per-profile, not gated on AAP) ───────────────── if (device != null && features != null) { - item("reactions_header") { - SettingsCategoryHeader(text = stringResource(R.string.settings_reaction_label)) - } - - if (features.hasEarDetection) { - item("reaction_auto_play") { + item("reactions_section") { + SettingsSection(title = stringResource(R.string.settings_reaction_label)) { + if (features.hasEarDetection) { + SettingsSwitchItem( + icon = Icons.TwoTone.PlayCircle, + 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( - icon = Icons.TwoTone.PlayCircle, - title = stringResource(R.string.settings_autopplay_label), - subtitle = stringResource(R.string.settings_autoplay_description), - checked = reactions.autoPlay, - onCheckedChange = onAutoPlayChange, - requiresUpgrade = !isPro, + 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_pause") { - 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, + 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.hasDualPods) { - val earDetectionActive = reactions.autoPlay || reactions.autoPause - item("reaction_one_pod_mode") { - 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), - ) - }, + if (features.hasCase) { + SettingsSwitchItem( + icon = Icons.AutoMirrored.TwoTone.Message, + title = stringResource(R.string.settings_popup_caseopen_label), + subtitle = stringResource(R.string.settings_popup_caseopen_description), + checked = reactions.showPopUpOnCaseOpen, + onCheckedChange = onShowPopUpOnCaseOpenChange, + requiresUpgrade = !isPro, ) } - } - 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( icon = Icons.AutoMirrored.TwoTone.Message, - title = stringResource(R.string.settings_popup_caseopen_label), - subtitle = stringResource(R.string.settings_popup_caseopen_description), - checked = reactions.showPopUpOnCaseOpen, - onCheckedChange = onShowPopUpOnCaseOpenChange, + title = stringResource(R.string.settings_popup_connected_label), + subtitle = stringResource(R.string.settings_popup_connected_description), + checked = reactions.showPopUpOnConnection, + onCheckedChange = onShowPopUpOnConnectionChange, 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 @@ -461,201 +447,185 @@ fun DeviceSettingsScreen( // ── Sound ──────────────────────────────────── val personalizedVol = device.personalizedVolume val toneVol = device.toneVolume - if ((features.hasPersonalizedVolume && personalizedVol != null) || (features.hasToneVolume && toneVol != null)) { - item("sound_header") { - SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_sound_label)) - } - } - - if (features.hasPersonalizedVolume && personalizedVol != null) { - item("personalized_volume") { - SettingsSwitchItem( - icon = Icons.AutoMirrored.TwoTone.VolumeUp, - title = stringResource(R.string.device_settings_personalized_volume_label), - subtitle = stringResource(R.string.device_settings_personalized_volume_description), - checked = personalizedVol.enabled, - onCheckedChange = onPersonalizedVolumeChange, - enabled = enabled, - ) - } - } - - if (features.hasToneVolume && toneVol != null) { - item("tone_volume") { - ToneVolumeSlider( - level = toneVol.level, - onLevelChange = onToneVolumeChange, - enabled = enabled, - ) + val showSoundSection = + (features.hasPersonalizedVolume && personalizedVol != null) || (features.hasToneVolume && toneVol != null) + if (showSoundSection) { + item("sound_section") { + SettingsSection(title = stringResource(R.string.device_settings_category_sound_label)) { + if (features.hasPersonalizedVolume && personalizedVol != null) { + SettingsSwitchItem( + icon = Icons.AutoMirrored.TwoTone.VolumeUp, + title = stringResource(R.string.device_settings_personalized_volume_label), + subtitle = stringResource(R.string.device_settings_personalized_volume_description), + checked = personalizedVol.enabled, + onCheckedChange = onPersonalizedVolumeChange, + enabled = enabled, + ) + } + if (features.hasToneVolume && toneVol != null) { + ToneVolumeSlider( + level = toneVol.level, + onLevelChange = onToneVolumeChange, + enabled = enabled, + ) + } + } } } // ── Controls ───────────────────────────────── - item("controls_header") { - SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_controls_label)) - } - 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 - 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 - 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 - 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 - if (features.hasEndCallMuteMic && endCallMuteMic != null) { - item("end_call_mute_mic") { - EndCallMuteMicControl( - current = endCallMuteMic, - onChange = onEndCallMuteMicChange, - enabled = enabled, - ) + val showControlsSection = features.hasStemConfig || + (features.hasEndCallMuteMic && endCallMuteMic != null) || + (features.hasPressSpeed && pressSpd != null) || + (features.hasPressHoldDuration && pressHold != null) || + (features.hasVolumeSwipe && volSwipe != null) || + (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) { - val micMode = device.microphoneMode - ?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO) - item("microphone_mode") { - SegmentedSettingRow( - icon = Icons.TwoTone.Mic, - title = stringResource(R.string.device_settings_microphone_mode_label), - subtitle = stringResource(R.string.device_settings_microphone_mode_description), - options = listOf( - stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO, - stringResource(R.string.device_settings_microphone_mode_left) to AapSetting.MicrophoneMode.Mode.ALWAYS_LEFT, - 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( - 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, - ) - } - } - - // ── 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, - ) + // ── Other ─────────────────────────────────── + val showOtherSection = features.hasMicrophoneMode || features.hasSleepDetection + if (showOtherSection) { + item("other_section") { + SettingsSection(title = stringResource(R.string.settings_category_other_label)) { + if (features.hasMicrophoneMode) { + val micMode = device.microphoneMode + ?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO) + SegmentedSettingRow( + icon = Icons.TwoTone.Mic, + title = stringResource(R.string.device_settings_microphone_mode_label), + subtitle = stringResource(R.string.device_settings_microphone_mode_description), + options = listOf( + stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO, + stringResource(R.string.device_settings_microphone_mode_left) to AapSetting.MicrophoneMode.Mode.ALWAYS_LEFT, + stringResource(R.string.device_settings_microphone_mode_right) to AapSetting.MicrophoneMode.Mode.ALWAYS_RIGHT, + ), + selected = micMode.mode, + onSelected = onMicrophoneModeChange, + enabled = enabled, + ) + } + if (features.hasSleepDetection) { + val sleepDet = device.sleepDetection + ?: AapSetting.SleepDetection(enabled = true) + 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 ─────────────────────────────── val connectedDevices = device.connectedDevices if (connectedDevices != null && connectedDevices.devices.isNotEmpty()) { - item("connections_header") { - SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_connections_label)) - } - item("connected_devices") { - ConnectedDevicesList( - devices = connectedDevices.devices, - audioSource = device.audioSource, - ) + item("connections_section") { + SettingsSection(title = stringResource(R.string.device_settings_category_connections_label)) { + ConnectedDevicesList( + devices = connectedDevices.devices, + audioSource = device.audioSource, + ) + } } } // EQ visualization (debug only) val eqBands = device.eqBands if (eu.darken.capod.BuildConfig.DEBUG && eqBands != null && eqBands.sets.isNotEmpty()) { - item("eq_header") { - SettingsCategoryHeader(text = stringResource(R.string.device_settings_eq_label)) - } - item("eq_bars") { - EqBarsChart( - sets = eqBands.sets, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) + item("eq_section") { + SettingsSection(title = stringResource(R.string.device_settings_eq_label)) { + EqBarsChart( + 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 @Composable private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper {