feat(ui): Add Compose previews for all UI components and screens

Add @Preview2 multi-preview annotation (light/dark), mock data provider
with realistic device scenarios, and ~45 preview functions across cards,
screens, popup, and settings components.
This commit is contained in:
darken
2026-02-24 17:19:44 +01:00
committed by Matthias Urhahn
parent 679250f7da
commit 2ca8f686c8
30 changed files with 918 additions and 2 deletions
@@ -0,0 +1,10 @@
package eu.darken.capod.common.compose
import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
@Preview(showBackground = true, name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(showBackground = true, name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
annotation class Preview2
@@ -1,11 +1,15 @@
package eu.darken.capod.common.compose
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import eu.darken.capod.common.theming.CapodTheme
@Composable
fun PreviewWrapper(content: @Composable () -> Unit) {
CapodTheme {
content()
CapodTheme(dynamicColor = false) {
Surface(color = MaterialTheme.colorScheme.background) {
content()
}
}
}
@@ -0,0 +1,272 @@
package eu.darken.capod.common.compose.preview
import android.content.Context
import eu.darken.capod.R
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasChargeDetection
import eu.darken.capod.pods.core.HasChargeDetectionDual
import eu.darken.capod.pods.core.HasDualMicrophone
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.unknown.UnknownDevice
import eu.darken.capod.profiles.core.AppleDeviceProfile
import eu.darken.capod.profiles.core.DeviceProfile
import java.time.Instant
object MockPodDataProvider {
fun dummyScanResult(rssi: Int = -50): BleScanResult = BleScanResult(
receivedAt = Instant.now(),
address = "AA:BB:CC:DD:EE:FF",
rssi = rssi,
generatedAtNanos = 0L,
manufacturerSpecificData = mapOf(
76 to byteArrayOf(0x07, 0x19, 0x01, 0x02, 0x20, 0x75, 0xAA.toByte(), 0x30, 0x01, 0x00, 0x00, 0x2D)
),
)
// --- Dual pod scenarios ---
fun airPodsProFullCharge(): DualPodDevice = MockDualPodDevice(
_model = PodDevice.Model.AIRPODS_PRO2,
_label = "Work AirPods",
batteryLeftPodPercent = 1.0f,
batteryRightPodPercent = 1.0f,
_batteryCasePercent = 1.0f,
leftPodIcon = R.drawable.device_airpods_pro2_left,
rightPodIcon = R.drawable.device_airpods_pro2_right,
_caseIcon = R.drawable.device_airpods_pro2_case,
)
fun airPodsProMixed(): DualPodDevice = MockDualPodDevice(
_model = PodDevice.Model.AIRPODS_PRO2,
_label = "My AirPods Pro",
batteryLeftPodPercent = 0.80f,
batteryRightPodPercent = 0.45f,
_batteryCasePercent = 0.60f,
_isLeftPodCharging = true,
leftPodIcon = R.drawable.device_airpods_pro2_left,
rightPodIcon = R.drawable.device_airpods_pro2_right,
_caseIcon = R.drawable.device_airpods_pro2_case,
)
fun airPodsProLowBattery(): DualPodDevice = MockDualPodDevice(
_model = PodDevice.Model.AIRPODS_PRO2,
_label = "My AirPods Pro",
batteryLeftPodPercent = 0.10f,
batteryRightPodPercent = 0.05f,
_batteryCasePercent = null,
_isLeftPodInEar = true,
_isRightPodInEar = true,
leftPodIcon = R.drawable.device_airpods_pro2_left,
rightPodIcon = R.drawable.device_airpods_pro2_right,
_caseIcon = R.drawable.device_airpods_pro2_case,
)
fun airPodsProInCase(): DualPodDevice = MockDualPodDevice(
_model = PodDevice.Model.AIRPODS_PRO2,
_label = "My AirPods Pro",
batteryLeftPodPercent = null,
batteryRightPodPercent = null,
_batteryCasePercent = 0.90f,
leftPodIcon = R.drawable.device_airpods_pro2_left,
rightPodIcon = R.drawable.device_airpods_pro2_right,
_caseIcon = R.drawable.device_airpods_pro2_case,
)
fun airPodsGen1Wearing(): DualPodDevice = MockDualPodDevice(
_model = PodDevice.Model.AIRPODS_GEN1,
_label = "Old AirPods",
batteryLeftPodPercent = 0.70f,
batteryRightPodPercent = 0.65f,
_batteryCasePercent = 0.50f,
_isLeftPodInEar = true,
_isRightPodInEar = true,
_isLeftPodMicrophone = true,
leftPodIcon = R.drawable.device_airpods_gen1_left,
rightPodIcon = R.drawable.device_airpods_gen1_right,
_caseIcon = R.drawable.device_airpods_gen1_case,
)
fun powerBeatsPro(): DualPodDevice = MockDualPodDeviceNoCase(
_model = PodDevice.Model.POWERBEATS_PRO,
_label = "PowerBeats Pro",
batteryLeftPodPercent = 0.55f,
batteryRightPodPercent = 0.60f,
leftPodIcon = R.drawable.device_powerbeats_pro_left,
rightPodIcon = R.drawable.device_powerbeats_pro_right,
)
// --- Single pod scenarios ---
fun airPodsMax(): SinglePodDevice = MockSinglePodDevice(
_model = PodDevice.Model.AIRPODS_MAX,
_label = "AirPods Max",
batteryHeadsetPercent = 0.85f,
_isBeingWorn = true,
)
fun airPodsMaxCharging(): SinglePodDevice = MockSinglePodDevice(
_model = PodDevice.Model.AIRPODS_MAX,
_label = "AirPods Max",
batteryHeadsetPercent = 0.40f,
_isHeadsetBeingCharged = true,
)
fun beatsSolo3(): SinglePodDevice = MockSinglePodDevice(
_model = PodDevice.Model.BEATS_SOLO_3,
_label = "Beats Solo 3",
batteryHeadsetPercent = 0.70f,
)
// --- Unknown device ---
fun unknownDevice(): PodDevice = UnknownDevice(
scanResult = dummyScanResult(rssi = -70),
)
// --- Profile helpers ---
fun profile(label: String, model: PodDevice.Model): AppleDeviceProfile = AppleDeviceProfile(
label = label,
model = model,
)
// --- UpgradeInfo ---
fun fossInfo(isPro: Boolean = false): UpgradeRepo.Info = MockUpgradeInfo(
type = UpgradeRepo.Type.FOSS,
isPro = isPro,
)
fun gplayInfo(isPro: Boolean = false): UpgradeRepo.Info = MockUpgradeInfo(
type = UpgradeRepo.Type.GPLAY,
isPro = isPro,
)
}
private data class MockUpgradeInfo(
override val type: UpgradeRepo.Type,
override val isPro: Boolean,
override val upgradedAt: Instant? = null,
override val error: Throwable? = null,
) : UpgradeRepo.Info
private class MockDualPodDevice(
private val _model: PodDevice.Model,
private val _label: String,
override val batteryLeftPodPercent: Float?,
override val batteryRightPodPercent: Float?,
private val _batteryCasePercent: Float?,
private val _isCaseCharging: Boolean = false,
private val _isLeftPodCharging: Boolean = false,
private val _isRightPodCharging: Boolean = false,
private val _isLeftPodInEar: Boolean = false,
private val _isRightPodInEar: Boolean = false,
private val _isLeftPodMicrophone: Boolean = false,
private val _isRightPodMicrophone: Boolean = false,
override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left,
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
private val _caseIcon: Int = R.drawable.device_airpods_gen1_case,
rssi: Int = -50,
) : DualPodDevice, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
override val identifier: PodDevice.Id = PodDevice.Id()
override val model: PodDevice.Model = _model
override val seenLastAt: Instant = Instant.now()
override val seenFirstAt: Instant = Instant.now()
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
override val signalQuality: Float = 0.75f
override val iconRes: Int = _model.iconRes
override val meta: PodDevice.Meta = object : PodDevice.Meta {
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
}
override fun getLabel(context: Context): String = _model.label
// HasCase
override val batteryCasePercent: Float? = _batteryCasePercent
override val isCaseCharging: Boolean = _isCaseCharging
override val caseIcon: Int = _caseIcon
// HasChargeDetectionDual
override val isLeftPodCharging: Boolean = _isLeftPodCharging
override val isRightPodCharging: Boolean = _isRightPodCharging
// HasEarDetectionDual
override val isLeftPodInEar: Boolean = _isLeftPodInEar
override val isRightPodInEar: Boolean = _isRightPodInEar
// HasDualMicrophone
override val isLeftPodMicrophone: Boolean = _isLeftPodMicrophone
override val isRightPodMicrophone: Boolean = _isRightPodMicrophone
}
private class MockDualPodDeviceNoCase(
private val _model: PodDevice.Model,
private val _label: String,
override val batteryLeftPodPercent: Float?,
override val batteryRightPodPercent: Float?,
override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left,
override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right,
rssi: Int = -50,
) : DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual {
override val identifier: PodDevice.Id = PodDevice.Id()
override val model: PodDevice.Model = _model
override val seenLastAt: Instant = Instant.now()
override val seenFirstAt: Instant = Instant.now()
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
override val signalQuality: Float = 0.70f
override val iconRes: Int = _model.iconRes
override val meta: PodDevice.Meta = object : PodDevice.Meta {
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
}
override fun getLabel(context: Context): String = _model.label
// HasChargeDetectionDual
override val isLeftPodCharging: Boolean = false
override val isRightPodCharging: Boolean = false
// HasEarDetectionDual
override val isLeftPodInEar: Boolean = false
override val isRightPodInEar: Boolean = false
}
private class MockSinglePodDevice(
private val _model: PodDevice.Model,
private val _label: String,
override val batteryHeadsetPercent: Float?,
private val _isHeadsetBeingCharged: Boolean = false,
private val _isBeingWorn: Boolean = false,
rssi: Int = -50,
) : SinglePodDevice, HasChargeDetection, HasEarDetection {
override val identifier: PodDevice.Id = PodDevice.Id()
override val model: PodDevice.Model = _model
override val seenLastAt: Instant = Instant.now()
override val seenFirstAt: Instant = Instant.now()
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
override val signalQuality: Float = 0.80f
override val iconRes: Int = _model.iconRes
override val meta: PodDevice.Meta = object : PodDevice.Meta {
override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model)
}
override fun getLabel(context: Context): String = _model.label
// HasChargeDetection
override val isHeadsetBeingCharged: Boolean = _isHeadsetBeingCharged
// HasEarDetection
override val isBeingWorn: Boolean = _isBeingWorn
}
@@ -13,12 +13,16 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -94,3 +98,14 @@ fun SettingsBaseItem(
trailingContent?.invoke()
}
}
@Preview2
@Composable
private fun SettingsBaseItemPreview() = PreviewWrapper {
SettingsBaseItem(
title = "General Settings",
subtitle = "Monitor mode, scanner, notifications",
icon = Icons.TwoTone.Settings,
onClick = {},
)
}
@@ -7,6 +7,8 @@ import androidx.compose.runtime.Composable
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
@Composable
fun SettingsCategoryHeader(
@@ -21,3 +23,9 @@ fun SettingsCategoryHeader(
modifier = modifier.padding(horizontal = 16.dp, vertical = 12.dp)
)
}
@Preview2
@Composable
private fun SettingsCategoryHeaderPreview() = PreviewWrapper {
SettingsCategoryHeader(text = "Other")
}
@@ -6,6 +6,8 @@ import androidx.compose.material3.MaterialTheme
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 SettingsDivider(
@@ -16,3 +18,9 @@ fun SettingsDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.12f)
)
}
@Preview2
@Composable
private fun SettingsDividerPreview() = PreviewWrapper {
SettingsDivider()
}
@@ -11,6 +11,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Tune
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -22,6 +24,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
@Composable
fun <T> SettingsListPreferenceItem(
@@ -89,3 +93,16 @@ fun <T> SettingsListPreferenceItem(
)
}
}
@Preview2
@Composable
private fun SettingsListPreferenceItemPreview() = PreviewWrapper {
SettingsListPreferenceItem(
icon = Icons.TwoTone.Tune,
title = "Mode",
entries = listOf("Manual", "Automatic", "Always"),
selectedEntry = "Automatic",
onEntrySelected = {},
entryLabel = { it },
)
}
@@ -6,10 +6,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.twotone.VolumeUp
import androidx.compose.runtime.Composable
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 SettingsSliderItem(
@@ -55,3 +59,16 @@ fun SettingsSliderItem(
)
}
}
@Preview2
@Composable
private fun SettingsSliderItemPreview() = PreviewWrapper {
SettingsSliderItem(
icon = Icons.AutoMirrored.TwoTone.VolumeUp,
title = "Volume",
subtitle = "Adjust volume level",
value = 0.6f,
onValueChange = {},
valueLabel = { "${(it * 100).toInt()}%" },
)
}
@@ -2,10 +2,14 @@ package eu.darken.capod.common.settings
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Notifications
import androidx.compose.runtime.Composable
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 SettingsSwitchItem(
@@ -34,3 +38,27 @@ fun SettingsSwitchItem(
}
)
}
@Preview2
@Composable
private fun SettingsSwitchItemOnPreview() = PreviewWrapper {
SettingsSwitchItem(
icon = Icons.TwoTone.Notifications,
title = "Show notifications",
subtitle = "Display connection notifications",
checked = true,
onCheckedChange = {},
)
}
@Preview2
@Composable
private fun SettingsSwitchItemOffPreview() = PreviewWrapper {
SettingsSwitchItem(
icon = Icons.TwoTone.Notifications,
title = "Show notifications",
subtitle = "Display connection notifications",
checked = false,
onCheckedChange = {},
)
}
@@ -24,6 +24,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -121,3 +123,9 @@ fun OnboardingScreen(
}
}
}
@Preview2
@Composable
private fun OnboardingScreenPreview() = PreviewWrapper {
OnboardingScreen(onPrivacyPolicy = {}, onContinue = {})
}
@@ -41,6 +41,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
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.common.compose.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -276,6 +279,101 @@ private fun PodDeviceCard(device: PodDevice, showDebug: Boolean, now: Instant) {
}
}
@Preview2
@Composable
private fun OverviewScreenWithDevicesPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
permissions = emptySet(),
devices = listOf(
MockPodDataProvider.airPodsProMixed(),
MockPodDataProvider.airPodsMax(),
MockPodDataProvider.unknownDevice(),
),
isDebugMode = false,
isBluetoothEnabled = true,
profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
),
upgradeInfo = MockPodDataProvider.fossInfo(),
showUnmatchedDevices = false,
),
onRequestPermission = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
onToggleUnmatched = {},
)
}
@Preview2
@Composable
private fun OverviewScreenEmptyPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
permissions = emptySet(),
devices = emptyList(),
isDebugMode = false,
isBluetoothEnabled = true,
profiles = listOf(MockPodDataProvider.profile("My AirPods", PodDevice.Model.AIRPODS_PRO2)),
upgradeInfo = MockPodDataProvider.fossInfo(),
showUnmatchedDevices = false,
),
onRequestPermission = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
onToggleUnmatched = {},
)
}
@Preview2
@Composable
private fun OverviewScreenNoProfilesPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
permissions = emptySet(),
devices = emptyList(),
isDebugMode = false,
isBluetoothEnabled = true,
profiles = emptyList(),
upgradeInfo = MockPodDataProvider.gplayInfo(),
showUnmatchedDevices = false,
),
onRequestPermission = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
onToggleUnmatched = {},
)
}
@Preview2
@Composable
private fun OverviewScreenBluetoothOffPreview() = PreviewWrapper {
OverviewScreen(
state = OverviewViewModel.State(
now = Instant.now(),
permissions = emptySet(),
devices = emptyList(),
isDebugMode = false,
isBluetoothEnabled = false,
profiles = listOf(MockPodDataProvider.profile("My AirPods", PodDevice.Model.AIRPODS_PRO2)),
upgradeInfo = MockPodDataProvider.fossInfo(isPro = true),
showUnmatchedDevices = false,
),
onRequestPermission = {},
onManageDevices = {},
onSettings = {},
onUpgrade = {},
onToggleUnmatched = {},
)
}
@Composable
private fun ToolbarTitle(upgradeInfo: UpgradeRepo.Info) {
val appName = stringResource(R.string.app_name)
@@ -13,6 +13,8 @@ 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
@Composable
fun BluetoothDisabledCard() {
@@ -38,3 +40,9 @@ fun BluetoothDisabledCard() {
}
}
}
@Preview2
@Composable
private fun BluetoothDisabledCardPreview() = PreviewWrapper {
BluetoothDisabledCard()
}
@@ -46,6 +46,9 @@ import eu.darken.capod.pods.core.firstSeenFormatted
import eu.darken.capod.pods.core.formatBatteryPercent
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.pods.core.lastSeenFormatted
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import java.time.Duration
import java.time.Instant
@@ -246,6 +249,42 @@ fun DualPodsCard(
}
}
@Preview2
@Composable
private fun DualPodsCardFullChargePreview() = PreviewWrapper {
DualPodsCard(device = MockPodDataProvider.airPodsProFullCharge(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun DualPodsCardMixedBatteryPreview() = PreviewWrapper {
DualPodsCard(device = MockPodDataProvider.airPodsProMixed(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun DualPodsCardLowBatteryPreview() = PreviewWrapper {
DualPodsCard(device = MockPodDataProvider.airPodsProLowBattery(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun DualPodsCardInCasePreview() = PreviewWrapper {
DualPodsCard(device = MockPodDataProvider.airPodsProInCase(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun DualPodsCardDebugPreview() = PreviewWrapper {
DualPodsCard(device = MockPodDataProvider.airPodsProMixed(), showDebug = true, now = Instant.now())
}
@Preview2
@Composable
private fun DualPodsCardNoCasePreview() = PreviewWrapper {
DualPodsCard(device = MockPodDataProvider.powerBeatsPro(), showDebug = false, now = Instant.now())
}
@Composable
private fun BatteryRow(
label: String,
@@ -13,6 +13,8 @@ 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
@Composable
fun MonitoringActiveCard() {
@@ -38,3 +40,9 @@ fun MonitoringActiveCard() {
}
}
}
@Preview2
@Composable
private fun MonitoringActiveCardPreview() = PreviewWrapper {
MonitoringActiveCard()
}
@@ -18,6 +18,8 @@ import androidx.compose.material.icons.twotone.DevicesOther
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
@Composable
fun NoProfilesCard(onManageDevices: () -> Unit) {
@@ -57,3 +59,9 @@ fun NoProfilesCard(onManageDevices: () -> Unit) {
}
}
}
@Preview2
@Composable
private fun NoProfilesCardPreview() = PreviewWrapper {
NoProfilesCard(onManageDevices = {})
}
@@ -15,6 +15,8 @@ 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.common.permissions.Permission
@Composable
@@ -53,3 +55,9 @@ fun PermissionCard(
}
}
}
@Preview2
@Composable
private fun PermissionCardPreview() = PreviewWrapper {
PermissionCard(permission = Permission.BLUETOOTH_SCAN, onRequest = {})
}
@@ -34,6 +34,9 @@ import eu.darken.capod.pods.core.firstSeenFormatted
import eu.darken.capod.pods.core.formatBatteryPercent
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.pods.core.lastSeenFormatted
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import java.time.Duration
import java.time.Instant
@@ -173,3 +176,21 @@ fun SinglePodsCard(
}
}
}
@Preview2
@Composable
private fun SinglePodsCardWearingPreview() = PreviewWrapper {
SinglePodsCard(device = MockPodDataProvider.airPodsMax(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun SinglePodsCardChargingPreview() = PreviewWrapper {
SinglePodsCard(device = MockPodDataProvider.airPodsMaxCharging(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun SinglePodsCardDebugPreview() = PreviewWrapper {
SinglePodsCard(device = MockPodDataProvider.beatsSolo3(), showDebug = true, now = Instant.now())
}
@@ -25,6 +25,9 @@ import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.pods.core.lastSeenFormatted
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.compose.preview.MockPodDataProvider
import java.time.Instant
@Composable
@@ -103,3 +106,15 @@ fun UnknownPodDeviceCard(
}
}
}
@Preview2
@Composable
private fun UnknownPodDeviceCardPreview() = PreviewWrapper {
UnknownPodDeviceCard(device = MockPodDataProvider.unknownDevice(), showDebug = false, now = Instant.now())
}
@Preview2
@Composable
private fun UnknownPodDeviceCardDebugPreview() = PreviewWrapper {
UnknownPodDeviceCard(device = MockPodDataProvider.unknownDevice(), showDebug = true, now = Instant.now())
}
@@ -15,6 +15,8 @@ import androidx.compose.ui.res.pluralStringResource
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
@Composable
fun UnmatchedDevicesCard(
@@ -52,3 +54,15 @@ fun UnmatchedDevicesCard(
}
}
}
@Preview2
@Composable
private fun UnmatchedDevicesCardCollapsedPreview() = PreviewWrapper {
UnmatchedDevicesCard(count = 3, isExpanded = false, onToggle = {})
}
@Preview2
@Composable
private fun UnmatchedDevicesCardExpandedPreview() = PreviewWrapper {
UnmatchedDevicesCard(count = 3, isExpanded = true, onToggle = {})
}
@@ -24,6 +24,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.compose.waitForState
@@ -172,3 +174,21 @@ fun SettingsScreen(
}
}
}
@Preview2
@Composable
private fun SettingsScreenPreview() = PreviewWrapper {
SettingsScreen(
state = SettingsViewModel.State(sponsorUrl = "https://example.com"),
onNavigateUp = {},
onGeneralSettings = {},
onDeviceManager = {},
onReactions = {},
onSupport = {},
onChangelog = {},
onHelpTranslate = {},
onAcknowledgements = {},
onPrivacyPolicy = {},
onSponsor = {},
)
}
@@ -15,6 +15,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsBaseItem
@@ -162,3 +164,9 @@ fun AcknowledgementsScreen(
}
}
}
@Preview2
@Composable
private fun AcknowledgementsScreenPreview() = PreviewWrapper {
AcknowledgementsScreen(onNavigateUp = {}, onOpenUrl = {})
}
@@ -40,6 +40,8 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.bluetooth.ScannerMode
import eu.darken.capod.common.compose.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
@@ -244,6 +246,31 @@ fun GeneralSettingsScreen(
}
}
@Preview2
@Composable
private fun GeneralSettingsScreenPreview() = PreviewWrapper {
GeneralSettingsScreen(
state = GeneralSettingsViewModel.State(
monitorMode = MonitorMode.AUTOMATIC,
scannerMode = ScannerMode.BALANCED,
showConnectedNotification = true,
keepNotificationAfterDisconnect = false,
isOffloadedFilteringDisabled = false,
isOffloadedBatchingDisabled = false,
useIndirectScanResultCallback = false,
),
onNavigateUp = {},
onMonitorModeSelected = {},
onScannerModeSelected = {},
onShowConnectedNotificationChanged = {},
onKeepNotificationAfterDisconnectChanged = {},
onDebugSettings = {},
onOffloadedFilteringDisabledChanged = {},
onOffloadedBatchingDisabledChanged = {},
onUseIndirectScanResultCallbackChanged = {},
)
}
@Composable
private fun <T> ListPreferenceDialog(
title: String,
@@ -21,6 +21,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -118,3 +120,19 @@ fun DebugSettingsScreen(
}
}
}
@Preview2
@Composable
private fun DebugSettingsScreenPreview() = PreviewWrapper {
DebugSettingsScreen(
state = DebugSettingsViewModel.State(
isDebugModeEnabled = true,
showFakeData = false,
showUnfiltered = false,
),
onNavigateUp = {},
onDebugModeChanged = {},
onShowFakeDataChanged = {},
onShowUnfilteredChanged = {},
)
}
@@ -20,6 +20,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
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.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -125,3 +127,16 @@ fun SupportScreen(
}
}
}
@Preview2
@Composable
private fun SupportScreenPreview() = PreviewWrapper {
SupportScreen(
state = SupportViewModel.State(isRecording = false, currentLogPath = null),
onNavigateUp = {},
onDiscord = {},
onIssueTracker = {},
onTroubleShooter = {},
onDebugLogToggle = {},
)
}
@@ -71,6 +71,9 @@ import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.isVisible
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.pods.core.PodDevice
import eu.darken.capod.profiles.core.DeviceProfile
@@ -650,6 +653,68 @@ private fun HexColorInput(
)
}
@Preview2
@Composable
private fun WidgetConfigurationScreenPreview() = PreviewWrapper {
val profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
)
WidgetConfigurationScreen(
state = WidgetConfigurationViewModel.State(
profiles = profiles,
selectedProfile = profiles.first().id,
isPro = true,
theme = WidgetTheme.DEFAULT,
activePreset = WidgetTheme.Preset.MATERIAL_YOU,
isCustomMode = false,
),
onSelectProfile = {},
onSelectPreset = {},
onEnterCustomMode = { _, _ -> },
onSetBackgroundColor = {},
onSetForegroundColor = {},
onSetBackgroundAlpha = {},
onSetShowDeviceLabel = {},
onReset = {},
onConfirm = {},
onCancel = {},
)
}
@Preview2
@Composable
private fun WidgetConfigurationScreenCustomPreview() = PreviewWrapper {
val profiles = listOf(
MockPodDataProvider.profile("My AirPods Pro", PodDevice.Model.AIRPODS_PRO2),
)
WidgetConfigurationScreen(
state = WidgetConfigurationViewModel.State(
profiles = profiles,
selectedProfile = profiles.first().id,
isPro = true,
theme = WidgetTheme(
backgroundColor = 0xFF1565C0.toInt(),
foregroundColor = 0xFFFFFFFF.toInt(),
backgroundAlpha = 200,
showDeviceLabel = true,
),
activePreset = null,
isCustomMode = true,
),
onSelectProfile = {},
onSelectPreset = {},
onEnterCustomMode = { _, _ -> },
onSetBackgroundColor = {},
onSetForegroundColor = {},
onSetBackgroundAlpha = {},
onSetShowDeviceLabel = {},
onReset = {},
onConfirm = {},
onCancel = {},
)
}
private val SWATCH_COLORS = intArrayOf(
0xFFF44336.toInt(), // Red
0xFFE91E63.toInt(), // Pink
@@ -36,9 +36,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.common.compose.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.profiles.core.DeviceProfile
@Composable
@@ -121,6 +125,34 @@ fun DeviceManagerScreen(
}
}
@Preview2
@Composable
private fun DeviceManagerScreenWithProfilesPreview() = PreviewWrapper {
DeviceManagerScreen(
state = DeviceManagerViewModel.State(
profiles = listOf(
MockPodDataProvider.profile("Work AirPods", PodDevice.Model.AIRPODS_PRO2),
MockPodDataProvider.profile("AirPods Max", PodDevice.Model.AIRPODS_MAX),
MockPodDataProvider.profile("Gym Beats", PodDevice.Model.POWERBEATS_PRO),
),
),
onBack = {},
onAddDevice = {},
onEditProfile = {},
)
}
@Preview2
@Composable
private fun DeviceManagerScreenEmptyPreview() = PreviewWrapper {
DeviceManagerScreen(
state = DeviceManagerViewModel.State(profiles = emptyList()),
onBack = {},
onAddDevice = {},
onEditProfile = {},
)
}
@Composable
private fun EmptyState(
modifier: Modifier = Modifier,
@@ -48,6 +48,8 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.compose.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
@@ -548,6 +550,66 @@ private fun UnsavedChangesDialog(
)
}
@Preview2
@Composable
private fun DeviceProfileCreationScreenNewPreview() = PreviewWrapper {
DeviceProfileCreationScreen(
state = DeviceProfileCreationViewModel.State(
isEditMode = false,
name = "",
nameError = null,
selectedModel = null,
availableModels = PodDevice.Model.entries.filter { it != PodDevice.Model.UNKNOWN },
identityKey = null,
encryptionKey = null,
selectedDevice = null,
bondedDevices = emptyList(),
minimumSignalQuality = 0.15f,
canSave = false,
),
onBack = {},
onSave = {},
onDelete = {},
onNameChange = {},
onModelChange = {},
onDeviceChange = {},
onIdentityKeyChange = {},
onEncryptionKeyChange = {},
onSignalQualityChange = {},
onKeyGuide = {},
)
}
@Preview2
@Composable
private fun DeviceProfileCreationScreenEditPreview() = PreviewWrapper {
DeviceProfileCreationScreen(
state = DeviceProfileCreationViewModel.State(
isEditMode = true,
name = "My AirPods Pro",
nameError = null,
selectedModel = PodDevice.Model.AIRPODS_PRO2,
availableModels = PodDevice.Model.entries.filter { it != PodDevice.Model.UNKNOWN },
identityKey = null,
encryptionKey = null,
selectedDevice = null,
bondedDevices = emptyList(),
minimumSignalQuality = 0.25f,
canSave = true,
),
onBack = {},
onSave = {},
onDelete = {},
onNameChange = {},
onModelChange = {},
onDeviceChange = {},
onIdentityKeyChange = {},
onEncryptionKeyChange = {},
onSignalQualityChange = {},
onKeyGuide = {},
)
}
@Composable
private fun DeleteConfirmationDialog(
onDelete: () -> Unit,
@@ -41,6 +41,8 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -267,3 +269,28 @@ fun ReactionSettingsScreen(
)
}
}
@Preview2
@Composable
private fun ReactionSettingsScreenPreview() = PreviewWrapper {
ReactionSettingsScreen(
state = ReactionSettingsViewModel.State(
isPro = true,
onePodMode = false,
autoPlay = true,
autoPause = true,
autoConnect = false,
autoConnectCondition = AutoConnectCondition.WHEN_SEEN,
showPopUpOnCaseOpen = true,
showPopUpOnConnection = false,
),
onNavigateUp = {},
onOnePodModeChanged = {},
onAutoPlayChanged = {},
onAutoPauseChanged = {},
onAutoConnectChanged = {},
onAutoConnectConditionSelected = {},
onShowPopUpOnCaseOpenChanged = {},
onShowPopUpOnConnectionChanged = {},
)
}
@@ -30,6 +30,9 @@ import androidx.compose.ui.text.style.TextAlign
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.common.compose.preview.MockPodDataProvider
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.PodDevice
@@ -192,3 +195,21 @@ private fun BatteryColumn(
}
}
}
@Preview2
@Composable
private fun PopUpContentDualPodPreview() = PreviewWrapper {
PopUpContent(device = MockPodDataProvider.airPodsProMixed(), onClose = {})
}
@Preview2
@Composable
private fun PopUpContentDualPodNoCasePreview() = PreviewWrapper {
PopUpContent(device = MockPodDataProvider.powerBeatsPro(), onClose = {})
}
@Preview2
@Composable
private fun PopUpContentSinglePodPreview() = PreviewWrapper {
PopUpContent(device = MockPodDataProvider.airPodsMax(), onClose = {})
}
@@ -30,6 +30,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.waitForState
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@@ -214,3 +216,16 @@ private fun ResultContent(
style = MaterialTheme.typography.bodySmall,
)
}
@Preview2
@Composable
private fun TroubleShooterScreenPreview() = PreviewWrapper {
TroubleShooterScreen(
state = TroubleShooterViewModel.State(
bleState = TroubleShooterViewModel.BleState.Intro(),
),
onStart = {},
onRetry = {},
onNavigateUp = {},
)
}