mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
feat(popup): Suppress popup when app is visible and add settings hints
Hide the overlay popup when MainActivity is in the foreground since the user can already see battery info in the app. Show an info card when popups are enabled explaining they only appear outside the app. Show a warning card with a Fix button when monitor mode is MANUAL. Refactor SettingsInfoBox into a reusable component with INFO/WARNING types and optional action slot.
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
package eu.darken.capod.common.settings
|
||||
|
||||
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.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -17,33 +21,61 @@ import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
|
||||
enum class InfoBoxType { INFO, WARNING }
|
||||
|
||||
@Composable
|
||||
fun SettingsInfoBox(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
type: InfoBoxType = InfoBoxType.INFO,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
val containerColor = when (type) {
|
||||
InfoBoxType.INFO -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
|
||||
InfoBoxType.WARNING -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.4f)
|
||||
}
|
||||
val iconTint = when (type) {
|
||||
InfoBoxType.INFO -> MaterialTheme.colorScheme.primary
|
||||
InfoBoxType.WARNING -> MaterialTheme.colorScheme.tertiary
|
||||
}
|
||||
val icon = when (type) {
|
||||
InfoBoxType.INFO -> Icons.Outlined.Info
|
||||
InfoBoxType.WARNING -> Icons.Outlined.Warning
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f),
|
||||
color = containerColor,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp, bottom = if (action != null) 8.dp else 16.dp),
|
||||
verticalAlignment = if (action != null) Alignment.Top else Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
tint = iconTint,
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp, top = 2.dp)
|
||||
.padding(end = 10.dp)
|
||||
.size(20.dp),
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = if (action != null) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
)
|
||||
if (action != null) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,3 +87,26 @@ private fun SettingsInfoBoxPreview() = PreviewWrapper {
|
||||
text = "If ear detection only works for one pod, this is an Apple limitation. Only the \"primary pod\" (used for microphone) is detected.",
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SettingsInfoBoxWarningPreview() = PreviewWrapper {
|
||||
SettingsInfoBox(
|
||||
text = "This feature requires the monitor to be running.",
|
||||
type = InfoBoxType.WARNING,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SettingsInfoBoxWarningWithActionPreview() = PreviewWrapper {
|
||||
SettingsInfoBox(
|
||||
text = "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.",
|
||||
type = InfoBoxType.WARNING,
|
||||
action = {
|
||||
TextButton(onClick = {}) {
|
||||
Text("Fix it")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import eu.darken.capod.common.uix.Activity2
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.currentThemeState
|
||||
import eu.darken.capod.main.core.themeState
|
||||
import eu.darken.capod.reaction.ui.popup.PopUpWindow
|
||||
import javax.inject.Inject
|
||||
import eu.darken.capod.common.datastore.valueBlocking
|
||||
|
||||
@@ -41,6 +42,7 @@ class MainActivity : Activity2() {
|
||||
@Inject lateinit var navCtrl: NavigationController
|
||||
@Inject lateinit var navigationEntries: Set<@JvmSuppressWildcards NavigationEntry>
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var popUpWindow: PopUpWindow
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -98,6 +100,17 @@ class MainActivity : Activity2() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
popUpWindow.isMainActivityVisible = true
|
||||
popUpWindow.close()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
popUpWindow.isMainActivityVisible = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
consumeUpgradeExtra(intent)
|
||||
|
||||
@@ -86,6 +86,8 @@ 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.common.settings.SettingsCategoryHeader
|
||||
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
|
||||
@@ -168,6 +170,7 @@ fun DeviceSettingsScreenHost(
|
||||
onAutoConnectConditionChange = { vm.setAutoConnectCondition(it) },
|
||||
onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) },
|
||||
onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) },
|
||||
onFixMonitorMode = { vm.setMonitorModeAutomatic() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -204,6 +207,7 @@ fun DeviceSettingsScreen(
|
||||
onAutoConnectConditionChange: (AutoConnectCondition) -> Unit = {},
|
||||
onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {},
|
||||
onShowPopUpOnConnectionChange: (Boolean) -> Unit = {},
|
||||
onFixMonitorMode: () -> Unit = {},
|
||||
) {
|
||||
val device = state.device
|
||||
val features = device?.model?.features
|
||||
@@ -377,6 +381,22 @@ fun DeviceSettingsScreen(
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -87,7 +87,14 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
// The Bluetooth HEADSET profile lookup can be slow to produce its first value.
|
||||
// Seed this branch so the screen can render immediately after navigation.
|
||||
bluetoothManager.connectedDevices.onStart { emit(emptyList()) },
|
||||
) { _, device, upgrade, forcing, connectedDevices ->
|
||||
generalSettings.monitorMode.flow,
|
||||
) { args ->
|
||||
val device = args[1] as PodDevice?
|
||||
val upgrade = args[2] as eu.darken.capod.common.upgrade.UpgradeRepo.Info
|
||||
val forcing = args[3] as Boolean
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val connectedDevices = args[4] as Collection<eu.darken.capod.common.bluetooth.BluetoothDevice2>
|
||||
val monitorMode = args[5] as MonitorMode
|
||||
val connectedAddresses = connectedDevices.map { it.address }.toSet()
|
||||
State(
|
||||
device = device,
|
||||
@@ -96,6 +103,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
isNudgeAvailable = bluetoothManager.isNudgeAvailable,
|
||||
isForceConnecting = forcing,
|
||||
isClassicallyConnected = device?.address?.let { it in connectedAddresses } == true,
|
||||
monitorMode = monitorMode,
|
||||
)
|
||||
}
|
||||
}.asLiveState()
|
||||
@@ -118,6 +126,7 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
val isNudgeAvailable: Boolean = true,
|
||||
val isForceConnecting: Boolean = false,
|
||||
val isClassicallyConnected: Boolean = false,
|
||||
val monitorMode: MonitorMode = MonitorMode.AUTOMATIC,
|
||||
) {
|
||||
val reactions: ReactionConfig get() = device?.reactions ?: ReactionConfig()
|
||||
}
|
||||
@@ -316,6 +325,10 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
fun setShowPopUpOnConnection(enabled: Boolean) =
|
||||
proGatedReaction(enabled) { it.copy(showPopUpOnConnection = enabled) }
|
||||
|
||||
fun setMonitorModeAutomatic() = launch {
|
||||
generalSettings.monitorMode.value(MonitorMode.AUTOMATIC)
|
||||
}
|
||||
|
||||
fun navToStemConfig() = launch {
|
||||
if (upgradeRepo.isPro()) {
|
||||
navTo(Nav.Main.StemActionConfig)
|
||||
|
||||
@@ -52,7 +52,13 @@ class PopUpWindow @Inject constructor(
|
||||
private var lifecycleOwner: OverlayLifecycleOwner? = null
|
||||
private var deviceState: MutableState<PodDevice?>? = null
|
||||
|
||||
@Volatile var isMainActivityVisible: Boolean = false
|
||||
|
||||
fun show(device: PodDevice) {
|
||||
if (isMainActivityVisible) {
|
||||
log(TAG) { "Suppressing popup, MainActivity is visible" }
|
||||
return
|
||||
}
|
||||
try {
|
||||
log(TAG) { "open()" }
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
<string name="settings_popup_caseopen_description">Show a popup when the device case is opened (experimental).</string>
|
||||
<string name="settings_popup_connected_label">Show connection popup</string>
|
||||
<string name="settings_popup_connected_description">Show a popup when the device connects for the first time.</string>
|
||||
<string name="settings_popup_info_not_in_app">Popups are only shown when you are not in 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="notification_channel_device_status_label">Device status</string>
|
||||
<string name="notification_channel_device_status_connected_label">Connected device</string>
|
||||
|
||||
Reference in New Issue
Block a user