mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -17,6 +19,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.darken.capod.common.compose.Preview2
|
import eu.darken.capod.common.compose.Preview2
|
||||||
import eu.darken.capod.common.compose.PreviewWrapper
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
@@ -28,6 +31,7 @@ fun SettingsInfoBox(
|
|||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
type: InfoBoxType = InfoBoxType.INFO,
|
type: InfoBoxType = InfoBoxType.INFO,
|
||||||
|
title: String? = null,
|
||||||
action: @Composable (() -> Unit)? = null,
|
action: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val containerColor = when (type) {
|
val containerColor = when (type) {
|
||||||
@@ -66,6 +70,16 @@ fun SettingsInfoBox(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
horizontalAlignment = if (action != null) Alignment.End else Alignment.Start,
|
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 = text,
|
text = text,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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 = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = if (requiresUpgrade) null else onCheckedChange,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ fun DeviceSettingsScreenHost(
|
|||||||
onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) },
|
onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) },
|
||||||
onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) },
|
onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) },
|
||||||
onFixMonitorMode = { vm.setMonitorModeAutomatic() },
|
onFixMonitorMode = { vm.setMonitorModeAutomatic() },
|
||||||
|
onOpenIssueTracker = { vm.openIssueTracker() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +212,7 @@ fun DeviceSettingsScreen(
|
|||||||
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
||||||
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
||||||
onFixMonitorMode: () -> Unit = {},
|
onFixMonitorMode: () -> Unit = {},
|
||||||
|
onOpenIssueTracker: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val device = state.device
|
val device = state.device
|
||||||
val features = device?.model?.features
|
val features = device?.model?.features
|
||||||
@@ -379,8 +381,19 @@ fun DeviceSettingsScreen(
|
|||||||
checked = sleepDet.enabled,
|
checked = sleepDet.enabled,
|
||||||
onCheckedChange = onSleepDetectionChange,
|
onCheckedChange = onSleepDetectionChange,
|
||||||
enabled = enabled,
|
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) {
|
if (hasAnyAapReaction) {
|
||||||
ReactionsDivider()
|
ReactionsDivider()
|
||||||
@@ -516,30 +529,62 @@ fun DeviceSettingsScreen(
|
|||||||
onCheckedChange = onPersonalizedVolumeChange,
|
onCheckedChange = onPersonalizedVolumeChange,
|
||||||
enabled = enabled,
|
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 (features.hasToneVolume && toneVol != null) {
|
||||||
ToneVolumeSlider(
|
if (isPro) {
|
||||||
level = toneVol.level,
|
ToneVolumeSlider(
|
||||||
onLevelChange = onToneVolumeChange,
|
level = toneVol.level,
|
||||||
enabled = enabled,
|
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 (features.hasMicrophoneMode) {
|
||||||
val micMode = device.microphoneMode
|
if (isPro) {
|
||||||
?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO)
|
val micMode = device.microphoneMode
|
||||||
SegmentedSettingRow(
|
?: AapSetting.MicrophoneMode(AapSetting.MicrophoneMode.Mode.AUTO)
|
||||||
icon = Icons.TwoTone.Mic,
|
SegmentedSettingRow(
|
||||||
title = stringResource(R.string.device_settings_microphone_mode_label),
|
icon = Icons.TwoTone.Mic,
|
||||||
subtitle = stringResource(R.string.device_settings_microphone_mode_description),
|
title = stringResource(R.string.device_settings_microphone_mode_label),
|
||||||
options = listOf(
|
subtitle = stringResource(R.string.device_settings_microphone_mode_description),
|
||||||
stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO,
|
options = listOf(
|
||||||
stringResource(R.string.device_settings_microphone_mode_left) to AapSetting.MicrophoneMode.Mode.ALWAYS_LEFT,
|
stringResource(R.string.device_settings_microphone_mode_auto) to AapSetting.MicrophoneMode.Mode.AUTO,
|
||||||
stringResource(R.string.device_settings_microphone_mode_right) to AapSetting.MicrophoneMode.Mode.ALWAYS_RIGHT,
|
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,
|
selected = micMode.mode,
|
||||||
enabled = enabled,
|
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
|
package eu.darken.capod.main.ui.devicesettings
|
||||||
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import eu.darken.capod.common.WebpageTool
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.datastore.value
|
import eu.darken.capod.common.datastore.value
|
||||||
@@ -50,6 +51,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
|||||||
private val profilesRepo: DeviceProfilesRepo,
|
private val profilesRepo: DeviceProfilesRepo,
|
||||||
private val generalSettings: GeneralSettings,
|
private val generalSettings: GeneralSettings,
|
||||||
private val timeSource: TimeSource,
|
private val timeSource: TimeSource,
|
||||||
|
private val webpageTool: WebpageTool,
|
||||||
) : ViewModel4(dispatcherProvider) {
|
) : ViewModel4(dispatcherProvider) {
|
||||||
|
|
||||||
private val targetProfileId = MutableStateFlow<ProfileId?>(null)
|
private val targetProfileId = MutableStateFlow<ProfileId?>(null)
|
||||||
@@ -208,7 +210,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun setPersonalizedVolume(enabled: Boolean) = send(AapCommand.SetPersonalizedVolume(enabled))
|
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))
|
fun setAdaptiveAudioNoise(level: Int) = send(AapCommand.SetAdaptiveAudioNoise(level))
|
||||||
|
|
||||||
@@ -225,7 +227,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
|||||||
endCall: AapSetting.EndCallMuteMic.EndCallMode,
|
endCall: AapSetting.EndCallMuteMic.EndCallMode,
|
||||||
) = send(AapCommand.SetEndCallMuteMic(muteMic, endCall))
|
) = 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))
|
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 {
|
fun setDeviceName(name: String) = launch {
|
||||||
val address = currentAddress() ?: return@launch
|
val address = currentAddress() ?: return@launch
|
||||||
@@ -395,6 +397,10 @@ class DeviceSettingsViewModel @Inject constructor(
|
|||||||
navTo(Nav.Main.Upgrade)
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openIssueTracker() {
|
||||||
|
webpageTool.open("https://github.com/d4rken-org/capod/issues")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("DeviceSettings", "VM")
|
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="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="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_label">Device status</string>
|
||||||
<string name="notification_channel_device_status_connected_label">Connected device</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>
|
<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,
|
profilesRepo = profilesRepo,
|
||||||
generalSettings = generalSettings,
|
generalSettings = generalSettings,
|
||||||
timeSource = timeSource,
|
timeSource = timeSource,
|
||||||
|
webpageTool = mockk(relaxed = true),
|
||||||
).also { vm = it }
|
).also { vm = it }
|
||||||
|
|
||||||
private fun runVmTest(testBody: suspend TestScope.() -> Unit) = runTest(testDispatcher) {
|
private fun runVmTest(testBody: suspend TestScope.() -> Unit) = runTest(testDispatcher) {
|
||||||
|
|||||||
Reference in New Issue
Block a user