diff --git a/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt b/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt index 517885a4..14b29d6e 100644 --- a/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt +++ b/app/src/main/java/eu/darken/capod/common/settings/SettingsInfoBox.kt @@ -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") + } + }, + ) +} diff --git a/app/src/main/java/eu/darken/capod/common/settings/SettingsSwitchItem.kt b/app/src/main/java/eu/darken/capod/common/settings/SettingsSwitchItem.kt index e474af16..cbb40ca6 100644 --- a/app/src/main/java/eu/darken/capod/common/settings/SettingsSwitchItem.kt +++ b/app/src/main/java/eu/darken/capod/common/settings/SettingsSwitchItem.kt @@ -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) ) diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index 972333bd..0145e5d8 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -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, + ) + } } } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt index 401fc542..1c2425e9 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt @@ -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(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") } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d49a6b2b..650d68e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,6 +90,10 @@ 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. Fix it + Experimental feature + This feature hasn\'t been tested on all devices yet. If something isn\'t working correctly, please open an issue with a debug log. + Open Issue Tracker + Device status Connected device You can disable this notification in system settings. diff --git a/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt b/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt index f7458409..d477fecd 100644 --- a/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt +++ b/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt @@ -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) {