mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
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:
@@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+242
-255
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user