mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
feat(device-settings): Add experimental info box, revise pro-gating, fix switch bypass
Add SettingsInfoBox title support and experimental feature warning for Sleep Detection and Personalized Volume toggles with issue tracker action. Pro-gate tone volume, microphone mode. Un-gate sleep detection, conversation awareness. Fix SettingsSwitchItem allowing direct switch toggle to bypass requiresUpgrade.
This commit is contained in:
@@ -2,7 +2,9 @@ package eu.darken.capod.common.settings
|
||||
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -17,6 +19,7 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
@@ -28,6 +31,7 @@ fun SettingsInfoBox(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
type: InfoBoxType = InfoBoxType.INFO,
|
||||
title: String? = null,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
val containerColor = when (type) {
|
||||
@@ -66,6 +70,16 @@ fun SettingsInfoBox(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = if (action != null) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -110,3 +124,18 @@ private fun SettingsInfoBoxWarningWithActionPreview() = PreviewWrapper {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SettingsInfoBoxWarningWithTitlePreview() = PreviewWrapper {
|
||||
SettingsInfoBox(
|
||||
title = "Experimental feature",
|
||||
text = "This feature hasn't been tested on all AirPods devices yet. If something isn't working correctly, please open an issue on GitHub with a debug log.",
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = {}) {
|
||||
Text("Open Issue Tracker")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ fun SettingsSwitchItem(
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
onCheckedChange = if (requiresUpgrade) null else onCheckedChange,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
|
||||
@@ -175,6 +175,7 @@ fun DeviceSettingsScreenHost(
|
||||
onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) },
|
||||
onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) },
|
||||
onFixMonitorMode = { vm.setMonitorModeAutomatic() },
|
||||
onOpenIssueTracker = { vm.openIssueTracker() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -211,6 +212,7 @@ fun DeviceSettingsScreen(
|
||||
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
||||
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
||||
onFixMonitorMode: () -> Unit = {},
|
||||
onOpenIssueTracker: () -> Unit = {},
|
||||
) {
|
||||
val device = state.device
|
||||
val features = device?.model?.features
|
||||
@@ -379,8 +381,19 @@ fun DeviceSettingsScreen(
|
||||
checked = sleepDet.enabled,
|
||||
onCheckedChange = onSleepDetectionChange,
|
||||
enabled = enabled,
|
||||
requiresUpgrade = !isPro,
|
||||
)
|
||||
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()
|
||||
@@ -516,30 +529,62 @@ fun DeviceSettingsScreen(
|
||||
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) {
|
||||
ToneVolumeSlider(
|
||||
level = toneVol.level,
|
||||
onLevelChange = onToneVolumeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
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) {
|
||||
val micMode = device.microphoneMode
|
||||
?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO)
|
||||
SegmentedSettingRow(
|
||||
icon = Icons.TwoTone.Mic,
|
||||
title = stringResource(R.string.device_settings_microphone_mode_label),
|
||||
subtitle = stringResource(R.string.device_settings_microphone_mode_description),
|
||||
options = listOf(
|
||||
stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO,
|
||||
stringResource(R.string.device_settings_microphone_mode_left) to AapSetting.MicrophoneMode.Mode.ALWAYS_LEFT,
|
||||
stringResource(R.string.device_settings_microphone_mode_right) to AapSetting.MicrophoneMode.Mode.ALWAYS_RIGHT,
|
||||
),
|
||||
selected = micMode.mode,
|
||||
onSelected = onMicrophoneModeChange,
|
||||
enabled = enabled,
|
||||
)
|
||||
if (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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.darken.capod.main.ui.devicesettings
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.datastore.value
|
||||
@@ -50,6 +51,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val timeSource: TimeSource,
|
||||
private val webpageTool: WebpageTool,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
private val targetProfileId = MutableStateFlow<ProfileId?>(null)
|
||||
@@ -208,7 +210,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
|
||||
fun setPersonalizedVolume(enabled: Boolean) = send(AapCommand.SetPersonalizedVolume(enabled))
|
||||
|
||||
fun setToneVolume(level: Int) = send(AapCommand.SetToneVolume(level))
|
||||
fun setToneVolume(level: Int) = sendProGated(AapCommand.SetToneVolume(level))
|
||||
|
||||
fun setAdaptiveAudioNoise(level: Int) = send(AapCommand.SetAdaptiveAudioNoise(level))
|
||||
|
||||
@@ -225,7 +227,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
endCall: AapSetting.EndCallMuteMic.EndCallMode,
|
||||
) = send(AapCommand.SetEndCallMuteMic(muteMic, endCall))
|
||||
|
||||
fun setMicrophoneMode(mode: AapSetting.MicrophoneMode.Mode) = send(AapCommand.SetMicrophoneMode(mode))
|
||||
fun setMicrophoneMode(mode: AapSetting.MicrophoneMode.Mode) = sendProGated(AapCommand.SetMicrophoneMode(mode))
|
||||
|
||||
fun setEarDetectionEnabled(enabled: Boolean) = send(AapCommand.SetEarDetectionEnabled(enabled))
|
||||
|
||||
@@ -246,7 +248,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setSleepDetection(enabled: Boolean) = sendProGated(AapCommand.SetSleepDetection(enabled))
|
||||
fun setSleepDetection(enabled: Boolean) = send(AapCommand.SetSleepDetection(enabled))
|
||||
|
||||
fun setDeviceName(name: String) = launch {
|
||||
val address = currentAddress() ?: return@launch
|
||||
@@ -395,6 +397,10 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
|
||||
fun openIssueTracker() {
|
||||
webpageTool.open("https://github.com/d4rken-org/capod/issues")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("DeviceSettings", "VM")
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@
|
||||
<string name="settings_popup_warning_manual_mode">Popups require the monitor to be running. Your monitor mode is set to \"When app is open\", which stops the monitor when you leave the app.</string>
|
||||
<string name="general_fix_it_action">Fix it</string>
|
||||
|
||||
<string name="device_settings_experimental_title">Experimental feature</string>
|
||||
<string name="device_settings_experimental_description">This feature hasn\'t been tested on all devices yet. If something isn\'t working correctly, please open an issue with a debug log.</string>
|
||||
<string name="device_settings_experimental_action">Open Issue Tracker</string>
|
||||
|
||||
<string name="notification_channel_device_status_label">Device status</string>
|
||||
<string name="notification_channel_device_status_connected_label">Connected device</string>
|
||||
<string name="monitor_notification_extra_enabled_hint">You can disable this notification in system settings.</string>
|
||||
|
||||
+1
@@ -120,6 +120,7 @@ class DeviceSettingsViewModelTest : BaseTest() {
|
||||
profilesRepo = profilesRepo,
|
||||
generalSettings = generalSettings,
|
||||
timeSource = timeSource,
|
||||
webpageTool = mockk(relaxed = true),
|
||||
).also { vm = it }
|
||||
|
||||
private fun runVmTest(testBody: suspend TestScope.() -> Unit) = runTest(testDispatcher) {
|
||||
|
||||
Reference in New Issue
Block a user