mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
refactor(device-settings): Extract section cards into own files
This commit is contained in:
@@ -1,68 +1,33 @@
|
||||
package eu.darken.capod.main.ui.devicesettings
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.GraphicEq
|
||||
import androidx.compose.material.icons.twotone.Headphones
|
||||
import androidx.compose.material.icons.twotone.Hearing
|
||||
import androidx.compose.material.icons.twotone.LooksOne
|
||||
import androidx.compose.material.icons.twotone.Loop
|
||||
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.Swipe
|
||||
import androidx.compose.material.icons.twotone.TouchApp
|
||||
import androidx.compose.material.icons.twotone.Workspaces
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -74,35 +39,23 @@ import eu.darken.capod.common.compose.preview.MOCK_NOW
|
||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||
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.main.core.MonitorMode
|
||||
import eu.darken.capod.common.settings.InfoBoxType
|
||||
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.main.ui.devicesettings.cards.AapUnavailableCard
|
||||
import eu.darken.capod.main.ui.devicesettings.cards.ControlsCard
|
||||
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.NoiseControlCard
|
||||
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.cards.ReactionsCard
|
||||
import eu.darken.capod.main.ui.devicesettings.cards.SoundCard
|
||||
import eu.darken.capod.main.ui.devicesettings.cards.buildModelLabel
|
||||
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
|
||||
import eu.darken.capod.main.ui.devicesettings.dialogs.AutoConnectConditionDialog
|
||||
import eu.darken.capod.main.ui.devicesettings.dialogs.SystemRenameUnavailableDialog
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.firstSeenFormatted
|
||||
import eu.darken.capod.monitor.core.lastSeenFormatted
|
||||
import eu.darken.capod.monitor.core.resolvedAncCycleMask
|
||||
import eu.darken.capod.monitor.core.visibleAncModes
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
@@ -126,7 +79,6 @@ fun DeviceSettingsScreenHost(
|
||||
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 ->
|
||||
@@ -143,15 +95,7 @@ fun DeviceSettingsScreenHost(
|
||||
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
|
||||
}
|
||||
snackbarHostState.showSnackbar(offRejectedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +132,7 @@ fun DeviceSettingsScreenHost(
|
||||
onEndCallMuteMicChange = { muteMic, endCall -> vm.setEndCallMuteMic(muteMic, endCall) },
|
||||
onMicrophoneModeChange = { vm.setMicrophoneMode(it) },
|
||||
onListeningModeCycleChange = { vm.setListeningModeCycle(it) },
|
||||
onOffVisibilityChange = { enabled, mask -> vm.setListeningModeOffVisibility(enabled, mask) },
|
||||
onAllowOffOptionChange = { vm.setAllowOffOption(it) },
|
||||
onSleepDetectionChange = { vm.setSleepDetection(it) },
|
||||
onDeviceNameChange = { vm.setDeviceName(it) },
|
||||
onStemActionsClick = { vm.navToStemConfig() },
|
||||
@@ -227,7 +171,7 @@ fun DeviceSettingsScreen(
|
||||
onEndCallMuteMicChange: (AapSetting.EndCallMuteMic.MuteMicMode, AapSetting.EndCallMuteMic.EndCallMode) -> Unit = { _, _ -> },
|
||||
onMicrophoneModeChange: (AapSetting.MicrophoneMode.Mode) -> Unit = {},
|
||||
onListeningModeCycleChange: (Int) -> Unit = {},
|
||||
onOffVisibilityChange: (enabled: Boolean, currentCycleMask: Int) -> Unit = { _, _ -> },
|
||||
onAllowOffOptionChange: (Boolean) -> Unit = {},
|
||||
onSleepDetectionChange: (Boolean) -> Unit = {},
|
||||
onDeviceNameChange: (String) -> Unit = {},
|
||||
onStemActionsClick: () -> Unit = {},
|
||||
@@ -243,14 +187,10 @@ fun DeviceSettingsScreen(
|
||||
onFixMonitorMode: () -> Unit = {},
|
||||
onOpenIssueTracker: () -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val device = state.device
|
||||
val features = device?.model?.features
|
||||
val enabled = device?.isAapReady == true
|
||||
val isPro = state.isPro
|
||||
val reactions = state.reactions
|
||||
|
||||
var showAutoConnectConditionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -372,156 +312,23 @@ fun DeviceSettingsScreen(
|
||||
// ── Reactions (per-profile, not gated on AAP) ─────────────────
|
||||
if (device != null && features != null) {
|
||||
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 onePodModeActive = reactions.onePodMode ||
|
||||
reactions.autoPlay ||
|
||||
reactions.autoPause ||
|
||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_onepod_mode_label),
|
||||
subtitle = stringResource(R.string.settings_onepod_mode_description),
|
||||
icon = Icons.TwoTone.LooksOne,
|
||||
onClick = { if (onePodModeActive) onOnePodModeChange(!reactions.onePodMode) },
|
||||
enabled = onePodModeActive,
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = reactions.onePodMode,
|
||||
onCheckedChange = onOnePodModeChange,
|
||||
enabled = onePodModeActive,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
val earDetectionWarningVisible =
|
||||
(
|
||||
reactions.autoPlay ||
|
||||
reactions.autoPause ||
|
||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
|
||||
) && !device.isAapConnected
|
||||
if (earDetectionWarningVisible) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_eardetection_info_description),
|
||||
)
|
||||
}
|
||||
ReactionsDivider()
|
||||
}
|
||||
if (device.isAapConnected) {
|
||||
val convAwareness = device.conversationalAwareness
|
||||
val hasAnyAapReaction =
|
||||
(features.hasConversationAwareness && convAwareness != null) ||
|
||||
features.hasSleepDetection
|
||||
if (features.hasConversationAwareness && convAwareness != null) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.Hearing,
|
||||
title = stringResource(R.string.conversation_awareness_label),
|
||||
subtitle = stringResource(R.string.device_settings_conversation_awareness_description),
|
||||
checked = convAwareness.enabled,
|
||||
onCheckedChange = onConversationalAwarenessChange,
|
||||
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,
|
||||
)
|
||||
if (sleepDet.enabled) {
|
||||
SettingsInfoBox(
|
||||
title = stringResource(R.string.device_settings_experimental_title),
|
||||
text = stringResource(R.string.device_settings_experimental_description),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onOpenIssueTracker) {
|
||||
Text(stringResource(R.string.device_settings_experimental_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasAnyAapReaction) {
|
||||
ReactionsDivider()
|
||||
}
|
||||
}
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.BluetoothConnected,
|
||||
title = stringResource(R.string.settings_autoconnect_label),
|
||||
subtitle = stringResource(R.string.settings_autoconnect_description),
|
||||
checked = reactions.autoConnect,
|
||||
onCheckedChange = onAutoConnectChange,
|
||||
)
|
||||
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 (reactions.autoConnect && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_autoconnect_info_android12),
|
||||
)
|
||||
}
|
||||
ReactionsDivider()
|
||||
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,
|
||||
)
|
||||
}
|
||||
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,
|
||||
)
|
||||
val anyPopupEnabled = reactions.showPopUpOnCaseOpen || reactions.showPopUpOnConnection
|
||||
if (anyPopupEnabled && state.monitorMode == MonitorMode.MANUAL) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_popup_warning_manual_mode),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onFixMonitorMode) {
|
||||
Text(stringResource(R.string.general_fix_it_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (anyPopupEnabled) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_popup_info_not_in_app),
|
||||
)
|
||||
}
|
||||
}
|
||||
ReactionsCard(
|
||||
device = device,
|
||||
features = features,
|
||||
isPro = isPro,
|
||||
monitorMode = state.monitorMode,
|
||||
onAutoPlayChange = onAutoPlayChange,
|
||||
onAutoPauseChange = onAutoPauseChange,
|
||||
onOnePodModeChange = onOnePodModeChange,
|
||||
onConversationalAwarenessChange = onConversationalAwarenessChange,
|
||||
onSleepDetectionChange = onSleepDetectionChange,
|
||||
onAutoConnectChange = onAutoConnectChange,
|
||||
onAutoConnectConditionChange = onAutoConnectConditionChange,
|
||||
onShowPopUpOnCaseOpenChange = onShowPopUpOnCaseOpenChange,
|
||||
onShowPopUpOnConnectionChange = onShowPopUpOnConnectionChange,
|
||||
onFixMonitorMode = onFixMonitorMode,
|
||||
onOpenIssueTracker = onOpenIssueTracker,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,103 +344,24 @@ fun DeviceSettingsScreen(
|
||||
}
|
||||
|
||||
// ── Noise Control ────────────────────────────
|
||||
val ancMode = device.ancMode
|
||||
val adaptiveNoise = device.adaptiveAudioNoise
|
||||
if (features.hasAncControl && ancMode != null) {
|
||||
val cycleMask = device.resolvedAncCycleMask
|
||||
val cycleSummary = if (cycleMask != null) listeningModeCycleSummary(context, ancMode.supported, cycleMask) else null
|
||||
val cycleSubtitle = if (cycleSummary != null) {
|
||||
buildString {
|
||||
append(cycleSummary)
|
||||
append('\n')
|
||||
append(
|
||||
context.getString(
|
||||
if (state.hasCustomLongPressStemAction) {
|
||||
R.string.device_settings_listening_mode_cycle_summary_helper_override
|
||||
} else {
|
||||
R.string.device_settings_listening_mode_cycle_summary_helper
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (features.hasAncControl && device.ancMode != null) {
|
||||
item("noise_control_section") {
|
||||
SettingsSection(title = stringResource(R.string.device_settings_noise_control_label)) {
|
||||
NoiseControlCurrentModeControl(
|
||||
currentMode = ancMode.current,
|
||||
pendingMode = device.pendingAncMode,
|
||||
supportedModes = device.visibleAncModes,
|
||||
onModeSelected = onAncModeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
val ncWithOneAirPod = device.ncWithOneAirPod
|
||||
val showNcWithOneAirPod = features.hasNcOneAirpod && ncWithOneAirPod != null
|
||||
val hasNoiseExtras = (features.hasAdaptiveAudioNoise && adaptiveNoise != null) ||
|
||||
features.hasListeningModeCycle ||
|
||||
showNcWithOneAirPod
|
||||
if (hasNoiseExtras) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
if (showNcWithOneAirPod) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.LooksOne,
|
||||
title = stringResource(R.string.device_settings_nc_one_airpod_label),
|
||||
subtitle = stringResource(R.string.device_settings_nc_one_airpod_description),
|
||||
checked = ncWithOneAirPod!!.enabled,
|
||||
onCheckedChange = onNcWithOneAirPodChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (features.hasListeningModeCycle && cycleMask != null && cycleSubtitle != null) {
|
||||
SettingsPreferenceItem(
|
||||
icon = Icons.TwoTone.Loop,
|
||||
title = stringResource(R.string.device_settings_listening_mode_cycle_label),
|
||||
subtitle = cycleSubtitle,
|
||||
value = stringResource(
|
||||
if (isPro) {
|
||||
R.string.general_edit_action
|
||||
} else {
|
||||
R.string.general_upgrade_action
|
||||
},
|
||||
),
|
||||
onClick = {
|
||||
if (isPro) {
|
||||
onShowListeningModeCycleDialogChange(true)
|
||||
} else {
|
||||
onUpgrade()
|
||||
}
|
||||
},
|
||||
enabled = if (isPro) enabled else true,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
if (state.hasCustomLongPressStemAction) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.stem_actions_long_press_anc_cycle_info),
|
||||
type = InfoBoxType.INFO,
|
||||
action = {
|
||||
TextButton(onClick = onStemActionsClick) {
|
||||
Text(stringResource(R.string.device_settings_noise_control_open_stem_actions_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features.hasAdaptiveAudioNoise && adaptiveNoise != null) {
|
||||
AdaptiveNoiseSlider(
|
||||
level = adaptiveNoise.level,
|
||||
onLevelChange = onAdaptiveAudioNoiseChange,
|
||||
enabled = enabled,
|
||||
isAdaptiveMode = ancMode.current == AapSetting.AncMode.Value.ADAPTIVE
|
||||
|| device.pendingAncMode == AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
}
|
||||
}
|
||||
NoiseControlCard(
|
||||
device = device,
|
||||
features = features,
|
||||
isPro = isPro,
|
||||
enabled = enabled,
|
||||
hasCustomLongPressStemAction = state.hasCustomLongPressStemAction,
|
||||
showListeningModeCycleDialog = showListeningModeCycleDialog,
|
||||
onShowListeningModeCycleDialogChange = onShowListeningModeCycleDialogChange,
|
||||
onAncModeChange = onAncModeChange,
|
||||
onNcWithOneAirPodChange = onNcWithOneAirPodChange,
|
||||
onAdaptiveAudioNoiseChange = onAdaptiveAudioNoiseChange,
|
||||
onAllowOffOptionChange = onAllowOffOptionChange,
|
||||
onListeningModeCycleChange = onListeningModeCycleChange,
|
||||
onStemActionsClick = onStemActionsClick,
|
||||
onUpgrade = onUpgrade,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,141 +374,41 @@ fun DeviceSettingsScreen(
|
||||
features.hasMicrophoneMode
|
||||
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 (personalizedVol.enabled) {
|
||||
SettingsInfoBox(
|
||||
title = stringResource(R.string.device_settings_experimental_title),
|
||||
text = stringResource(R.string.device_settings_experimental_description),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onOpenIssueTracker) {
|
||||
Text(stringResource(R.string.device_settings_experimental_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features.hasToneVolume && toneVol != null) {
|
||||
if (isPro) {
|
||||
ToneVolumeSlider(
|
||||
level = toneVol.level,
|
||||
onLevelChange = onToneVolumeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
} else {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.AutoMirrored.TwoTone.VolumeUp,
|
||||
title = stringResource(R.string.device_settings_tone_volume_label),
|
||||
subtitle = stringResource(R.string.device_settings_tone_volume_description),
|
||||
onClick = onUpgrade,
|
||||
requiresUpgrade = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features.hasMicrophoneMode) {
|
||||
if (isPro) {
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.TwoTone.Mic,
|
||||
title = stringResource(R.string.device_settings_microphone_mode_label),
|
||||
subtitle = stringResource(R.string.device_settings_microphone_mode_description),
|
||||
onClick = onUpgrade,
|
||||
requiresUpgrade = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SoundCard(
|
||||
device = device,
|
||||
features = features,
|
||||
isPro = isPro,
|
||||
enabled = enabled,
|
||||
onPersonalizedVolumeChange = onPersonalizedVolumeChange,
|
||||
onToneVolumeChange = onToneVolumeChange,
|
||||
onMicrophoneModeChange = onMicrophoneModeChange,
|
||||
onUpgrade = onUpgrade,
|
||||
onOpenIssueTracker = onOpenIssueTracker,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Controls ─────────────────────────────────
|
||||
val pressSpd = device.pressSpeed
|
||||
val pressHold = device.pressHoldDuration
|
||||
val volSwipe = device.volumeSwipe
|
||||
val volSwipeLen = device.volumeSwipeLength
|
||||
val endCallMuteMic = device.endCallMuteMic
|
||||
val showControlsSection = features.hasStemConfig ||
|
||||
(features.hasEndCallMuteMic && endCallMuteMic != null) ||
|
||||
(features.hasPressSpeed && pressSpd != null) ||
|
||||
(features.hasPressHoldDuration && pressHold != null) ||
|
||||
(features.hasVolumeSwipe && volSwipe != null) ||
|
||||
(features.hasVolumeSwipeLength && volSwipeLen != null)
|
||||
(features.hasEndCallMuteMic && device.endCallMuteMic != null) ||
|
||||
(features.hasPressSpeed && device.pressSpeed != null) ||
|
||||
(features.hasPressHoldDuration && device.pressHoldDuration != null) ||
|
||||
(features.hasVolumeSwipe && device.volumeSwipe != null) ||
|
||||
(features.hasVolumeSwipeLength && device.volumeSwipeLength != 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) {
|
||||
CallControlSettings(
|
||||
current = endCallMuteMic,
|
||||
onChange = onEndCallMuteMicChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (features.hasPressSpeed && pressSpd != null) {
|
||||
PressSpeedSetting(
|
||||
selected = pressSpd.value,
|
||||
onSelected = onPressSpeedChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (features.hasPressHoldDuration && pressHold != null) {
|
||||
PressHoldDurationSetting(
|
||||
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) {
|
||||
VolumeSwipeLengthSetting(
|
||||
selected = volSwipeLen.value,
|
||||
onSelected = onVolumeSwipeLengthChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
ControlsCard(
|
||||
device = device,
|
||||
features = features,
|
||||
isPro = isPro,
|
||||
enabled = enabled,
|
||||
onStemActionsClick = onStemActionsClick,
|
||||
onEndCallMuteMicChange = onEndCallMuteMicChange,
|
||||
onPressSpeedChange = onPressSpeedChange,
|
||||
onPressHoldDurationChange = onPressHoldDurationChange,
|
||||
onVolumeSwipeChange = onVolumeSwipeChange,
|
||||
onVolumeSwipeLengthChange = onVolumeSwipeLengthChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,334 +443,10 @@ fun DeviceSettingsScreen(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
showListeningModeCycleDialog &&
|
||||
device != null &&
|
||||
features?.hasListeningModeCycle == true &&
|
||||
device.isAapConnected
|
||||
) {
|
||||
val ancMode = device.ancMode
|
||||
val currentCycleMask = (device.listeningModeCycle ?: AapSetting.ListeningModeCycle(modeMask = 0x0E)).modeMask
|
||||
if (ancMode != null) {
|
||||
ListeningModeCycleDialog(
|
||||
supportedModes = ancMode.supported,
|
||||
currentCycleMask = currentCycleMask,
|
||||
onSave = { newMask ->
|
||||
val supportsOff = ancMode.supported.contains(AapSetting.AncMode.Value.OFF)
|
||||
if (supportsOff) {
|
||||
val offBit = cycleBit(AapSetting.AncMode.Value.OFF)
|
||||
onOffVisibilityChange((newMask and offBit) != 0, newMask and offBit.inv())
|
||||
} else {
|
||||
onListeningModeCycleChange(newMask)
|
||||
}
|
||||
},
|
||||
onDismiss = { onShowListeningModeCycleDialogChange(false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showAutoConnectConditionDialog && device != null) {
|
||||
AutoConnectConditionDialog(
|
||||
current = reactions.autoConnectCondition,
|
||||
hasEarDetection = features?.hasEarDetection == true,
|
||||
hasCase = features?.hasCase == true,
|
||||
onSelect = {
|
||||
onAutoConnectConditionChange(it)
|
||||
showAutoConnectConditionDialog = false
|
||||
},
|
||||
onDismiss = { showAutoConnectConditionDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReactionsDivider() {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoiseControlCurrentModeControl(
|
||||
currentMode: AapSetting.AncMode.Value,
|
||||
pendingMode: AapSetting.AncMode.Value?,
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
onModeSelected: (AapSetting.AncMode.Value) -> Unit,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
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.Headphones,
|
||||
title = stringResource(R.string.device_settings_noise_control_current_mode_label),
|
||||
subtitle = null,
|
||||
enabled = enabled,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
AncModeSelector(
|
||||
currentMode = currentMode,
|
||||
supportedModes = supportedModes,
|
||||
onModeSelected = onModeSelected,
|
||||
pendingMode = pendingMode,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListeningModeCycleDialog(
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
currentCycleMask: Int,
|
||||
onSave: (Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val supportedMask = supportedModes.fold(0) { mask, mode -> mask or cycleBit(mode) }
|
||||
var draftMask by remember(currentCycleMask, supportedMask) {
|
||||
mutableIntStateOf(currentCycleMask and supportedMask)
|
||||
}
|
||||
val selectedCount = Integer.bitCount(draftMask and supportedMask)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.device_settings_listening_mode_cycle_dialog_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.device_settings_listening_mode_cycle_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Column(modifier = Modifier.selectableGroup()) {
|
||||
supportedModes.forEach { mode ->
|
||||
val bit = cycleBit(mode)
|
||||
val isSelected = (draftMask and bit) != 0
|
||||
val canToggle = !isSelected || selectedCount > 2
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
enabled = canToggle,
|
||||
role = Role.Checkbox,
|
||||
onClick = {
|
||||
draftMask = if (isSelected) {
|
||||
draftMask and bit.inv()
|
||||
} else {
|
||||
draftMask or bit
|
||||
}
|
||||
},
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null,
|
||||
enabled = canToggle,
|
||||
)
|
||||
Icon(
|
||||
imageVector = mode.icon(),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = if (canToggle) 1f else 0.5f),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
Text(
|
||||
text = mode.listeningModeCycleDialogLabel(context),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (canToggle) 1f else 0.5f),
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.device_settings_listening_mode_cycle_minimum),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSave(draftMask)
|
||||
onDismiss()
|
||||
},
|
||||
enabled = selectedCount >= 2,
|
||||
) {
|
||||
Text(text = stringResource(R.string.general_save_action))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(R.string.general_cancel_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToneVolumeSlider(
|
||||
level: Int,
|
||||
onLevelChange: (Int) -> Unit,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
var sliderValue by remember(level) { mutableIntStateOf(level) }
|
||||
SettingsSliderItem(
|
||||
icon = Icons.AutoMirrored.TwoTone.VolumeUp,
|
||||
title = stringResource(R.string.device_settings_tone_volume_label),
|
||||
subtitle = stringResource(R.string.device_settings_tone_volume_description),
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { sliderValue = it.toInt() },
|
||||
onValueChangeFinished = { onLevelChange(sliderValue) },
|
||||
valueRange = 15f..100f,
|
||||
steps = 84,
|
||||
enabled = enabled,
|
||||
valueLabel = { "${it.toInt()}%" },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdaptiveNoiseSlider(
|
||||
level: Int,
|
||||
onLevelChange: (Int) -> Unit,
|
||||
enabled: Boolean,
|
||||
isAdaptiveMode: Boolean,
|
||||
) {
|
||||
var sliderValue by remember(level) { mutableIntStateOf(level) }
|
||||
val subtitleRes = if (isAdaptiveMode) {
|
||||
R.string.device_settings_adaptive_noise_description
|
||||
} else {
|
||||
R.string.device_settings_adaptive_noise_requires_adaptive
|
||||
}
|
||||
SettingsSliderItem(
|
||||
icon = Icons.TwoTone.GraphicEq,
|
||||
title = stringResource(R.string.device_settings_adaptive_noise_label),
|
||||
subtitle = stringResource(subtitleRes),
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { sliderValue = it.toInt() },
|
||||
onValueChangeFinished = { onLevelChange(sliderValue) },
|
||||
valueRange = 0f..100f,
|
||||
steps = 99,
|
||||
enabled = enabled && isAdaptiveMode,
|
||||
valueLabel = { "${it.toInt()}%" },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SettingsCompoundHeader(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 0.6f else 0.3f),
|
||||
modifier = Modifier.padding(end = 16.dp, top = 4.dp),
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f),
|
||||
)
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = if (enabled) 1f else 0.5f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T> SegmentedSettingRow(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
options: List<Pair<String, T>>,
|
||||
selected: T,
|
||||
onSelected: (T) -> Unit,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: String? = null,
|
||||
) {
|
||||
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 = icon,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
enabled = enabled,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
options.forEachIndexed { index, (label, value) ->
|
||||
SegmentedButton(
|
||||
selected = value == selected,
|
||||
onClick = { if (enabled) onSelected(value) },
|
||||
enabled = enabled,
|
||||
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cycleBit(mode: AapSetting.AncMode.Value): Int = when (mode) {
|
||||
AapSetting.AncMode.Value.OFF -> 0x01
|
||||
AapSetting.AncMode.Value.ON -> 0x02
|
||||
AapSetting.AncMode.Value.TRANSPARENCY -> 0x04
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> 0x08
|
||||
}
|
||||
|
||||
private fun listeningModeCycleSummary(
|
||||
context: android.content.Context,
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
cycleMask: Int,
|
||||
): String = supportedModes
|
||||
.filter { mode -> (cycleMask and cycleBit(mode)) != 0 }
|
||||
.joinToString(separator = " • ") { it.shortLabel(context) }
|
||||
|
||||
private fun AapSetting.AncMode.Value.listeningModeCycleDialogLabel(
|
||||
context: android.content.Context,
|
||||
): String = when (this) {
|
||||
AapSetting.AncMode.Value.OFF -> context.getString(R.string.device_settings_allow_off_label)
|
||||
AapSetting.AncMode.Value.ON -> context.getString(R.string.device_settings_listening_mode_cycle_anc)
|
||||
AapSetting.AncMode.Value.TRANSPARENCY -> context.getString(R.string.device_settings_listening_mode_cycle_transparency)
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> context.getString(R.string.device_settings_listening_mode_cycle_adaptive)
|
||||
}
|
||||
|
||||
private fun previewFullState(isPro: Boolean) = DeviceSettingsViewModel.State(
|
||||
internal fun previewFullState(isPro: Boolean) = DeviceSettingsViewModel.State(
|
||||
device = PodDevice(
|
||||
profileId = "preview",
|
||||
label = "My AirPods Pro",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.cards
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.Swipe
|
||||
import androidx.compose.material.icons.twotone.TouchApp
|
||||
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.common.settings.SettingsPreferenceItem
|
||||
import eu.darken.capod.common.settings.SettingsSection
|
||||
import eu.darken.capod.common.settings.SettingsSwitchItem
|
||||
import eu.darken.capod.main.ui.devicesettings.components.CallControlSettings
|
||||
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.devicesettings.previewFullState
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
|
||||
@Composable
|
||||
internal fun ControlsCard(
|
||||
device: PodDevice,
|
||||
features: PodModel.Features,
|
||||
isPro: Boolean,
|
||||
enabled: Boolean,
|
||||
onStemActionsClick: () -> Unit = {},
|
||||
onEndCallMuteMicChange: (AapSetting.EndCallMuteMic.MuteMicMode, AapSetting.EndCallMuteMic.EndCallMode) -> Unit = { _, _ -> },
|
||||
onPressSpeedChange: (AapSetting.PressSpeed.Value) -> Unit = {},
|
||||
onPressHoldDurationChange: (AapSetting.PressHoldDuration.Value) -> Unit = {},
|
||||
onVolumeSwipeChange: (Boolean) -> Unit = {},
|
||||
onVolumeSwipeLengthChange: (AapSetting.VolumeSwipeLength.Value) -> Unit = {},
|
||||
) {
|
||||
val pressSpd = device.pressSpeed
|
||||
val pressHold = device.pressHoldDuration
|
||||
val volSwipe = device.volumeSwipe
|
||||
val volSwipeLen = device.volumeSwipeLength
|
||||
val endCallMuteMic = device.endCallMuteMic
|
||||
|
||||
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) {
|
||||
CallControlSettings(
|
||||
current = endCallMuteMic,
|
||||
onChange = onEndCallMuteMicChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (features.hasPressSpeed && pressSpd != null) {
|
||||
PressSpeedSetting(
|
||||
selected = pressSpd.value,
|
||||
onSelected = onPressSpeedChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (features.hasPressHoldDuration && pressHold != null) {
|
||||
PressHoldDurationSetting(
|
||||
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) {
|
||||
VolumeSwipeLengthSetting(
|
||||
selected = volSwipeLen.value,
|
||||
onSelected = onVolumeSwipeLengthChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ControlsCardPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = true)
|
||||
val device = state.device!!
|
||||
ControlsCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
enabled = device.isAapReady,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ControlsCardNonProPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = false)
|
||||
val device = state.device!!
|
||||
ControlsCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
enabled = device.isAapReady,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.cards
|
||||
|
||||
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.DoNotDisturbOn
|
||||
import androidx.compose.material.icons.twotone.GraphicEq
|
||||
import androidx.compose.material.icons.twotone.Headphones
|
||||
import androidx.compose.material.icons.twotone.LooksOne
|
||||
import androidx.compose.material.icons.twotone.Loop
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.common.settings.InfoBoxType
|
||||
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.main.ui.components.icon
|
||||
import eu.darken.capod.main.ui.components.shortLabel
|
||||
import eu.darken.capod.main.ui.devicesettings.components.SettingsCompoundHeader
|
||||
import eu.darken.capod.main.ui.devicesettings.previewFullState
|
||||
import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.resolvedAncCycleMask
|
||||
import eu.darken.capod.monitor.core.visibleAncModes
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
|
||||
@Composable
|
||||
internal fun NoiseControlCard(
|
||||
device: PodDevice,
|
||||
features: PodModel.Features,
|
||||
isPro: Boolean,
|
||||
enabled: Boolean,
|
||||
hasCustomLongPressStemAction: Boolean = false,
|
||||
showListeningModeCycleDialog: Boolean = false,
|
||||
onShowListeningModeCycleDialogChange: (Boolean) -> Unit = {},
|
||||
onAncModeChange: (AapSetting.AncMode.Value) -> Unit = {},
|
||||
onNcWithOneAirPodChange: (Boolean) -> Unit = {},
|
||||
onAdaptiveAudioNoiseChange: (Int) -> Unit = {},
|
||||
onAllowOffOptionChange: (Boolean) -> Unit = {},
|
||||
onListeningModeCycleChange: (Int) -> Unit = {},
|
||||
onStemActionsClick: () -> Unit = {},
|
||||
onUpgrade: () -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val ancMode = device.ancMode ?: return
|
||||
val adaptiveNoise = device.adaptiveAudioNoise
|
||||
val cycleMask = device.resolvedAncCycleMask
|
||||
val cycleSummary = if (cycleMask != null) listeningModeCycleSummary(context, ancMode.supported, cycleMask) else null
|
||||
val cycleSubtitle = if (cycleSummary != null) {
|
||||
buildString {
|
||||
append(cycleSummary)
|
||||
append('\n')
|
||||
append(
|
||||
context.getString(
|
||||
if (hasCustomLongPressStemAction) {
|
||||
R.string.device_settings_listening_mode_cycle_summary_helper_override
|
||||
} else {
|
||||
R.string.device_settings_listening_mode_cycle_summary_helper
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
SettingsSection(title = stringResource(R.string.device_settings_noise_control_label)) {
|
||||
NoiseControlCurrentModeControl(
|
||||
currentMode = ancMode.current,
|
||||
pendingMode = device.pendingAncMode,
|
||||
supportedModes = device.visibleAncModes,
|
||||
onModeSelected = onAncModeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
val ncWithOneAirPod = device.ncWithOneAirPod
|
||||
val showNcWithOneAirPod = features.hasNcOneAirpod && ncWithOneAirPod != null
|
||||
val hasNoiseExtras = (features.hasAdaptiveAudioNoise && adaptiveNoise != null) ||
|
||||
features.hasListeningModeCycle ||
|
||||
showNcWithOneAirPod
|
||||
if (hasNoiseExtras) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
if (features.hasListeningModeCycle && cycleMask != null && cycleSubtitle != null) {
|
||||
SettingsPreferenceItem(
|
||||
icon = Icons.TwoTone.Loop,
|
||||
title = stringResource(R.string.device_settings_listening_mode_cycle_label),
|
||||
subtitle = cycleSubtitle,
|
||||
value = stringResource(
|
||||
if (isPro) {
|
||||
R.string.general_edit_action
|
||||
} else {
|
||||
R.string.general_upgrade_action
|
||||
},
|
||||
),
|
||||
onClick = {
|
||||
if (isPro) {
|
||||
onShowListeningModeCycleDialogChange(true)
|
||||
} else {
|
||||
onUpgrade()
|
||||
}
|
||||
},
|
||||
enabled = if (isPro) enabled else true,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
if (hasCustomLongPressStemAction) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.stem_actions_long_press_anc_cycle_info),
|
||||
type = InfoBoxType.INFO,
|
||||
action = {
|
||||
TextButton(onClick = onStemActionsClick) {
|
||||
Text(stringResource(R.string.device_settings_noise_control_open_stem_actions_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features.hasAllowOffOption) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.DoNotDisturbOn,
|
||||
title = stringResource(R.string.device_settings_allow_off_label),
|
||||
subtitle = stringResource(R.string.device_settings_allow_off_description),
|
||||
checked = device.allowOffOption?.enabled != false,
|
||||
onCheckedChange = { onAllowOffOptionChange(it) },
|
||||
enabled = if (isPro) enabled else true,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
if (showNcWithOneAirPod) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.LooksOne,
|
||||
title = stringResource(R.string.device_settings_nc_one_airpod_label),
|
||||
subtitle = stringResource(R.string.device_settings_nc_one_airpod_description),
|
||||
checked = ncWithOneAirPod.enabled,
|
||||
onCheckedChange = onNcWithOneAirPodChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
if (features.hasAdaptiveAudioNoise && adaptiveNoise != null) {
|
||||
AdaptiveNoiseSlider(
|
||||
level = adaptiveNoise.level,
|
||||
onLevelChange = onAdaptiveAudioNoiseChange,
|
||||
enabled = enabled,
|
||||
isAdaptiveMode = ancMode.current == AapSetting.AncMode.Value.ADAPTIVE
|
||||
|| device.pendingAncMode == AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showListeningModeCycleDialog && features.hasListeningModeCycle && device.isAapConnected) {
|
||||
val currentCycleMask = (device.listeningModeCycle ?: AapSetting.ListeningModeCycle(modeMask = 0x0E)).modeMask
|
||||
// When Allow Off is confirmed disabled, strip OFF from the dialog entirely so
|
||||
// the user can't (re-)introduce it invisibly via a stale cycle-mask bit.
|
||||
val allowOffEnabled = device.allowOffOption?.enabled != false
|
||||
val dialogSupportedModes = if (allowOffEnabled) {
|
||||
ancMode.supported
|
||||
} else {
|
||||
ancMode.supported - AapSetting.AncMode.Value.OFF
|
||||
}
|
||||
ListeningModeCycleDialog(
|
||||
supportedModes = dialogSupportedModes,
|
||||
currentCycleMask = currentCycleMask,
|
||||
onSave = { newMask -> onListeningModeCycleChange(newMask) },
|
||||
onDismiss = { onShowListeningModeCycleDialogChange(false) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoiseControlCurrentModeControl(
|
||||
currentMode: AapSetting.AncMode.Value,
|
||||
pendingMode: AapSetting.AncMode.Value?,
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
onModeSelected: (AapSetting.AncMode.Value) -> Unit,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
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.Headphones,
|
||||
title = stringResource(R.string.device_settings_noise_control_current_mode_label),
|
||||
subtitle = null,
|
||||
enabled = enabled,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
AncModeSelector(
|
||||
currentMode = currentMode,
|
||||
supportedModes = supportedModes,
|
||||
onModeSelected = onModeSelected,
|
||||
pendingMode = pendingMode,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdaptiveNoiseSlider(
|
||||
level: Int,
|
||||
onLevelChange: (Int) -> Unit,
|
||||
enabled: Boolean,
|
||||
isAdaptiveMode: Boolean,
|
||||
) {
|
||||
var sliderValue by remember(level) { mutableIntStateOf(level) }
|
||||
val subtitleRes = if (isAdaptiveMode) {
|
||||
R.string.device_settings_adaptive_noise_description
|
||||
} else {
|
||||
R.string.device_settings_adaptive_noise_requires_adaptive
|
||||
}
|
||||
SettingsSliderItem(
|
||||
icon = Icons.TwoTone.GraphicEq,
|
||||
title = stringResource(R.string.device_settings_adaptive_noise_label),
|
||||
subtitle = stringResource(subtitleRes),
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { sliderValue = it.toInt() },
|
||||
onValueChangeFinished = { onLevelChange(sliderValue) },
|
||||
valueRange = 0f..100f,
|
||||
steps = 99,
|
||||
enabled = enabled && isAdaptiveMode,
|
||||
valueLabel = { "${it.toInt()}%" },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListeningModeCycleDialog(
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
currentCycleMask: Int,
|
||||
onSave: (Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val supportedMask = supportedModes.fold(0) { mask, mode -> mask or cycleBit(mode) }
|
||||
var draftMask by remember(currentCycleMask, supportedMask) {
|
||||
mutableIntStateOf(currentCycleMask and supportedMask)
|
||||
}
|
||||
val selectedCount = Integer.bitCount(draftMask and supportedMask)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.device_settings_listening_mode_cycle_dialog_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.device_settings_listening_mode_cycle_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Column(modifier = Modifier.selectableGroup()) {
|
||||
supportedModes.forEach { mode ->
|
||||
val bit = cycleBit(mode)
|
||||
val isSelected = (draftMask and bit) != 0
|
||||
val canToggle = !isSelected || selectedCount > 2
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
enabled = canToggle,
|
||||
role = Role.Checkbox,
|
||||
onClick = {
|
||||
draftMask = if (isSelected) {
|
||||
draftMask and bit.inv()
|
||||
} else {
|
||||
draftMask or bit
|
||||
}
|
||||
},
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null,
|
||||
enabled = canToggle,
|
||||
)
|
||||
Icon(
|
||||
imageVector = mode.icon(),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = if (canToggle) 1f else 0.5f),
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
Text(
|
||||
text = mode.listeningModeCycleDialogLabel(context),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (canToggle) 1f else 0.5f),
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.device_settings_listening_mode_cycle_minimum),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSave(draftMask)
|
||||
onDismiss()
|
||||
},
|
||||
enabled = selectedCount >= 2,
|
||||
) {
|
||||
Text(text = stringResource(R.string.general_save_action))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(R.string.general_cancel_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun cycleBit(mode: AapSetting.AncMode.Value): Int = when (mode) {
|
||||
AapSetting.AncMode.Value.OFF -> 0x01
|
||||
AapSetting.AncMode.Value.ON -> 0x02
|
||||
AapSetting.AncMode.Value.TRANSPARENCY -> 0x04
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> 0x08
|
||||
}
|
||||
|
||||
private fun listeningModeCycleSummary(
|
||||
context: android.content.Context,
|
||||
supportedModes: List<AapSetting.AncMode.Value>,
|
||||
cycleMask: Int,
|
||||
): String = supportedModes
|
||||
.filter { mode -> (cycleMask and cycleBit(mode)) != 0 }
|
||||
.joinToString(separator = " • ") { it.shortLabel(context) }
|
||||
|
||||
private fun AapSetting.AncMode.Value.listeningModeCycleDialogLabel(
|
||||
context: android.content.Context,
|
||||
): String = when (this) {
|
||||
AapSetting.AncMode.Value.OFF -> context.getString(R.string.device_settings_listening_mode_cycle_off)
|
||||
AapSetting.AncMode.Value.ON -> context.getString(R.string.device_settings_listening_mode_cycle_anc)
|
||||
AapSetting.AncMode.Value.TRANSPARENCY -> context.getString(R.string.device_settings_listening_mode_cycle_transparency)
|
||||
AapSetting.AncMode.Value.ADAPTIVE -> context.getString(R.string.device_settings_listening_mode_cycle_adaptive)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun NoiseControlCardPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = true)
|
||||
val device = state.device!!
|
||||
NoiseControlCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
enabled = device.isAapReady,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun NoiseControlCardNonProPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = false)
|
||||
val device = state.device!!
|
||||
NoiseControlCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
enabled = device.isAapReady,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.cards
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.twotone.BluetoothConnected
|
||||
import androidx.compose.material.icons.twotone.Hearing
|
||||
import androidx.compose.material.icons.twotone.LooksOne
|
||||
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.Workspaces
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.common.settings.InfoBoxType
|
||||
import eu.darken.capod.common.settings.SettingsBaseItem
|
||||
import eu.darken.capod.common.settings.SettingsInfoBox
|
||||
import eu.darken.capod.common.settings.SettingsSection
|
||||
import eu.darken.capod.common.settings.SettingsSwitchItem
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import eu.darken.capod.main.ui.devicesettings.dialogs.AutoConnectConditionDialog
|
||||
import eu.darken.capod.main.ui.devicesettings.previewFullState
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
|
||||
|
||||
@Composable
|
||||
internal fun ReactionsCard(
|
||||
device: PodDevice,
|
||||
features: PodModel.Features,
|
||||
isPro: Boolean,
|
||||
monitorMode: MonitorMode,
|
||||
onAutoPlayChange: (Boolean) -> Unit = {},
|
||||
onAutoPauseChange: (Boolean) -> Unit = {},
|
||||
onOnePodModeChange: (Boolean) -> Unit = {},
|
||||
onConversationalAwarenessChange: (Boolean) -> Unit = {},
|
||||
onSleepDetectionChange: (Boolean) -> Unit = {},
|
||||
onAutoConnectChange: (Boolean) -> Unit = {},
|
||||
onAutoConnectConditionChange: (AutoConnectCondition) -> Unit = {},
|
||||
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
||||
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
||||
onFixMonitorMode: () -> Unit = {},
|
||||
onOpenIssueTracker: () -> Unit = {},
|
||||
) {
|
||||
val reactions = device.reactions
|
||||
val enabled = device.isAapReady
|
||||
|
||||
var showAutoConnectConditionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
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 onePodModeActive = reactions.onePodMode ||
|
||||
reactions.autoPlay ||
|
||||
reactions.autoPause ||
|
||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
|
||||
SettingsBaseItem(
|
||||
title = stringResource(R.string.settings_onepod_mode_label),
|
||||
subtitle = stringResource(R.string.settings_onepod_mode_description),
|
||||
icon = Icons.TwoTone.LooksOne,
|
||||
onClick = { if (onePodModeActive) onOnePodModeChange(!reactions.onePodMode) },
|
||||
enabled = onePodModeActive,
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = reactions.onePodMode,
|
||||
onCheckedChange = onOnePodModeChange,
|
||||
enabled = onePodModeActive,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
val earDetectionWarningVisible =
|
||||
(
|
||||
reactions.autoPlay ||
|
||||
reactions.autoPause ||
|
||||
(reactions.autoConnect && reactions.autoConnectCondition == AutoConnectCondition.IN_EAR)
|
||||
) && !device.isAapConnected
|
||||
if (earDetectionWarningVisible) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_eardetection_info_description),
|
||||
)
|
||||
}
|
||||
ReactionsDivider()
|
||||
}
|
||||
if (device.isAapConnected) {
|
||||
val convAwareness = device.conversationalAwareness
|
||||
val hasAnyAapReaction =
|
||||
(features.hasConversationAwareness && convAwareness != null) ||
|
||||
features.hasSleepDetection
|
||||
if (features.hasConversationAwareness && convAwareness != null) {
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.Hearing,
|
||||
title = stringResource(R.string.conversation_awareness_label),
|
||||
subtitle = stringResource(R.string.device_settings_conversation_awareness_description),
|
||||
checked = convAwareness.enabled,
|
||||
onCheckedChange = onConversationalAwarenessChange,
|
||||
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,
|
||||
)
|
||||
if (sleepDet.enabled) {
|
||||
SettingsInfoBox(
|
||||
title = stringResource(R.string.device_settings_experimental_title),
|
||||
text = stringResource(R.string.device_settings_experimental_description),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onOpenIssueTracker) {
|
||||
Text(stringResource(R.string.device_settings_experimental_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasAnyAapReaction) {
|
||||
ReactionsDivider()
|
||||
}
|
||||
}
|
||||
SettingsSwitchItem(
|
||||
icon = Icons.TwoTone.BluetoothConnected,
|
||||
title = stringResource(R.string.settings_autoconnect_label),
|
||||
subtitle = stringResource(R.string.settings_autoconnect_description),
|
||||
checked = reactions.autoConnect,
|
||||
onCheckedChange = onAutoConnectChange,
|
||||
)
|
||||
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 (reactions.autoConnect && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_autoconnect_info_android12),
|
||||
)
|
||||
}
|
||||
ReactionsDivider()
|
||||
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,
|
||||
)
|
||||
}
|
||||
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,
|
||||
)
|
||||
val anyPopupEnabled = reactions.showPopUpOnCaseOpen || reactions.showPopUpOnConnection
|
||||
if (anyPopupEnabled && monitorMode == MonitorMode.MANUAL) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_popup_warning_manual_mode),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onFixMonitorMode) {
|
||||
Text(stringResource(R.string.general_fix_it_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (anyPopupEnabled) {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.settings_popup_info_not_in_app),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showAutoConnectConditionDialog) {
|
||||
AutoConnectConditionDialog(
|
||||
current = reactions.autoConnectCondition,
|
||||
hasEarDetection = features.hasEarDetection,
|
||||
hasCase = features.hasCase,
|
||||
onSelect = {
|
||||
onAutoConnectConditionChange(it)
|
||||
showAutoConnectConditionDialog = false
|
||||
},
|
||||
onDismiss = { showAutoConnectConditionDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReactionsDivider() {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ReactionsCardPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = true)
|
||||
val device = state.device!!
|
||||
ReactionsCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
monitorMode = state.monitorMode,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ReactionsCardNonProPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = false)
|
||||
val device = state.device!!
|
||||
ReactionsCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
monitorMode = state.monitorMode,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.cards
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.VolumeUp
|
||||
import androidx.compose.material.icons.twotone.Mic
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.common.settings.InfoBoxType
|
||||
import eu.darken.capod.common.settings.SettingsBaseItem
|
||||
import eu.darken.capod.common.settings.SettingsInfoBox
|
||||
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.main.ui.devicesettings.components.SegmentedSettingRow
|
||||
import eu.darken.capod.main.ui.devicesettings.previewFullState
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
|
||||
@Composable
|
||||
internal fun SoundCard(
|
||||
device: PodDevice,
|
||||
features: PodModel.Features,
|
||||
isPro: Boolean,
|
||||
enabled: Boolean,
|
||||
onPersonalizedVolumeChange: (Boolean) -> Unit = {},
|
||||
onToneVolumeChange: (Int) -> Unit = {},
|
||||
onMicrophoneModeChange: (AapSetting.MicrophoneMode.Mode) -> Unit = {},
|
||||
onUpgrade: () -> Unit = {},
|
||||
onOpenIssueTracker: () -> Unit = {},
|
||||
) {
|
||||
val personalizedVol = device.personalizedVolume
|
||||
val toneVol = device.toneVolume
|
||||
|
||||
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 (personalizedVol.enabled) {
|
||||
SettingsInfoBox(
|
||||
title = stringResource(R.string.device_settings_experimental_title),
|
||||
text = stringResource(R.string.device_settings_experimental_description),
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = onOpenIssueTracker) {
|
||||
Text(stringResource(R.string.device_settings_experimental_action))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features.hasToneVolume && toneVol != null) {
|
||||
if (isPro) {
|
||||
ToneVolumeSlider(
|
||||
level = toneVol.level,
|
||||
onLevelChange = onToneVolumeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
} else {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.AutoMirrored.TwoTone.VolumeUp,
|
||||
title = stringResource(R.string.device_settings_tone_volume_label),
|
||||
subtitle = stringResource(R.string.device_settings_tone_volume_description),
|
||||
onClick = onUpgrade,
|
||||
requiresUpgrade = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features.hasMicrophoneMode) {
|
||||
if (isPro) {
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
SettingsBaseItem(
|
||||
icon = Icons.TwoTone.Mic,
|
||||
title = stringResource(R.string.device_settings_microphone_mode_label),
|
||||
subtitle = stringResource(R.string.device_settings_microphone_mode_description),
|
||||
onClick = onUpgrade,
|
||||
requiresUpgrade = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToneVolumeSlider(
|
||||
level: Int,
|
||||
onLevelChange: (Int) -> Unit,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
var sliderValue by remember(level) { mutableIntStateOf(level) }
|
||||
SettingsSliderItem(
|
||||
icon = Icons.AutoMirrored.TwoTone.VolumeUp,
|
||||
title = stringResource(R.string.device_settings_tone_volume_label),
|
||||
subtitle = stringResource(R.string.device_settings_tone_volume_description),
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { sliderValue = it.toInt() },
|
||||
onValueChangeFinished = { onLevelChange(sliderValue) },
|
||||
valueRange = 15f..100f,
|
||||
steps = 84,
|
||||
enabled = enabled,
|
||||
valueLabel = { "${it.toInt()}%" },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SoundCardPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = true)
|
||||
val device = state.device!!
|
||||
SoundCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
enabled = device.isAapReady,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SoundCardNonProPreview() = PreviewWrapper {
|
||||
val state = previewFullState(isPro = false)
|
||||
val device = state.device!!
|
||||
SoundCard(
|
||||
device = device,
|
||||
features = device.model.features,
|
||||
isPro = state.isPro,
|
||||
enabled = device.isAapReady,
|
||||
)
|
||||
}
|
||||
-1
@@ -24,7 +24,6 @@ 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
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ 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
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ 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
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package eu.darken.capod.main.ui.devicesettings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.Mic
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
|
||||
@Composable
|
||||
internal fun <T> SegmentedSettingRow(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
options: List<Pair<String, T>>,
|
||||
selected: T,
|
||||
onSelected: (T) -> Unit,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: String? = null,
|
||||
) {
|
||||
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 = icon,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
enabled = enabled,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
options.forEachIndexed { index, (label, value) ->
|
||||
SegmentedButton(
|
||||
selected = value == selected,
|
||||
onClick = { if (enabled) onSelected(value) },
|
||||
enabled = enabled,
|
||||
shape = SegmentedButtonDefaults.itemShape(index, options.size),
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SegmentedSettingRowPreview() = PreviewWrapper {
|
||||
SegmentedSettingRow(
|
||||
icon = Icons.TwoTone.Mic,
|
||||
title = "Microphone Mode",
|
||||
subtitle = "Which microphone to use during calls",
|
||||
options = listOf("Auto" to "auto", "Left" to "left", "Right" to "right"),
|
||||
selected = "auto",
|
||||
onSelected = {},
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
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.Headphones
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
|
||||
@Composable
|
||||
internal fun SettingsCompoundHeader(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 0.6f else 0.3f),
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.5f),
|
||||
)
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = if (enabled) 1f else 0.5f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SettingsCompoundHeaderPreview() = PreviewWrapper {
|
||||
SettingsCompoundHeader(
|
||||
icon = Icons.TwoTone.Headphones,
|
||||
title = "Noise Control",
|
||||
subtitle = "Switch between Off, On, Transparency, Adaptive",
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SettingsCompoundHeaderDisabledPreview() = PreviewWrapper {
|
||||
SettingsCompoundHeader(
|
||||
icon = Icons.TwoTone.Headphones,
|
||||
title = "Noise Control",
|
||||
subtitle = null,
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
-1
@@ -7,7 +7,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user