mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
feat(settings): Tailor device settings to connection state
- Hide reactions and AAP sections unless device is classically connected - Move advanced-settings-unavailable card to the bottom of the list - Show 'device not nearby' infobox when out of range - Show missing-paired-device banner with edit-profile action - Replace pending banner with snackbar on user-initiated change
This commit is contained in:
@@ -118,6 +118,7 @@ internal fun DeviceSettingsReactionsContent() = PreviewWrapper {
|
||||
now = MOCK_NOW,
|
||||
isPro = true,
|
||||
isNudgeAvailable = true,
|
||||
isClassicallyConnected = true,
|
||||
),
|
||||
onNavigateUp = {},
|
||||
)
|
||||
|
||||
@@ -289,6 +289,8 @@ object MockPodDataProvider {
|
||||
profileId = "preview-cached",
|
||||
ble = null,
|
||||
aap = null,
|
||||
profileAddress = "AA:BB:CC:DD:EE:FF",
|
||||
profileModel = PodModel.AIRPODS_PRO2,
|
||||
cached = CachedDeviceState(
|
||||
profileId = "preview-cached",
|
||||
model = PodModel.AIRPODS_PRO2,
|
||||
|
||||
@@ -48,6 +48,7 @@ import eu.darken.capod.main.ui.devicesettings.cards.DeviceInfoCard
|
||||
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.cards.ReactionsCard
|
||||
import eu.darken.capod.main.ui.overview.cards.components.MissingPairedDeviceBanner
|
||||
import eu.darken.capod.main.ui.devicesettings.cards.SoundCard
|
||||
import eu.darken.capod.main.ui.devicesettings.cards.buildDeviceInfoDetailItems
|
||||
import eu.darken.capod.main.ui.devicesettings.cards.buildModelLabel
|
||||
@@ -86,6 +87,18 @@ fun DeviceSettingsScreenHost(
|
||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||
val offRejectedMessage = stringResource(R.string.device_settings_anc_off_rejected_message)
|
||||
val chargeCapRejectedMessage = stringResource(R.string.device_settings_charge_cap_rejected_message)
|
||||
val pendingInfoMessage = stringResource(R.string.device_settings_pending_info)
|
||||
|
||||
val hasPendingSettings = state?.device?.hasPendingSettings
|
||||
var lastPendingState by remember { mutableStateOf<Boolean?>(null) }
|
||||
LaunchedEffect(hasPendingSettings) {
|
||||
val current = hasPendingSettings ?: return@LaunchedEffect
|
||||
val previous = lastPendingState
|
||||
lastPendingState = current
|
||||
if (previous == false && current) {
|
||||
snackbarHostState.showSnackbar(pendingInfoMessage)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
vm.events.collect { event ->
|
||||
@@ -148,6 +161,7 @@ fun DeviceSettingsScreenHost(
|
||||
onDynamicEndOfChargeChange = { vm.setDynamicEndOfCharge(it) },
|
||||
onDeviceNameChange = { vm.setDeviceName(it) },
|
||||
onPressControlsClick = { vm.navToPressControls() },
|
||||
onEditProfile = { vm.navToEditProfile() },
|
||||
onForceConnect = { vm.forceConnect() },
|
||||
onUpgrade = { vm.launchUpgrade() },
|
||||
onOnePodModeChange = { vm.setOnePodMode(it) },
|
||||
@@ -185,6 +199,7 @@ fun DeviceSettingsScreen(
|
||||
onDynamicEndOfChargeChange: (Boolean) -> Unit = {},
|
||||
onDeviceNameChange: (String) -> Unit = {},
|
||||
onPressControlsClick: () -> Unit = {},
|
||||
onEditProfile: () -> Unit = {},
|
||||
onForceConnect: () -> Unit = {},
|
||||
onUpgrade: () -> Unit = {},
|
||||
onOnePodModeChange: (Boolean) -> Unit = {},
|
||||
@@ -279,27 +294,46 @@ fun DeviceSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Not connected info — BLE live but no AAP connection
|
||||
if (device != null && device.ble != null && !device.isAapConnected && device.address != null) {
|
||||
if (state.isClassicallyConnected) {
|
||||
// Device is connected for audio but AAP isn't available — show passive info
|
||||
item("aap_unavailable_info") {
|
||||
AapUnavailableCard(onOpenTracker = onOpenAapTracker)
|
||||
}
|
||||
} else {
|
||||
// Device is nearby but not connected — prompt user to connect
|
||||
item("not_connected_info") {
|
||||
NotConnectedCard(
|
||||
isNudgeAvailable = state.isNudgeAvailable,
|
||||
isForceConnecting = state.isForceConnecting,
|
||||
onConnect = onForceConnect,
|
||||
)
|
||||
}
|
||||
// Profile has no paired Bluetooth device — supersedes all other state cards
|
||||
if (device != null && !device.hasSelectedPairedDevice) {
|
||||
item("missing_paired_device") {
|
||||
MissingPairedDeviceBanner(
|
||||
onClick = onEditProfile,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reactions (per-profile, not gated on AAP) ─────────────────
|
||||
if (device != null && features != null) {
|
||||
// Not nearby — no live BLE; settings require the device to be present
|
||||
if (device != null && device.hasSelectedPairedDevice &&
|
||||
device.ble == null && !state.isClassicallyConnected
|
||||
) {
|
||||
item("not_nearby_info") {
|
||||
SettingsInfoBox(
|
||||
title = stringResource(R.string.device_settings_not_nearby_label),
|
||||
text = stringResource(R.string.device_settings_not_nearby_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Not connected info — device is nearby but not connected; prompt user to connect
|
||||
if (device != null && device.hasSelectedPairedDevice &&
|
||||
device.ble != null && !device.isAapConnected && device.address != null &&
|
||||
!state.isClassicallyConnected
|
||||
) {
|
||||
item("not_connected_info") {
|
||||
NotConnectedCard(
|
||||
isNudgeAvailable = state.isNudgeAvailable,
|
||||
isForceConnecting = state.isForceConnecting,
|
||||
onConnect = onForceConnect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reactions (gated on classic connection — needs phone to be the audio target) ──
|
||||
if (device != null && device.hasSelectedPairedDevice &&
|
||||
features != null && state.isClassicallyConnected
|
||||
) {
|
||||
item("reactions_section") {
|
||||
ReactionsCard(
|
||||
device = device,
|
||||
@@ -322,15 +356,7 @@ fun DeviceSettingsScreen(
|
||||
}
|
||||
|
||||
// Settings — only show when AAP is connected
|
||||
if (features != null && device.isAapConnected) {
|
||||
|
||||
if (device.hasPendingSettings == true) {
|
||||
item("pending_info") {
|
||||
SettingsInfoBox(
|
||||
text = stringResource(R.string.device_settings_pending_info),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (features != null && device.isAapConnected && device.hasSelectedPairedDevice) {
|
||||
|
||||
// ── Noise Control ────────────────────────────
|
||||
if (features.hasAncControl && device.ancMode != null) {
|
||||
@@ -445,6 +471,16 @@ fun DeviceSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced settings unavailable — phone's Bluetooth lacks AAP support; passive info, shown last
|
||||
if (device != null && device.hasSelectedPairedDevice &&
|
||||
device.ble != null && !device.isAapConnected && device.address != null &&
|
||||
state.isClassicallyConnected
|
||||
) {
|
||||
item("aap_unavailable_info") {
|
||||
AapUnavailableCard(onOpenTracker = onOpenAapTracker)
|
||||
}
|
||||
}
|
||||
|
||||
item("bottom_spacer") {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
@@ -501,6 +537,7 @@ internal fun previewFullState(isPro: Boolean) = DeviceSettingsViewModel.State(
|
||||
),
|
||||
now = MOCK_NOW,
|
||||
isPro = isPro,
|
||||
isClassicallyConnected = true,
|
||||
)
|
||||
|
||||
@Preview2
|
||||
@@ -566,3 +603,15 @@ private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper {
|
||||
onNavigateUp = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun DeviceSettingsMissingPairedDevicePreview() = PreviewWrapper {
|
||||
DeviceSettingsScreen(
|
||||
state = DeviceSettingsViewModel.State(
|
||||
device = MockPodDataProvider.dualPodMissingPairedDevice(),
|
||||
now = MOCK_NOW,
|
||||
),
|
||||
onNavigateUp = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -442,6 +442,12 @@ class DeviceSettingsViewModel @Inject constructor(
|
||||
navTo(Nav.Main.PressControls(profileId = profileId))
|
||||
}
|
||||
|
||||
fun navToEditProfile() = launch {
|
||||
log(TAG, INFO) { "navToEditProfile()" }
|
||||
val profileId = targetProfileId.value ?: return@launch
|
||||
navTo(Nav.Main.DeviceProfileCreation(profileId = profileId))
|
||||
}
|
||||
|
||||
fun launchUpgrade() {
|
||||
log(TAG, INFO) { "launchUpgrade()" }
|
||||
navTo(Nav.Main.Upgrade)
|
||||
|
||||
@@ -472,6 +472,8 @@
|
||||
<string name="device_settings_not_connected_description">This device is nearby but not connected to this phone. Connect to access settings and controls.</string>
|
||||
<string name="device_settings_not_connected_connect_action">Connect</string>
|
||||
<string name="device_settings_not_connected_open_settings_action">Open Bluetooth Settings</string>
|
||||
<string name="device_settings_not_nearby_label">Device not nearby</string>
|
||||
<string name="device_settings_not_nearby_description">Settings are only available when this device is nearby and connected to your phone.</string>
|
||||
<string name="device_settings_aap_unavailable_label">Advanced settings unavailable</string>
|
||||
<string name="device_settings_aap_unavailable_description">Advanced AirPods settings require a Bluetooth feature that is not available on this phone. This may be resolved by a future Android update from your device manufacturer.</string>
|
||||
<string name="device_settings_aap_unavailable_action">View compatibility tracker</string>
|
||||
|
||||
Reference in New Issue
Block a user