From 34c23751b3842b537a650c4a490325df796a5115 Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 13 Apr 2026 18:58:49 +0200 Subject: [PATCH] refactor(ui): Split PodCardComponents into per-component files Move each composable from PodCardComponents.kt into its own file under a new cards/components/ subpackage. Add previews to each file. Also add getBatteryIcon() for Compose Material Icon battery levels used in the popup. --- .../main/ui/overview/cards/DualPodsCard.kt | 7 + .../ui/overview/cards/PodCardComponents.kt | 468 ------------------ .../main/ui/overview/cards/SinglePodsCard.kt | 5 + .../ui/overview/cards/UnknownPodDeviceCard.kt | 1 + .../cards/components/AncModeSelector.kt | 91 ++++ .../cards/components/BatteryCapsule.kt | 76 +++ .../components/ConversationAwarenessToggle.kt | 47 ++ .../overview/cards/components/DebugSection.kt | 38 ++ .../cards/components/DeviceConnectionBadge.kt | 142 ++++++ .../cards/components/SignalIndicator.kt | 119 +++++ .../overview/cards/components/StatusChip.kt | 118 +++++ .../core/apple/ble/PodDeviceExtensions.kt | 23 + .../capod/reaction/ui/popup/PopUpContent.kt | 6 +- .../main/ui/overview/cards/SignalLevelTest.kt | 3 + 14 files changed, 673 insertions(+), 471 deletions(-) delete mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/PodCardComponents.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/AncModeSelector.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/BatteryCapsule.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/ConversationAwarenessToggle.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DebugSection.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DeviceConnectionBadge.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/SignalIndicator.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/StatusChip.kt diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt index 968bf095..2dede7c3 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt @@ -43,6 +43,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import eu.darken.capod.R +import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector +import eu.darken.capod.main.ui.overview.cards.components.BatteryCapsule +import eu.darken.capod.main.ui.overview.cards.components.DebugSection +import eu.darken.capod.main.ui.overview.cards.components.DeviceConnectionBadge +import eu.darken.capod.main.ui.overview.cards.components.SignalIndicator +import eu.darken.capod.main.ui.overview.cards.components.StatusChip +import eu.darken.capod.main.ui.overview.cards.components.StatusChipRow import eu.darken.capod.common.SystemTimeSource import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/PodCardComponents.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/PodCardComponents.kt deleted file mode 100644 index 5bb4a23f..00000000 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/PodCardComponents.kt +++ /dev/null @@ -1,468 +0,0 @@ -package eu.darken.capod.main.ui.overview.cards - -import androidx.annotation.VisibleForTesting -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -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.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Key -import androidx.compose.material.icons.twotone.BatteryChargingFull -import androidx.compose.material.icons.twotone.Bluetooth -import androidx.compose.material.icons.twotone.Hearing -import androidx.compose.material.icons.twotone.KeyboardVoice -import androidx.compose.material.icons.twotone.LinkOff -import androidx.compose.material.icons.twotone.Lock -import androidx.compose.material.icons.twotone.SettingsInputAntenna -import androidx.compose.material.icons.twotone.Stars -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import eu.darken.capod.R -import eu.darken.capod.common.compose.Preview2 -import eu.darken.capod.common.compose.PreviewWrapper -import eu.darken.capod.monitor.core.BleKeyState -import eu.darken.capod.monitor.core.ConnectionState -import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting - -private val CapsuleShape = RoundedCornerShape(6.dp) - -@Composable -fun BatteryCapsule( - percent: Float, - modifier: Modifier = Modifier, -) { - val clamped = if (percent >= 0f) percent.coerceIn(0f, 1f) else -1f - val animatedFraction by animateFloatAsState( - targetValue = if (clamped >= 0f) clamped else 0f, - animationSpec = tween(600, easing = FastOutSlowInEasing), - label = "batteryFill", - ) - - val barColor = when { - clamped < 0f -> MaterialTheme.colorScheme.surfaceVariant - clamped > 0.30f -> MaterialTheme.colorScheme.primary - clamped >= 0.15f -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.error - } - - Box( - modifier = modifier - .clip(CapsuleShape) - .background(MaterialTheme.colorScheme.surfaceVariant), - ) { - if (clamped >= 0f) { - Box( - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(fraction = animatedFraction) - .clip(CapsuleShape) - .background(barColor), - ) - } - } -} - -@Composable -fun StatusChip( - icon: ImageVector, - label: String, - modifier: Modifier = Modifier, -) { - Surface( - modifier = modifier, - shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - ) { - Row( - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer, - ) - Spacer(modifier = Modifier.width(3.dp)) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun StatusChipRow( - isCharging: Boolean, - isInEar: Boolean, - showEarDetection: Boolean, - isMicrophone: Boolean, - showMicrophone: Boolean, - chargingLabel: String, - inEarLabel: String, - microphoneLabel: String, - modifier: Modifier = Modifier, -) { - FlowRow( - modifier = modifier.animateContentSize(), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - if (isCharging) { - StatusChip( - icon = Icons.TwoTone.BatteryChargingFull, - label = chargingLabel, - ) - } - if (showMicrophone && isMicrophone) { - StatusChip( - icon = Icons.TwoTone.KeyboardVoice, - label = microphoneLabel, - ) - } - if (showEarDetection && isInEar) { - StatusChip( - icon = Icons.TwoTone.Hearing, - label = inEarLabel, - ) - } - } -} - -@VisibleForTesting -internal enum class SignalLevel { DISCONNECTED, BARS_0, BARS_1, BARS_2, BARS_3, BARS_4 } - -@VisibleForTesting -internal fun signalLevelOf(signalQuality: Float, isLive: Boolean): SignalLevel { - if (!isLive) return SignalLevel.DISCONNECTED - if (!signalQuality.isFinite()) return SignalLevel.BARS_0 - val q = signalQuality.coerceIn(0f, 1f) - return when { - q >= 0.80f -> SignalLevel.BARS_4 - q >= 0.60f -> SignalLevel.BARS_3 - q >= 0.40f -> SignalLevel.BARS_2 - q >= 0.20f -> SignalLevel.BARS_1 - else -> SignalLevel.BARS_0 - } -} - -@Composable -fun SignalIndicator( - signalQuality: Float, - isLive: Boolean, - modifier: Modifier = Modifier, -) { - val level = signalLevelOf(signalQuality, isLive) - val description = when (level) { - SignalLevel.DISCONNECTED -> stringResource(R.string.signal_indicator_disconnected_cd) - SignalLevel.BARS_0 -> stringResource(R.string.signal_indicator_bars_cd, 0) - SignalLevel.BARS_1 -> stringResource(R.string.signal_indicator_bars_cd, 1) - SignalLevel.BARS_2 -> stringResource(R.string.signal_indicator_bars_cd, 2) - SignalLevel.BARS_3 -> stringResource(R.string.signal_indicator_bars_cd, 3) - SignalLevel.BARS_4 -> stringResource(R.string.signal_indicator_bars_cd, 4) - } - val tint = MaterialTheme.colorScheme.onSurfaceVariant - val rootModifier = modifier - .size(12.dp) - .semantics(mergeDescendants = true) { contentDescription = description } - - if (level == SignalLevel.DISCONNECTED) { - Icon( - imageVector = Icons.TwoTone.LinkOff, - contentDescription = null, - modifier = rootModifier, - tint = tint, - ) - return - } - - val activeBars = when (level) { - SignalLevel.BARS_0 -> 0 - SignalLevel.BARS_1 -> 1 - SignalLevel.BARS_2 -> 2 - SignalLevel.BARS_3 -> 3 - SignalLevel.BARS_4 -> 4 - SignalLevel.DISCONNECTED -> 0 // unreachable; handled above - } - val inactiveColor = tint.copy(alpha = 0.3f) - Canvas(modifier = rootModifier) { - val barCount = 4 - val gapWidth = size.width * 0.12f - val totalGap = gapWidth * (barCount - 1) - val barWidth = (size.width - totalGap) / barCount - val corner = CornerRadius(barWidth * 0.4f, barWidth * 0.4f) - for (i in 0 until barCount) { - val heightFraction = (i + 1).toFloat() / barCount - val barHeight = size.height * heightFraction - val x = i * (barWidth + gapWidth) - val y = size.height - barHeight - drawRoundRect( - color = if (i < activeBars) tint else inactiveColor, - topLeft = Offset(x, y), - size = Size(barWidth, barHeight), - cornerRadius = corner, - ) - } - } -} - -@Composable -fun DeviceConnectionBadge( - state: ConnectionState, - modifier: Modifier = Modifier, -) { - if (!state.hasBleData && state.bleKeyState == BleKeyState.NONE && !state.isAapConnected) return - Surface( - modifier = modifier, - shape = RoundedCornerShape(8.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - ) { - Row( - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(3.dp), - ) { - if (state.hasBleData) { - Icon( - imageVector = Icons.TwoTone.SettingsInputAntenna, - contentDescription = stringResource(R.string.signal_badge_ble_cd), - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - if (state.bleKeyState != BleKeyState.NONE) { - Icon( - imageVector = Icons.Outlined.Key, - contentDescription = stringResource(R.string.signal_badge_key_irk_cd), - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - if (state.bleKeyState == BleKeyState.IRK_AND_ENCRYPTED) { - Icon( - imageVector = Icons.TwoTone.Lock, - contentDescription = stringResource(R.string.signal_badge_key_encrypted_cd), - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - if (state.isAapConnected) { - Icon( - imageVector = Icons.TwoTone.Bluetooth, - contentDescription = stringResource(R.string.signal_badge_aap_cd), - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } -} - -@Composable -fun DebugSection( - rawDataHex: List, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "--- Debug ---", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = rawDataHex.joinToString("\n"), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} - -@Composable -fun AncModeSelector( - modifier: Modifier = Modifier, - currentMode: AapSetting.AncMode.Value, - supportedModes: List, - onModeSelected: (AapSetting.AncMode.Value) -> Unit, - pendingMode: AapSetting.AncMode.Value? = null, - enabled: Boolean = true, -) { - val displayMode = pendingMode ?: currentMode - Box(modifier = modifier.fillMaxWidth()) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - supportedModes.forEachIndexed { index, mode -> - SegmentedButton( - selected = mode == displayMode, - onClick = { onModeSelected(mode) }, - enabled = enabled, - shape = SegmentedButtonDefaults.itemShape(index, supportedModes.size), - colors = if (pendingMode != null && mode == displayMode) { - SegmentedButtonDefaults.colors( - activeContainerColor = MaterialTheme.colorScheme.secondaryContainer, - activeContentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } else { - SegmentedButtonDefaults.colors() - }, - label = { - Text( - text = when (mode) { - AapSetting.AncMode.Value.OFF -> stringResource(R.string.anc_mode_off) - AapSetting.AncMode.Value.ON -> stringResource(R.string.anc_mode_on) - AapSetting.AncMode.Value.TRANSPARENCY -> stringResource(R.string.anc_mode_transparency) - AapSetting.AncMode.Value.ADAPTIVE -> stringResource(R.string.anc_mode_adaptive) - }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - ) - } - } - } -} - -@Composable -fun ConversationAwarenessToggle( - enabled: Boolean, - onToggle: (Boolean) -> Unit, -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.conversation_awareness_label), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f), - ) - Switch( - checked = enabled, - onCheckedChange = onToggle, - ) - } -} - -@Preview2 -@Composable -private fun DeviceConnectionBadgeAllIconsPreview() = PreviewWrapper { - DeviceConnectionBadge( - state = ConnectionState( - hasBleData = true, - bleKeyState = BleKeyState.IRK_AND_ENCRYPTED, - isAapConnected = true, - rssiQuality = 1f, - ), - ) -} - -@Preview2 -@Composable -private fun DeviceConnectionBadgeBleOnlyPreview() = PreviewWrapper { - DeviceConnectionBadge( - state = ConnectionState( - hasBleData = true, - bleKeyState = BleKeyState.NONE, - isAapConnected = false, - rssiQuality = 0.7f, - ), - ) -} - -@Preview2 -@Composable -private fun DeviceConnectionBadgeBleIrkPreview() = PreviewWrapper { - DeviceConnectionBadge( - state = ConnectionState( - hasBleData = true, - bleKeyState = BleKeyState.IRK_ONLY, - isAapConnected = false, - rssiQuality = 0.5f, - ), - ) -} - -@Preview2 -@Composable -private fun DeviceConnectionBadgeAapOnlyPreview() = PreviewWrapper { - DeviceConnectionBadge( - state = ConnectionState( - hasBleData = false, - bleKeyState = BleKeyState.NONE, - isAapConnected = true, - rssiQuality = 0f, - ), - ) -} - -@Preview2 -@Composable -private fun DeviceConnectionBadgeEmptyPreview() = PreviewWrapper { - DeviceConnectionBadge( - state = ConnectionState( - hasBleData = false, - bleKeyState = BleKeyState.NONE, - isAapConnected = false, - rssiQuality = 0f, - ), - ) -} - -@Preview2 -@Composable -private fun SignalIndicatorPreviewsGroup() = PreviewWrapper { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SignalIndicator(signalQuality = 0.10f, isLive = true) - SignalIndicator(signalQuality = 0.35f, isLive = true) - SignalIndicator(signalQuality = 0.55f, isLive = true) - SignalIndicator(signalQuality = 0.75f, isLive = true) - SignalIndicator(signalQuality = 0.95f, isLive = true) - SignalIndicator(signalQuality = Float.NaN, isLive = true) - SignalIndicator(signalQuality = -0.3f, isLive = true) - SignalIndicator(signalQuality = 0.85f, isLive = false) - } -} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt index 955c3cdb..814c9fbb 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt @@ -43,6 +43,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import eu.darken.capod.R +import eu.darken.capod.main.ui.overview.cards.components.AncModeSelector +import eu.darken.capod.main.ui.overview.cards.components.DebugSection +import eu.darken.capod.main.ui.overview.cards.components.DeviceConnectionBadge +import eu.darken.capod.main.ui.overview.cards.components.SignalIndicator +import eu.darken.capod.main.ui.overview.cards.components.StatusChip import eu.darken.capod.common.SystemTimeSource import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt index 6f6a62d3..19d296af 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import eu.darken.capod.R +import eu.darken.capod.main.ui.overview.cards.components.SignalIndicator import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.preview.MockPodDataProvider diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/AncModeSelector.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/AncModeSelector.kt new file mode 100644 index 00000000..2b06a661 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/AncModeSelector.kt @@ -0,0 +1,91 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting + +@Composable +fun AncModeSelector( + modifier: Modifier = Modifier, + currentMode: AapSetting.AncMode.Value, + supportedModes: List, + onModeSelected: (AapSetting.AncMode.Value) -> Unit, + pendingMode: AapSetting.AncMode.Value? = null, + enabled: Boolean = true, +) { + val displayMode = pendingMode ?: currentMode + Box(modifier = modifier.fillMaxWidth()) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + supportedModes.forEachIndexed { index, mode -> + SegmentedButton( + selected = mode == displayMode, + onClick = { onModeSelected(mode) }, + enabled = enabled, + shape = SegmentedButtonDefaults.itemShape(index, supportedModes.size), + colors = if (pendingMode != null && mode == displayMode) { + SegmentedButtonDefaults.colors( + activeContainerColor = MaterialTheme.colorScheme.secondaryContainer, + activeContentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } else { + SegmentedButtonDefaults.colors() + }, + label = { + Text( + text = when (mode) { + AapSetting.AncMode.Value.OFF -> stringResource(R.string.anc_mode_off) + AapSetting.AncMode.Value.ON -> stringResource(R.string.anc_mode_on) + AapSetting.AncMode.Value.TRANSPARENCY -> stringResource(R.string.anc_mode_transparency) + AapSetting.AncMode.Value.ADAPTIVE -> stringResource(R.string.anc_mode_adaptive) + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + ) + } + } + } +} + +@Preview2 +@Composable +private fun AncModeSelectorPreview() = PreviewWrapper { + AncModeSelector( + currentMode = AapSetting.AncMode.Value.ON, + supportedModes = listOf( + AapSetting.AncMode.Value.OFF, + AapSetting.AncMode.Value.ON, + AapSetting.AncMode.Value.TRANSPARENCY, + AapSetting.AncMode.Value.ADAPTIVE, + ), + onModeSelected = {}, + ) +} + +@Preview2 +@Composable +private fun AncModeSelectorPendingPreview() = PreviewWrapper { + AncModeSelector( + currentMode = AapSetting.AncMode.Value.ON, + supportedModes = listOf( + AapSetting.AncMode.Value.OFF, + AapSetting.AncMode.Value.ON, + AapSetting.AncMode.Value.TRANSPARENCY, + ), + onModeSelected = {}, + pendingMode = AapSetting.AncMode.Value.TRANSPARENCY, + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/BatteryCapsule.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/BatteryCapsule.kt new file mode 100644 index 00000000..b821782c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/BatteryCapsule.kt @@ -0,0 +1,76 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper + +private val CapsuleShape = RoundedCornerShape(6.dp) + +@Composable +fun BatteryCapsule( + percent: Float, + modifier: Modifier = Modifier, +) { + val clamped = if (percent >= 0f) percent.coerceIn(0f, 1f) else -1f + val animatedFraction by animateFloatAsState( + targetValue = if (clamped >= 0f) clamped else 0f, + animationSpec = tween(600, easing = FastOutSlowInEasing), + label = "batteryFill", + ) + + val barColor = when { + clamped < 0f -> MaterialTheme.colorScheme.surfaceVariant + clamped > 0.30f -> MaterialTheme.colorScheme.primary + clamped >= 0.15f -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.error + } + + Box( + modifier = modifier + .clip(CapsuleShape) + .background(MaterialTheme.colorScheme.surfaceVariant), + ) { + if (clamped >= 0f) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(fraction = animatedFraction) + .clip(CapsuleShape) + .background(barColor), + ) + } + } +} + +@Preview2 +@Composable +private fun BatteryCapsuleFullPreview() = PreviewWrapper { + BatteryCapsule(percent = 1.0f, modifier = Modifier.width(120.dp).height(8.dp)) +} + +@Preview2 +@Composable +private fun BatteryCapsuleLowPreview() = PreviewWrapper { + BatteryCapsule(percent = 0.10f, modifier = Modifier.width(120.dp).height(8.dp)) +} + +@Preview2 +@Composable +private fun BatteryCapsuleUnknownPreview() = PreviewWrapper { + BatteryCapsule(percent = -1f, modifier = Modifier.width(120.dp).height(8.dp)) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/ConversationAwarenessToggle.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/ConversationAwarenessToggle.kt new file mode 100644 index 00000000..de03bf64 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/ConversationAwarenessToggle.kt @@ -0,0 +1,47 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper + +@Composable +fun ConversationAwarenessToggle( + enabled: Boolean, + onToggle: (Boolean) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.conversation_awareness_label), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) + Switch( + checked = enabled, + onCheckedChange = onToggle, + ) + } +} + +@Preview2 +@Composable +private fun ConversationAwarenessToggleEnabledPreview() = PreviewWrapper { + ConversationAwarenessToggle(enabled = true, onToggle = {}) +} + +@Preview2 +@Composable +private fun ConversationAwarenessToggleDisabledPreview() = PreviewWrapper { + ConversationAwarenessToggle(enabled = false, onToggle = {}) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DebugSection.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DebugSection.kt new file mode 100644 index 00000000..3c305d1f --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DebugSection.kt @@ -0,0 +1,38 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper + +@Composable +fun DebugSection( + rawDataHex: List, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "--- Debug ---", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = rawDataHex.joinToString("\n"), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +@Preview2 +@Composable +private fun DebugSectionPreview() = PreviewWrapper { + DebugSection(rawDataHex = listOf("07 19 01 0E 20 75 AA B5 31 00 00 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00")) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DeviceConnectionBadge.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DeviceConnectionBadge.kt new file mode 100644 index 00000000..6f454814 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/DeviceConnectionBadge.kt @@ -0,0 +1,142 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +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.Key +import androidx.compose.material.icons.twotone.Bluetooth +import androidx.compose.material.icons.twotone.Lock +import androidx.compose.material.icons.twotone.SettingsInputAntenna +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.monitor.core.BleKeyState +import eu.darken.capod.monitor.core.ConnectionState + +@Composable +fun DeviceConnectionBadge( + state: ConnectionState, + modifier: Modifier = Modifier, +) { + if (!state.hasBleData && state.bleKeyState == BleKeyState.NONE && !state.isAapConnected) return + Surface( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + Row( + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + if (state.hasBleData) { + Icon( + imageVector = Icons.TwoTone.SettingsInputAntenna, + contentDescription = stringResource(R.string.signal_badge_ble_cd), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (state.bleKeyState != BleKeyState.NONE) { + Icon( + imageVector = Icons.Outlined.Key, + contentDescription = stringResource(R.string.signal_badge_key_irk_cd), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (state.bleKeyState == BleKeyState.IRK_AND_ENCRYPTED) { + Icon( + imageVector = Icons.TwoTone.Lock, + contentDescription = stringResource(R.string.signal_badge_key_encrypted_cd), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (state.isAapConnected) { + Icon( + imageVector = Icons.TwoTone.Bluetooth, + contentDescription = stringResource(R.string.signal_badge_aap_cd), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Preview2 +@Composable +private fun DeviceConnectionBadgeAllIconsPreview() = PreviewWrapper { + DeviceConnectionBadge( + state = ConnectionState( + hasBleData = true, + bleKeyState = BleKeyState.IRK_AND_ENCRYPTED, + isAapConnected = true, + rssiQuality = 1f, + ), + ) +} + +@Preview2 +@Composable +private fun DeviceConnectionBadgeBleOnlyPreview() = PreviewWrapper { + DeviceConnectionBadge( + state = ConnectionState( + hasBleData = true, + bleKeyState = BleKeyState.NONE, + isAapConnected = false, + rssiQuality = 0.7f, + ), + ) +} + +@Preview2 +@Composable +private fun DeviceConnectionBadgeBleIrkPreview() = PreviewWrapper { + DeviceConnectionBadge( + state = ConnectionState( + hasBleData = true, + bleKeyState = BleKeyState.IRK_ONLY, + isAapConnected = false, + rssiQuality = 0.5f, + ), + ) +} + +@Preview2 +@Composable +private fun DeviceConnectionBadgeAapOnlyPreview() = PreviewWrapper { + DeviceConnectionBadge( + state = ConnectionState( + hasBleData = false, + bleKeyState = BleKeyState.NONE, + isAapConnected = true, + rssiQuality = 0f, + ), + ) +} + +@Preview2 +@Composable +private fun DeviceConnectionBadgeEmptyPreview() = PreviewWrapper { + DeviceConnectionBadge( + state = ConnectionState( + hasBleData = false, + bleKeyState = BleKeyState.NONE, + isAapConnected = false, + rssiQuality = 0f, + ), + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/SignalIndicator.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/SignalIndicator.kt new file mode 100644 index 00000000..08b39f0b --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/SignalIndicator.kt @@ -0,0 +1,119 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.LinkOff +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper + +@VisibleForTesting +internal enum class SignalLevel { DISCONNECTED, BARS_0, BARS_1, BARS_2, BARS_3, BARS_4 } + +@VisibleForTesting +internal fun signalLevelOf(signalQuality: Float, isLive: Boolean): SignalLevel { + if (!isLive) return SignalLevel.DISCONNECTED + if (!signalQuality.isFinite()) return SignalLevel.BARS_0 + val q = signalQuality.coerceIn(0f, 1f) + return when { + q >= 0.80f -> SignalLevel.BARS_4 + q >= 0.60f -> SignalLevel.BARS_3 + q >= 0.40f -> SignalLevel.BARS_2 + q >= 0.20f -> SignalLevel.BARS_1 + else -> SignalLevel.BARS_0 + } +} + +@Composable +fun SignalIndicator( + signalQuality: Float, + isLive: Boolean, + modifier: Modifier = Modifier, +) { + val level = signalLevelOf(signalQuality, isLive) + val description = when (level) { + SignalLevel.DISCONNECTED -> stringResource(R.string.signal_indicator_disconnected_cd) + SignalLevel.BARS_0 -> stringResource(R.string.signal_indicator_bars_cd, 0) + SignalLevel.BARS_1 -> stringResource(R.string.signal_indicator_bars_cd, 1) + SignalLevel.BARS_2 -> stringResource(R.string.signal_indicator_bars_cd, 2) + SignalLevel.BARS_3 -> stringResource(R.string.signal_indicator_bars_cd, 3) + SignalLevel.BARS_4 -> stringResource(R.string.signal_indicator_bars_cd, 4) + } + val tint = MaterialTheme.colorScheme.onSurfaceVariant + val rootModifier = modifier + .size(12.dp) + .semantics(mergeDescendants = true) { contentDescription = description } + + if (level == SignalLevel.DISCONNECTED) { + Icon( + imageVector = Icons.TwoTone.LinkOff, + contentDescription = null, + modifier = rootModifier, + tint = tint, + ) + return + } + + val activeBars = when (level) { + SignalLevel.BARS_0 -> 0 + SignalLevel.BARS_1 -> 1 + SignalLevel.BARS_2 -> 2 + SignalLevel.BARS_3 -> 3 + SignalLevel.BARS_4 -> 4 + SignalLevel.DISCONNECTED -> 0 // unreachable; handled above + } + val inactiveColor = tint.copy(alpha = 0.3f) + Canvas(modifier = rootModifier) { + val barCount = 4 + val gapWidth = size.width * 0.12f + val totalGap = gapWidth * (barCount - 1) + val barWidth = (size.width - totalGap) / barCount + val corner = CornerRadius(barWidth * 0.4f, barWidth * 0.4f) + for (i in 0 until barCount) { + val heightFraction = (i + 1).toFloat() / barCount + val barHeight = size.height * heightFraction + val x = i * (barWidth + gapWidth) + val y = size.height - barHeight + drawRoundRect( + color = if (i < activeBars) tint else inactiveColor, + topLeft = Offset(x, y), + size = Size(barWidth, barHeight), + cornerRadius = corner, + ) + } + } +} + +@Preview2 +@Composable +private fun SignalIndicatorPreviewsGroup() = PreviewWrapper { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SignalIndicator(signalQuality = 0.10f, isLive = true) + SignalIndicator(signalQuality = 0.35f, isLive = true) + SignalIndicator(signalQuality = 0.55f, isLive = true) + SignalIndicator(signalQuality = 0.75f, isLive = true) + SignalIndicator(signalQuality = 0.95f, isLive = true) + SignalIndicator(signalQuality = Float.NaN, isLive = true) + SignalIndicator(signalQuality = -0.3f, isLive = true) + SignalIndicator(signalQuality = 0.85f, isLive = false) + } +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/StatusChip.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/StatusChip.kt new file mode 100644 index 00000000..4914899a --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/components/StatusChip.kt @@ -0,0 +1,118 @@ +package eu.darken.capod.main.ui.overview.cards.components + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.BatteryChargingFull +import androidx.compose.material.icons.twotone.Hearing +import androidx.compose.material.icons.twotone.KeyboardVoice +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper + +@Composable +fun StatusChip( + icon: ImageVector, + label: String, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.secondaryContainer, + ) { + Row( + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + Spacer(modifier = Modifier.width(3.dp)) + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun StatusChipRow( + isCharging: Boolean, + isInEar: Boolean, + showEarDetection: Boolean, + isMicrophone: Boolean, + showMicrophone: Boolean, + chargingLabel: String, + inEarLabel: String, + microphoneLabel: String, + modifier: Modifier = Modifier, +) { + FlowRow( + modifier = modifier.animateContentSize(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (isCharging) { + StatusChip( + icon = Icons.TwoTone.BatteryChargingFull, + label = chargingLabel, + ) + } + if (showMicrophone && isMicrophone) { + StatusChip( + icon = Icons.TwoTone.KeyboardVoice, + label = microphoneLabel, + ) + } + if (showEarDetection && isInEar) { + StatusChip( + icon = Icons.TwoTone.Hearing, + label = inEarLabel, + ) + } + } +} + +@Preview2 +@Composable +private fun StatusChipChargingPreview() = PreviewWrapper { + StatusChip(icon = Icons.TwoTone.BatteryChargingFull, label = "Charging") +} + +@Preview2 +@Composable +private fun StatusChipRowAllPreview() = PreviewWrapper { + StatusChipRow( + isCharging = true, + isInEar = true, + showEarDetection = true, + isMicrophone = true, + showMicrophone = true, + chargingLabel = "Charging", + inEarLabel = "In Ear", + microphoneLabel = "Mic", + ) +} diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt index e624f0f0..a119bbce 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt @@ -3,6 +3,17 @@ package eu.darken.capod.pods.core.apple.ble import android.content.Context import android.icu.text.RelativeDateTimeFormatter import androidx.annotation.DrawableRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.Battery0Bar +import androidx.compose.material.icons.twotone.Battery1Bar +import androidx.compose.material.icons.twotone.Battery2Bar +import androidx.compose.material.icons.twotone.Battery3Bar +import androidx.compose.material.icons.twotone.Battery4Bar +import androidx.compose.material.icons.twotone.Battery5Bar +import androidx.compose.material.icons.twotone.Battery6Bar +import androidx.compose.material.icons.twotone.BatteryFull +import androidx.compose.material.icons.automirrored.twotone.BatteryUnknown +import androidx.compose.ui.graphics.vector.ImageVector import eu.darken.capod.R import java.time.Duration import java.time.Instant @@ -31,6 +42,18 @@ fun getBatteryDrawable(percent: Float?): Int = when { else -> R.drawable.ic_baseline_battery_0_bar_24 } +fun getBatteryIcon(percent: Float?): ImageVector = when { + percent == null -> Icons.AutoMirrored.TwoTone.BatteryUnknown + percent > 0.95f -> Icons.TwoTone.BatteryFull + percent > 0.80f -> Icons.TwoTone.Battery6Bar + percent > 0.65f -> Icons.TwoTone.Battery5Bar + percent > 0.50f -> Icons.TwoTone.Battery4Bar + percent > 0.35f -> Icons.TwoTone.Battery3Bar + percent > 0.20f -> Icons.TwoTone.Battery2Bar + percent > 0.05f -> Icons.TwoTone.Battery1Bar + else -> Icons.TwoTone.Battery0Bar +} + fun BlePodSnapshot.lastSeenFormatted(now: Instant): String { val formatter = RelativeDateTimeFormatter.getInstance() val duration = Duration.between(seenLastAt, now) diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpContent.kt b/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpContent.kt index a14085ad..46c7479d 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpContent.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/popup/PopUpContent.kt @@ -34,11 +34,11 @@ import eu.darken.capod.R import eu.darken.capod.common.compose.Preview2 import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.preview.MockPodDataProvider -import eu.darken.capod.main.ui.overview.cards.SignalIndicator +import eu.darken.capod.main.ui.overview.cards.components.SignalIndicator import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.pods.core.apple.PodModel import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent -import eu.darken.capod.pods.core.apple.ble.getBatteryDrawable +import eu.darken.capod.pods.core.apple.ble.getBatteryIcon import eu.darken.capod.pods.core.apple.ble.toBatteryFloat import eu.darken.capod.pods.core.apple.ble.toBatteryOrNull @@ -184,7 +184,7 @@ private fun BatteryColumn( ) } else { Icon( - painter = painterResource(getBatteryDrawable(nullablePercent)), + imageVector = getBatteryIcon(nullablePercent), contentDescription = null, modifier = Modifier.size(16.dp), ) diff --git a/app/src/test/java/eu/darken/capod/main/ui/overview/cards/SignalLevelTest.kt b/app/src/test/java/eu/darken/capod/main/ui/overview/cards/SignalLevelTest.kt index cd18ce1b..52e110e1 100644 --- a/app/src/test/java/eu/darken/capod/main/ui/overview/cards/SignalLevelTest.kt +++ b/app/src/test/java/eu/darken/capod/main/ui/overview/cards/SignalLevelTest.kt @@ -1,5 +1,8 @@ package eu.darken.capod.main.ui.overview.cards +import eu.darken.capod.main.ui.overview.cards.components.SignalLevel +import eu.darken.capod.main.ui.overview.cards.components.signalLevelOf + import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import testhelpers.BaseTest