mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 19:26:12 -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,
|
now = MOCK_NOW,
|
||||||
isPro = true,
|
isPro = true,
|
||||||
isNudgeAvailable = true,
|
isNudgeAvailable = true,
|
||||||
|
isClassicallyConnected = true,
|
||||||
),
|
),
|
||||||
onNavigateUp = {},
|
onNavigateUp = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ object MockPodDataProvider {
|
|||||||
profileId = "preview-cached",
|
profileId = "preview-cached",
|
||||||
ble = null,
|
ble = null,
|
||||||
aap = null,
|
aap = null,
|
||||||
|
profileAddress = "AA:BB:CC:DD:EE:FF",
|
||||||
|
profileModel = PodModel.AIRPODS_PRO2,
|
||||||
cached = CachedDeviceState(
|
cached = CachedDeviceState(
|
||||||
profileId = "preview-cached",
|
profileId = "preview-cached",
|
||||||
model = PodModel.AIRPODS_PRO2,
|
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.NoiseControlCard
|
||||||
import eu.darken.capod.main.ui.devicesettings.cards.NotConnectedCard
|
import eu.darken.capod.main.ui.devicesettings.cards.NotConnectedCard
|
||||||
import eu.darken.capod.main.ui.devicesettings.cards.ReactionsCard
|
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.SoundCard
|
||||||
import eu.darken.capod.main.ui.devicesettings.cards.buildDeviceInfoDetailItems
|
import eu.darken.capod.main.ui.devicesettings.cards.buildDeviceInfoDetailItems
|
||||||
import eu.darken.capod.main.ui.devicesettings.cards.buildModelLabel
|
import eu.darken.capod.main.ui.devicesettings.cards.buildModelLabel
|
||||||
@@ -86,6 +87,18 @@ fun DeviceSettingsScreenHost(
|
|||||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||||
val offRejectedMessage = stringResource(R.string.device_settings_anc_off_rejected_message)
|
val offRejectedMessage = stringResource(R.string.device_settings_anc_off_rejected_message)
|
||||||
val chargeCapRejectedMessage = stringResource(R.string.device_settings_charge_cap_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) {
|
LaunchedEffect(Unit) {
|
||||||
vm.events.collect { event ->
|
vm.events.collect { event ->
|
||||||
@@ -148,6 +161,7 @@ fun DeviceSettingsScreenHost(
|
|||||||
onDynamicEndOfChargeChange = { vm.setDynamicEndOfCharge(it) },
|
onDynamicEndOfChargeChange = { vm.setDynamicEndOfCharge(it) },
|
||||||
onDeviceNameChange = { vm.setDeviceName(it) },
|
onDeviceNameChange = { vm.setDeviceName(it) },
|
||||||
onPressControlsClick = { vm.navToPressControls() },
|
onPressControlsClick = { vm.navToPressControls() },
|
||||||
|
onEditProfile = { vm.navToEditProfile() },
|
||||||
onForceConnect = { vm.forceConnect() },
|
onForceConnect = { vm.forceConnect() },
|
||||||
onUpgrade = { vm.launchUpgrade() },
|
onUpgrade = { vm.launchUpgrade() },
|
||||||
onOnePodModeChange = { vm.setOnePodMode(it) },
|
onOnePodModeChange = { vm.setOnePodMode(it) },
|
||||||
@@ -185,6 +199,7 @@ fun DeviceSettingsScreen(
|
|||||||
onDynamicEndOfChargeChange: (Boolean) -> Unit = {},
|
onDynamicEndOfChargeChange: (Boolean) -> Unit = {},
|
||||||
onDeviceNameChange: (String) -> Unit = {},
|
onDeviceNameChange: (String) -> Unit = {},
|
||||||
onPressControlsClick: () -> Unit = {},
|
onPressControlsClick: () -> Unit = {},
|
||||||
|
onEditProfile: () -> Unit = {},
|
||||||
onForceConnect: () -> Unit = {},
|
onForceConnect: () -> Unit = {},
|
||||||
onUpgrade: () -> Unit = {},
|
onUpgrade: () -> Unit = {},
|
||||||
onOnePodModeChange: (Boolean) -> Unit = {},
|
onOnePodModeChange: (Boolean) -> Unit = {},
|
||||||
@@ -279,27 +294,46 @@ fun DeviceSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not connected info — BLE live but no AAP connection
|
// Profile has no paired Bluetooth device — supersedes all other state cards
|
||||||
if (device != null && device.ble != null && !device.isAapConnected && device.address != null) {
|
if (device != null && !device.hasSelectedPairedDevice) {
|
||||||
if (state.isClassicallyConnected) {
|
item("missing_paired_device") {
|
||||||
// Device is connected for audio but AAP isn't available — show passive info
|
MissingPairedDeviceBanner(
|
||||||
item("aap_unavailable_info") {
|
onClick = onEditProfile,
|
||||||
AapUnavailableCard(onOpenTracker = onOpenAapTracker)
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
}
|
)
|
||||||
} else {
|
|
||||||
// Device is nearby but not connected — prompt user to connect
|
|
||||||
item("not_connected_info") {
|
|
||||||
NotConnectedCard(
|
|
||||||
isNudgeAvailable = state.isNudgeAvailable,
|
|
||||||
isForceConnecting = state.isForceConnecting,
|
|
||||||
onConnect = onForceConnect,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Reactions (per-profile, not gated on AAP) ─────────────────
|
// Not nearby — no live BLE; settings require the device to be present
|
||||||
if (device != null && features != null) {
|
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") {
|
item("reactions_section") {
|
||||||
ReactionsCard(
|
ReactionsCard(
|
||||||
device = device,
|
device = device,
|
||||||
@@ -322,15 +356,7 @@ fun DeviceSettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings — only show when AAP is connected
|
// Settings — only show when AAP is connected
|
||||||
if (features != null && device.isAapConnected) {
|
if (features != null && device.isAapConnected && device.hasSelectedPairedDevice) {
|
||||||
|
|
||||||
if (device.hasPendingSettings == true) {
|
|
||||||
item("pending_info") {
|
|
||||||
SettingsInfoBox(
|
|
||||||
text = stringResource(R.string.device_settings_pending_info),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Noise Control ────────────────────────────
|
// ── Noise Control ────────────────────────────
|
||||||
if (features.hasAncControl && device.ancMode != null) {
|
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") {
|
item("bottom_spacer") {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
@@ -501,6 +537,7 @@ internal fun previewFullState(isPro: Boolean) = DeviceSettingsViewModel.State(
|
|||||||
),
|
),
|
||||||
now = MOCK_NOW,
|
now = MOCK_NOW,
|
||||||
isPro = isPro,
|
isPro = isPro,
|
||||||
|
isClassicallyConnected = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Preview2
|
@Preview2
|
||||||
@@ -566,3 +603,15 @@ private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper {
|
|||||||
onNavigateUp = {},
|
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))
|
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() {
|
fun launchUpgrade() {
|
||||||
log(TAG, INFO) { "launchUpgrade()" }
|
log(TAG, INFO) { "launchUpgrade()" }
|
||||||
navTo(Nav.Main.Upgrade)
|
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_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_connect_action">Connect</string>
|
||||||
<string name="device_settings_not_connected_open_settings_action">Open Bluetooth Settings</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_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_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>
|
<string name="device_settings_aap_unavailable_action">View compatibility tracker</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user