feat(screenshots): Set up localized Play Store screenshot pipeline

- Add Compose screenshot test infrastructure (build config, screenshotTest source set)

- Add 7 localized screenshots covering all key app screens

- Device spec set to Pixel 8 (1080x2400, 428dpi); batch size 4 for 1080p memory budget

- Add screen title heading and fix appearance card header layout in WidgetConfigurationScreen

- Balance padding on requires-Pro notice in widget config bottom bar
This commit is contained in:
darken
2026-02-25 22:57:34 +01:00
committed by Matthias Urhahn
parent c4e58917da
commit fc9bcafae9
21 changed files with 973 additions and 60 deletions
@@ -18,10 +18,13 @@ import eu.darken.capod.profiles.core.AppleDeviceProfile
import eu.darken.capod.profiles.core.DeviceProfile
import java.time.Instant
/** Fixed timestamp for deterministic preview/screenshot rendering. */
val MOCK_NOW: Instant = Instant.parse("2025-06-15T10:30:00Z")
object MockPodDataProvider {
fun dummyScanResult(rssi: Int = -50): BleScanResult = BleScanResult(
receivedAt = Instant.now(),
receivedAt = MOCK_NOW,
address = "AA:BB:CC:DD:EE:FF",
rssi = rssi,
generatedAtNanos = 0L,
@@ -177,8 +180,8 @@ private class MockDualPodDevice(
) : 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 seenLastAt: Instant = MOCK_NOW
override val seenFirstAt: Instant = MOCK_NOW
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
@@ -219,8 +222,8 @@ private class MockDualPodDeviceNoCase(
) : 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 seenLastAt: Instant = MOCK_NOW
override val seenFirstAt: Instant = MOCK_NOW
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
@@ -251,8 +254,8 @@ private class MockSinglePodDevice(
) : 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 seenLastAt: Instant = MOCK_NOW
override val seenFirstAt: Instant = MOCK_NOW
override val seenCounter: Int = 5
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
override val reliability: Float = 1.0f
@@ -121,6 +121,14 @@ fun WidgetConfigurationScreen(
.verticalScroll(rememberScrollState())
.padding(top = 24.dp, bottom = 16.dp),
) {
Text(
text = stringResource(R.string.widget_config_screen_title),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(start = screenHPad, end = screenHPad, bottom = 16.dp),
)
// Card A — Select Device
Card(
modifier = Modifier
@@ -170,16 +178,15 @@ fun WidgetConfigurationScreen(
) {
Column(modifier = Modifier.padding(cardPad)) {
// Appearance header + reset
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(R.string.widget_config_appearance_label),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.weight(1f),
)
TextButton(onClick = onReset) {
TextButton(
onClick = onReset,
modifier = Modifier.align(Alignment.End),
) {
Text(text = stringResource(R.string.widget_config_reset_label))
}
}
@@ -327,7 +334,7 @@ fun WidgetConfigurationScreen(
color = MaterialTheme.colorScheme.error,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp),
.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp),
)
}
@@ -496,11 +503,13 @@ private fun WidgetPreview(
// Inner preview content
LayoutInflater.from(ctx).inflate(R.layout.widget_config_preview, clipWrapper, true)
container.addView(clipWrapper, android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
android.view.Gravity.CENTER,
))
container.addView(
clipWrapper, android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
android.view.Gravity.CENTER,
)
)
container
},
@@ -744,7 +753,14 @@ private fun HexColorInput(
) {
val hexString = color?.let { String.format("%06X", 0xFFFFFF and it) } ?: ""
var textFieldValue by remember { mutableStateOf(TextFieldValue(text = hexString, selection = TextRange(hexString.length))) }
var textFieldValue by remember {
mutableStateOf(
TextFieldValue(
text = hexString,
selection = TextRange(hexString.length)
)
)
}
// Sync from external color changes (e.g. swatch clicks) only when content genuinely differs
LaunchedEffect(hexString) {
+1
View File
@@ -117,6 +117,7 @@
<string name="translators_thanks_description">darken</string>
<string name="widget_description">A widget showing the last known device status.</string>
<string name="widget_config_screen_title">Widget Configuration</string>
<string name="widget_configuration_title">Select Device</string>
<string name="widget_configuration_description">Choose which device profile this widget should display.</string>
<string name="common_feature_requires_pro_msg">This feature requires CAPod Pro.</string>