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
@@ -14,4 +14,6 @@
|
|||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
/fastlane/Appfile
|
/fastlane/Appfile
|
||||||
/fastlane/README.md
|
/fastlane/README.md
|
||||||
.kotlin
|
.kotlin
|
||||||
|
# Screenshot test reference images (ephemeral, regenerated on demand)
|
||||||
|
app/src/screenshotTest*/reference/
|
||||||
@@ -3,10 +3,10 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("kotlin-kapt")
|
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
|
id("com.android.compose.screenshot") version "0.0.1-alpha13"
|
||||||
}
|
}
|
||||||
apply(plugin = "dagger.hilt.android.plugin")
|
apply(plugin = "dagger.hilt.android.plugin")
|
||||||
|
|
||||||
@@ -95,6 +95,8 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
experimentalProperties["android.experimental.enableScreenshotTest"] = true
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
@@ -193,4 +195,8 @@ dependencies {
|
|||||||
|
|
||||||
"gplayImplementation"("com.android.billingclient:billing:8.0.0")
|
"gplayImplementation"("com.android.billingclient:billing:8.0.0")
|
||||||
"gplayImplementation"("com.android.billingclient:billing-ktx:8.0.0")
|
"gplayImplementation"("com.android.billingclient:billing-ktx:8.0.0")
|
||||||
|
|
||||||
|
"screenshotTestImplementation"(platform("androidx.compose:compose-bom:${Versions.Compose.bom}"))
|
||||||
|
"screenshotTestImplementation"("com.android.tools.screenshot:screenshot-validation-api:0.0.1-alpha13")
|
||||||
|
"screenshotTestImplementation"("androidx.compose.ui:ui-tooling")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
package eu.darken.capod.screenshots
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
|
import eu.darken.capod.main.ui.widget.WidgetConfigurationScreen
|
||||||
|
import eu.darken.capod.main.ui.widget.WidgetConfigurationViewModel
|
||||||
|
import eu.darken.capod.main.ui.widget.WidgetTheme
|
||||||
|
import eu.darken.capod.common.compose.preview.MOCK_NOW
|
||||||
|
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||||
|
import eu.darken.capod.common.theming.CapodTheme
|
||||||
|
import eu.darken.capod.main.ui.overview.OverviewScreen
|
||||||
|
import eu.darken.capod.main.ui.overview.OverviewViewModel
|
||||||
|
import eu.darken.capod.main.ui.settings.SettingsScreen
|
||||||
|
import eu.darken.capod.main.ui.settings.SettingsViewModel
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.profiles.ui.DeviceManagerScreen
|
||||||
|
import eu.darken.capod.profiles.ui.DeviceManagerViewModel
|
||||||
|
import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationScreen
|
||||||
|
import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationViewModel
|
||||||
|
import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition
|
||||||
|
import eu.darken.capod.reaction.ui.ReactionSettingsScreen
|
||||||
|
import eu.darken.capod.reaction.ui.ReactionSettingsViewModel
|
||||||
|
|
||||||
|
internal const val DS = "spec:width=1080px,height=2400px,dpi=428"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DashboardContent() = PreviewWrapper {
|
||||||
|
OverviewScreen(
|
||||||
|
state = OverviewViewModel.State(
|
||||||
|
now = MOCK_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.gplayInfo(isPro = true),
|
||||||
|
showUnmatchedDevices = false,
|
||||||
|
),
|
||||||
|
onRequestPermission = {},
|
||||||
|
onManageDevices = {},
|
||||||
|
onSettings = {},
|
||||||
|
onUpgrade = {},
|
||||||
|
onToggleUnmatched = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DeviceProfilesContent() = 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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun AddProfileContent() = 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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsIndexContent() = PreviewWrapper {
|
||||||
|
SettingsScreen(
|
||||||
|
state = SettingsViewModel.State(sponsorUrl = "https://example.com"),
|
||||||
|
onNavigateUp = {},
|
||||||
|
onGeneralSettings = {},
|
||||||
|
onDeviceManager = {},
|
||||||
|
onReactions = {},
|
||||||
|
onSupport = {},
|
||||||
|
onChangelog = {},
|
||||||
|
onHelpTranslate = {},
|
||||||
|
onAcknowledgements = {},
|
||||||
|
onPrivacyPolicy = {},
|
||||||
|
onSponsor = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ReactionSettingsContent() = 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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun WidgetConfigurationContent() = 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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun HomescreenWidgetContent() {
|
||||||
|
CapodTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color(0xFF1A237E),
|
||||||
|
Color(0xFF0D47A1),
|
||||||
|
Color(0xFF006064),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
WidgetDualCompact()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WidgetDualCompact() {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
WidgetPodRow(
|
||||||
|
icon = R.drawable.device_airpods_pro2_left,
|
||||||
|
percent = 85,
|
||||||
|
charging = false,
|
||||||
|
inEar = true,
|
||||||
|
)
|
||||||
|
WidgetPodRow(
|
||||||
|
icon = R.drawable.device_airpods_pro2_right,
|
||||||
|
percent = 92,
|
||||||
|
charging = true,
|
||||||
|
inEar = false,
|
||||||
|
)
|
||||||
|
WidgetCaseRow(
|
||||||
|
icon = R.drawable.device_airpods_pro2_case,
|
||||||
|
percent = 100,
|
||||||
|
charging = false,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "My AirPods Pro",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WidgetPodRow(
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
percent: Int,
|
||||||
|
charging: Boolean,
|
||||||
|
inEar: Boolean,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "$percent%",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
)
|
||||||
|
if (charging) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_baseline_power_24),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (inEar) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_baseline_hearing_24),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WidgetCaseRow(
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
percent: Int,
|
||||||
|
charging: Boolean,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "$percent%",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
)
|
||||||
|
if (charging) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_baseline_power_24),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "1 - Dashboard Light", locale = "en", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewDashboardLight() = DashboardContent()
|
||||||
|
|
||||||
|
@Preview(name = "2 - Dashboard Dark", locale = "en", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewDashboardDark() = DashboardContent()
|
||||||
|
|
||||||
|
@Preview(name = "3 - Device Profiles", locale = "en", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewDeviceProfiles() = DeviceProfilesContent()
|
||||||
|
|
||||||
|
@Preview(name = "4 - Add Profile", locale = "en", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewAddProfile() = AddProfileContent()
|
||||||
|
|
||||||
|
@Preview(name = "5 - Settings", locale = "en", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewSettingsIndex() = SettingsIndexContent()
|
||||||
|
|
||||||
|
@Preview(name = "6 - Reaction Settings", locale = "en", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewReactionSettings() = ReactionSettingsContent()
|
||||||
|
|
||||||
|
@Preview(name = "7 - Homescreen Widget", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewHomescreenWidget() = HomescreenWidgetContent()
|
||||||
|
|
||||||
|
@Preview(name = "7 - Widget Configuration", locale = "en", device = DS, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewWidgetConfiguration() = WidgetConfigurationContent()
|
||||||
@@ -18,10 +18,13 @@ import eu.darken.capod.profiles.core.AppleDeviceProfile
|
|||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
|
/** Fixed timestamp for deterministic preview/screenshot rendering. */
|
||||||
|
val MOCK_NOW: Instant = Instant.parse("2025-06-15T10:30:00Z")
|
||||||
|
|
||||||
object MockPodDataProvider {
|
object MockPodDataProvider {
|
||||||
|
|
||||||
fun dummyScanResult(rssi: Int = -50): BleScanResult = BleScanResult(
|
fun dummyScanResult(rssi: Int = -50): BleScanResult = BleScanResult(
|
||||||
receivedAt = Instant.now(),
|
receivedAt = MOCK_NOW,
|
||||||
address = "AA:BB:CC:DD:EE:FF",
|
address = "AA:BB:CC:DD:EE:FF",
|
||||||
rssi = rssi,
|
rssi = rssi,
|
||||||
generatedAtNanos = 0L,
|
generatedAtNanos = 0L,
|
||||||
@@ -177,8 +180,8 @@ private class MockDualPodDevice(
|
|||||||
) : DualPodDevice, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
|
) : DualPodDevice, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||||
override val model: PodDevice.Model = _model
|
override val model: PodDevice.Model = _model
|
||||||
override val seenLastAt: Instant = Instant.now()
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = Instant.now()
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
override val seenCounter: Int = 5
|
override val seenCounter: Int = 5
|
||||||
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
|
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
|
||||||
override val reliability: Float = 1.0f
|
override val reliability: Float = 1.0f
|
||||||
@@ -219,8 +222,8 @@ private class MockDualPodDeviceNoCase(
|
|||||||
) : DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual {
|
) : DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||||
override val model: PodDevice.Model = _model
|
override val model: PodDevice.Model = _model
|
||||||
override val seenLastAt: Instant = Instant.now()
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = Instant.now()
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
override val seenCounter: Int = 5
|
override val seenCounter: Int = 5
|
||||||
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
|
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
|
||||||
override val reliability: Float = 1.0f
|
override val reliability: Float = 1.0f
|
||||||
@@ -251,8 +254,8 @@ private class MockSinglePodDevice(
|
|||||||
) : SinglePodDevice, HasChargeDetection, HasEarDetection {
|
) : SinglePodDevice, HasChargeDetection, HasEarDetection {
|
||||||
override val identifier: PodDevice.Id = PodDevice.Id()
|
override val identifier: PodDevice.Id = PodDevice.Id()
|
||||||
override val model: PodDevice.Model = _model
|
override val model: PodDevice.Model = _model
|
||||||
override val seenLastAt: Instant = Instant.now()
|
override val seenLastAt: Instant = MOCK_NOW
|
||||||
override val seenFirstAt: Instant = Instant.now()
|
override val seenFirstAt: Instant = MOCK_NOW
|
||||||
override val seenCounter: Int = 5
|
override val seenCounter: Int = 5
|
||||||
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
|
override val scanResult: BleScanResult = MockPodDataProvider.dummyScanResult(rssi)
|
||||||
override val reliability: Float = 1.0f
|
override val reliability: Float = 1.0f
|
||||||
|
|||||||
@@ -121,6 +121,14 @@ fun WidgetConfigurationScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(top = 24.dp, bottom = 16.dp),
|
.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 A — Select Device
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -170,16 +178,15 @@ fun WidgetConfigurationScreen(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(cardPad)) {
|
Column(modifier = Modifier.padding(cardPad)) {
|
||||||
// Appearance header + reset
|
// Appearance header + reset
|
||||||
Row(
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.widget_config_appearance_label),
|
text = stringResource(R.string.widget_config_appearance_label),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
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))
|
Text(text = stringResource(R.string.widget_config_reset_label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,7 +334,7 @@ fun WidgetConfigurationScreen(
|
|||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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
|
// Inner preview content
|
||||||
LayoutInflater.from(ctx).inflate(R.layout.widget_config_preview, clipWrapper, true)
|
LayoutInflater.from(ctx).inflate(R.layout.widget_config_preview, clipWrapper, true)
|
||||||
|
|
||||||
container.addView(clipWrapper, android.widget.FrameLayout.LayoutParams(
|
container.addView(
|
||||||
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
|
clipWrapper, android.widget.FrameLayout.LayoutParams(
|
||||||
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
|
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
android.view.Gravity.CENTER,
|
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
))
|
android.view.Gravity.CENTER,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
container
|
container
|
||||||
},
|
},
|
||||||
@@ -744,7 +753,14 @@ private fun HexColorInput(
|
|||||||
) {
|
) {
|
||||||
val hexString = color?.let { String.format("%06X", 0xFFFFFF and it) } ?: ""
|
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
|
// Sync from external color changes (e.g. swatch clicks) only when content genuinely differs
|
||||||
LaunchedEffect(hexString) {
|
LaunchedEffect(hexString) {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
<string name="translators_thanks_description">darken</string>
|
<string name="translators_thanks_description">darken</string>
|
||||||
|
|
||||||
<string name="widget_description">A widget showing the last known device status.</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_title">Select Device</string>
|
||||||
<string name="widget_configuration_description">Choose which device profile this widget should display.</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>
|
<string name="common_feature_requires_pro_msg">This feature requires CAPod Pro.</string>
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package eu.darken.capod.screenshots
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-preview annotation generating one preview per Play Store-supported locale (light mode).
|
||||||
|
* Each [name] is the fastlane metadata directory name for direct use in the copy script.
|
||||||
|
*/
|
||||||
|
@Preview(locale = "en", name = "en-US", device = DS)
|
||||||
|
@Preview(locale = "af", name = "af", device = DS)
|
||||||
|
@Preview(locale = "am", name = "am", device = DS)
|
||||||
|
@Preview(locale = "ar", name = "ar", device = DS)
|
||||||
|
@Preview(locale = "az", name = "az-AZ", device = DS)
|
||||||
|
@Preview(locale = "be", name = "be", device = DS)
|
||||||
|
@Preview(locale = "bg", name = "bg", device = DS)
|
||||||
|
@Preview(locale = "bn-BD", name = "bn-BD", device = DS)
|
||||||
|
@Preview(locale = "ca", name = "ca", device = DS)
|
||||||
|
@Preview(locale = "cs", name = "cs-CZ", device = DS)
|
||||||
|
@Preview(locale = "da", name = "da-DK", device = DS)
|
||||||
|
@Preview(locale = "de", name = "de-DE", device = DS)
|
||||||
|
@Preview(locale = "el", name = "el-GR", device = DS)
|
||||||
|
@Preview(locale = "es", name = "es-ES", device = DS)
|
||||||
|
@Preview(locale = "es-MX", name = "es-419", device = DS)
|
||||||
|
@Preview(locale = "et-EE", name = "et", device = DS)
|
||||||
|
@Preview(locale = "eu-ES", name = "eu-ES", device = DS)
|
||||||
|
@Preview(locale = "fa", name = "fa", device = DS)
|
||||||
|
@Preview(locale = "fi", name = "fi-FI", device = DS)
|
||||||
|
@Preview(locale = "fil", name = "fil", device = DS)
|
||||||
|
@Preview(locale = "fr", name = "fr-FR", device = DS)
|
||||||
|
@Preview(locale = "gl-ES", name = "gl-ES", device = DS)
|
||||||
|
@Preview(locale = "hi-IN", name = "hi-IN", device = DS)
|
||||||
|
@Preview(locale = "hr", name = "hr", device = DS)
|
||||||
|
@Preview(locale = "hu", name = "hu-HU", device = DS)
|
||||||
|
@Preview(locale = "hy-AM", name = "hy-AM", device = DS)
|
||||||
|
@Preview(locale = "in", name = "id", device = DS)
|
||||||
|
@Preview(locale = "is", name = "is-IS", device = DS)
|
||||||
|
@Preview(locale = "it", name = "it-IT", device = DS)
|
||||||
|
@Preview(locale = "iw", name = "iw-IL", device = DS)
|
||||||
|
@Preview(locale = "ja", name = "ja-JP", device = DS)
|
||||||
|
@Preview(locale = "ka-GE", name = "ka-GE", device = DS)
|
||||||
|
@Preview(locale = "km-KH", name = "km-KH", device = DS)
|
||||||
|
@Preview(locale = "kn-IN", name = "kn-IN", device = DS)
|
||||||
|
@Preview(locale = "ko", name = "ko-KR", device = DS)
|
||||||
|
@Preview(locale = "ky-KG", name = "ky-KG", device = DS)
|
||||||
|
@Preview(locale = "lo-LA", name = "lo-LA", device = DS)
|
||||||
|
@Preview(locale = "lt", name = "lt", device = DS)
|
||||||
|
@Preview(locale = "lv", name = "lv", device = DS)
|
||||||
|
@Preview(locale = "mk-MK", name = "mk-MK", device = DS)
|
||||||
|
@Preview(locale = "ml-IN", name = "ml-IN", device = DS)
|
||||||
|
@Preview(locale = "mn-MN", name = "mn-MN", device = DS)
|
||||||
|
@Preview(locale = "mr-IN", name = "mr-IN", device = DS)
|
||||||
|
@Preview(locale = "ms", name = "ms", device = DS)
|
||||||
|
@Preview(locale = "my-MM", name = "my-MM", device = DS)
|
||||||
|
@Preview(locale = "nb", name = "no-NO", device = DS)
|
||||||
|
@Preview(locale = "ne-NP", name = "ne-NP", device = DS)
|
||||||
|
@Preview(locale = "nl", name = "nl-NL", device = DS)
|
||||||
|
@Preview(locale = "pl", name = "pl-PL", device = DS)
|
||||||
|
@Preview(locale = "pt", name = "pt-PT", device = DS)
|
||||||
|
@Preview(locale = "pt-BR", name = "pt-BR", device = DS)
|
||||||
|
@Preview(locale = "rm", name = "rm", device = DS)
|
||||||
|
@Preview(locale = "ro", name = "ro", device = DS)
|
||||||
|
@Preview(locale = "ru", name = "ru-RU", device = DS)
|
||||||
|
@Preview(locale = "sk", name = "sk", device = DS)
|
||||||
|
@Preview(locale = "sl", name = "sl", device = DS)
|
||||||
|
@Preview(locale = "sr", name = "sr", device = DS)
|
||||||
|
@Preview(locale = "sv", name = "sv-SE", device = DS)
|
||||||
|
@Preview(locale = "sw", name = "sw", device = DS)
|
||||||
|
@Preview(locale = "ta-IN", name = "ta-IN", device = DS)
|
||||||
|
@Preview(locale = "te-IN", name = "te-IN", device = DS)
|
||||||
|
@Preview(locale = "th", name = "th", device = DS)
|
||||||
|
@Preview(locale = "tr", name = "tr-TR", device = DS)
|
||||||
|
@Preview(locale = "uk", name = "uk", device = DS)
|
||||||
|
@Preview(locale = "vi", name = "vi", device = DS)
|
||||||
|
@Preview(locale = "zh-CN", name = "zh-CN", device = DS)
|
||||||
|
@Preview(locale = "zh-HK", name = "zh-HK", device = DS)
|
||||||
|
@Preview(locale = "zh-TW", name = "zh-TW", device = DS)
|
||||||
|
annotation class PlayStoreLocales
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same locales but with night mode enabled for dark theme screenshots.
|
||||||
|
*/
|
||||||
|
@Preview(locale = "en", name = "en-US", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "af", name = "af", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "am", name = "am", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ar", name = "ar", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "az", name = "az-AZ", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "be", name = "be", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "bg", name = "bg", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "bn-BD", name = "bn-BD", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ca", name = "ca", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "cs", name = "cs-CZ", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "da", name = "da-DK", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "de", name = "de-DE", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "el", name = "el-GR", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "es", name = "es-ES", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "es-MX", name = "es-419", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "et-EE", name = "et", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "eu-ES", name = "eu-ES", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "fa", name = "fa", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "fi", name = "fi-FI", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "fil", name = "fil", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "fr", name = "fr-FR", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "gl-ES", name = "gl-ES", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "hi-IN", name = "hi-IN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "hr", name = "hr", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "hu", name = "hu-HU", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "hy-AM", name = "hy-AM", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "in", name = "id", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "is", name = "is-IS", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "it", name = "it-IT", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "iw", name = "iw-IL", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ja", name = "ja-JP", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ka-GE", name = "ka-GE", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "km-KH", name = "km-KH", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "kn-IN", name = "kn-IN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ko", name = "ko-KR", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ky-KG", name = "ky-KG", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "lo-LA", name = "lo-LA", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "lt", name = "lt", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "lv", name = "lv", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "mk-MK", name = "mk-MK", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ml-IN", name = "ml-IN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "mn-MN", name = "mn-MN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "mr-IN", name = "mr-IN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ms", name = "ms", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "my-MM", name = "my-MM", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "nb", name = "no-NO", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ne-NP", name = "ne-NP", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "nl", name = "nl-NL", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "pl", name = "pl-PL", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "pt", name = "pt-PT", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "pt-BR", name = "pt-BR", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "rm", name = "rm", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ro", name = "ro", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ru", name = "ru-RU", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "sk", name = "sk", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "sl", name = "sl", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "sr", name = "sr", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "sv", name = "sv-SE", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "sw", name = "sw", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "ta-IN", name = "ta-IN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "te-IN", name = "te-IN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "th", name = "th", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "tr", name = "tr-TR", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "uk", name = "uk", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "vi", name = "vi", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "zh-CN", name = "zh-CN", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "zh-HK", name = "zh-HK", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(locale = "zh-TW", name = "zh-TW", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
annotation class PlayStoreLocalesDark
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoke test subset for fast iteration (6 locales covering LTR, RTL, CJK).
|
||||||
|
*/
|
||||||
|
@Preview(locale = "en", name = "en-US", device = DS)
|
||||||
|
@Preview(locale = "de", name = "de-DE", device = DS)
|
||||||
|
@Preview(locale = "ja", name = "ja-JP", device = DS)
|
||||||
|
@Preview(locale = "ar", name = "ar", device = DS)
|
||||||
|
@Preview(locale = "zh-CN", name = "zh-CN", device = DS)
|
||||||
|
@Preview(locale = "pt-BR", name = "pt-BR", device = DS)
|
||||||
|
annotation class PlayStoreLocalesSmoke
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// For IDE design preview, open ScreenshotPreviews.kt instead.
|
||||||
|
package eu.darken.capod.screenshots
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.android.tools.screenshot.PreviewTest
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocales
|
||||||
|
@Composable
|
||||||
|
fun DashboardLight() = DashboardContent()
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocalesDark
|
||||||
|
@Composable
|
||||||
|
fun DashboardDark() = DashboardContent()
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocales
|
||||||
|
@Composable
|
||||||
|
fun DeviceProfiles() = DeviceProfilesContent()
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocales
|
||||||
|
@Composable
|
||||||
|
fun AddProfile() = AddProfileContent()
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocales
|
||||||
|
@Composable
|
||||||
|
fun SettingsIndex() = SettingsIndexContent()
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocales
|
||||||
|
@Composable
|
||||||
|
fun ReactionSettings() = ReactionSettingsContent()
|
||||||
|
|
||||||
|
@PreviewTest
|
||||||
|
@PlayStoreLocales
|
||||||
|
@Composable
|
||||||
|
fun WidgetConfiguration() = WidgetConfigurationContent()
|
||||||
@@ -8,17 +8,17 @@ private fun DependencyHandler.implementation(dependencyNotation: Any): Dependenc
|
|||||||
private fun DependencyHandler.testImplementation(dependencyNotation: Any): Dependency? =
|
private fun DependencyHandler.testImplementation(dependencyNotation: Any): Dependency? =
|
||||||
add("testImplementation", dependencyNotation)
|
add("testImplementation", dependencyNotation)
|
||||||
|
|
||||||
private fun DependencyHandler.kapt(dependencyNotation: Any): Dependency? =
|
private fun DependencyHandler.ksp(dependencyNotation: Any): Dependency? =
|
||||||
add("kapt", dependencyNotation)
|
add("ksp", dependencyNotation)
|
||||||
|
|
||||||
private fun DependencyHandler.kaptTest(dependencyNotation: Any): Dependency? =
|
private fun DependencyHandler.kspTest(dependencyNotation: Any): Dependency? =
|
||||||
add("kaptTest", dependencyNotation)
|
add("kspTest", dependencyNotation)
|
||||||
|
|
||||||
private fun DependencyHandler.androidTestImplementation(dependencyNotation: Any): Dependency? =
|
private fun DependencyHandler.androidTestImplementation(dependencyNotation: Any): Dependency? =
|
||||||
add("androidTestImplementation", dependencyNotation)
|
add("androidTestImplementation", dependencyNotation)
|
||||||
|
|
||||||
private fun DependencyHandler.kaptAndroidTest(dependencyNotation: Any): Dependency? =
|
private fun DependencyHandler.kspAndroidTest(dependencyNotation: Any): Dependency? =
|
||||||
add("kaptAndroidTest", dependencyNotation)
|
add("kspAndroidTest", dependencyNotation)
|
||||||
|
|
||||||
private fun DependencyHandler.testRuntimeOnly(dependencyNotation: Any): Dependency? =
|
private fun DependencyHandler.testRuntimeOnly(dependencyNotation: Any): Dependency? =
|
||||||
add("testRuntimeOnly", dependencyNotation)
|
add("testRuntimeOnly", dependencyNotation)
|
||||||
@@ -46,18 +46,18 @@ fun DependencyHandlerScope.addDagger() {
|
|||||||
implementation("com.google.dagger:dagger-android:${Versions.Dagger.core}")
|
implementation("com.google.dagger:dagger-android:${Versions.Dagger.core}")
|
||||||
implementation("androidx.hilt:hilt-common:1.0.0")
|
implementation("androidx.hilt:hilt-common:1.0.0")
|
||||||
|
|
||||||
kapt("com.google.dagger:dagger-compiler:${Versions.Dagger.core}")
|
ksp("com.google.dagger:dagger-compiler:${Versions.Dagger.core}")
|
||||||
kapt("com.google.dagger:dagger-android-processor:${Versions.Dagger.core}")
|
ksp("com.google.dagger:dagger-android-processor:${Versions.Dagger.core}")
|
||||||
|
|
||||||
implementation("com.google.dagger:hilt-android:${Versions.Dagger.core}")
|
implementation("com.google.dagger:hilt-android:${Versions.Dagger.core}")
|
||||||
kapt("androidx.hilt:hilt-compiler:1.0.0")
|
ksp("androidx.hilt:hilt-compiler:1.0.0")
|
||||||
kapt("com.google.dagger:hilt-android-compiler:${Versions.Dagger.core}")
|
ksp("com.google.dagger:hilt-android-compiler:${Versions.Dagger.core}")
|
||||||
|
|
||||||
testImplementation("com.google.dagger:hilt-android-testing:${Versions.Dagger.core}")
|
testImplementation("com.google.dagger:hilt-android-testing:${Versions.Dagger.core}")
|
||||||
kaptTest("com.google.dagger:hilt-android-compiler:${Versions.Dagger.core}")
|
kspTest("com.google.dagger:hilt-android-compiler:${Versions.Dagger.core}")
|
||||||
|
|
||||||
androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.Dagger.core}")
|
androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.Dagger.core}")
|
||||||
kaptAndroidTest("com.google.dagger:hilt-android-compiler:${Versions.Dagger.core}")
|
kspAndroidTest("com.google.dagger:hilt-android-compiler:${Versions.Dagger.core}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.addMoshi() {
|
fun DependencyHandlerScope.addMoshi() {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
Prompt for automatic screenshots via /debugbadger:screenshots
|
|
||||||
|
|
||||||
First open the app and ensure we start out on "light mode".
|
|
||||||
If we are not using the current theme, you can change it via Settings>General>Theme mode
|
|
||||||
|
|
||||||
Create a total of 8 phone screenshots in the following order:
|
|
||||||
|
|
||||||
* Dashboard in "Light Theme"
|
|
||||||
* This is always the starting point.
|
|
||||||
* Dashboard in "Dark Theme"
|
|
||||||
* Change the theme back to "Light Theme" after taking the screenshot.
|
|
||||||
* Device Profiles Page
|
|
||||||
* Click the device "Configure" icon the toolbar on the dashbaord to reach it.
|
|
||||||
* Add new device profile page
|
|
||||||
* Reach it by pressing the FAB (bottom right corner) on the device profiles page.
|
|
||||||
* Settings Index Screen
|
|
||||||
* Can be reached via settings icon in the dashboard toolbar.
|
|
||||||
* Reaction Settings Screen
|
|
||||||
* Reached via "Reactions" entry on the settings index screen.
|
|
||||||
* Widgets in launcher
|
|
||||||
* Press the device home button or completely close the app
|
|
||||||
* The widgets will be on the launchers center workspace
|
|
||||||
* Widget config screen
|
|
||||||
* Long press the widget, then release it and press the edit icon in the widget
|
|
||||||
* If there are more than 1 widget, pick any of them
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Copies generated screenshot PNGs from the Compose Preview reference directory
|
||||||
|
# into fastlane metadata directories for Play Store upload.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./fastlane/copy_screenshots.sh # Copy all screenshots
|
||||||
|
# ./fastlane/copy_screenshots.sh --clean # Clean target dirs before copying
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
REF_DIR="$PROJECT_DIR/app/src/screenshotTestGplayDebug/reference/eu/darken/capod/screenshots/PlayStoreScreenshotsKt"
|
||||||
|
FASTLANE_DIR="$PROJECT_DIR/fastlane/metadata/android"
|
||||||
|
|
||||||
|
CLEAN=false
|
||||||
|
if [[ "${1:-}" == "--clean" ]]; then
|
||||||
|
CLEAN=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Map composable function names to screenshot filenames.
|
||||||
|
# Format: number_label — number controls Play Store ordering, label aids humans.
|
||||||
|
declare -A SCREEN_MAP=(
|
||||||
|
[DashboardLight]="1_dashboard_light"
|
||||||
|
[DashboardDark]="2_dashboard_dark"
|
||||||
|
[DeviceProfiles]="3_device_profiles"
|
||||||
|
[AddProfile]="4_add_profile"
|
||||||
|
[SettingsIndex]="5_settings"
|
||||||
|
[ReactionSettings]="6_reaction_settings"
|
||||||
|
[WidgetConfiguration]="7_widget_configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ ! -d "$REF_DIR" ]]; then
|
||||||
|
echo "ERROR: Reference directory not found: $REF_DIR"
|
||||||
|
echo "Run ./fastlane/generate_screenshots.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count available images
|
||||||
|
AVAILABLE=$(find "$REF_DIR" -name "*.png" | wc -l)
|
||||||
|
if (( AVAILABLE == 0 )); then
|
||||||
|
echo "ERROR: No PNG files found in $REF_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Found $AVAILABLE screenshot images"
|
||||||
|
|
||||||
|
if $CLEAN; then
|
||||||
|
echo "Cleaning existing phoneScreenshots directories..."
|
||||||
|
find "$FASTLANE_DIR" -path "*/images/phoneScreenshots" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
COPIED=0
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
for png in "$REF_DIR"/*.png; do
|
||||||
|
filename=$(basename "$png")
|
||||||
|
|
||||||
|
# Filename format: FunctionName_previewName_hash_index.png
|
||||||
|
# Split from the right: remove _index.png, then _hash
|
||||||
|
# FunctionName has no underscores (camelCase), previewName may have hyphens
|
||||||
|
|
||||||
|
# Remove .png extension
|
||||||
|
stem="${filename%.png}"
|
||||||
|
|
||||||
|
# Remove trailing _index (digit(s))
|
||||||
|
stem="${stem%_[0-9]*}"
|
||||||
|
|
||||||
|
# Remove trailing _hash (hex string)
|
||||||
|
stem="${stem%_[a-f0-9]*}"
|
||||||
|
|
||||||
|
# Now stem = FunctionName_previewName
|
||||||
|
# Split at first underscore
|
||||||
|
func_name="${stem%%_*}"
|
||||||
|
locale_name="${stem#*_}"
|
||||||
|
|
||||||
|
if [[ -z "${SCREEN_MAP[$func_name]+x}" ]]; then
|
||||||
|
echo "WARNING: Unknown function name '$func_name' in $filename — skipping"
|
||||||
|
(( ERRORS++ )) || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
screen_name="${SCREEN_MAP[$func_name]}"
|
||||||
|
target_dir="$FASTLANE_DIR/$locale_name/images/phoneScreenshots"
|
||||||
|
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
cp "$png" "$target_dir/${screen_name}.png"
|
||||||
|
(( COPIED++ )) || true
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Copy Complete ==="
|
||||||
|
echo "Copied: $COPIED images"
|
||||||
|
if (( ERRORS > 0 )); then
|
||||||
|
echo "Errors: $ERRORS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate: check each locale has the expected number of screenshots
|
||||||
|
echo ""
|
||||||
|
echo "Validation:"
|
||||||
|
INCOMPLETE=0
|
||||||
|
for locale_dir in "$FASTLANE_DIR"/*/images/phoneScreenshots; do
|
||||||
|
if [[ ! -d "$locale_dir" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
locale=$(echo "$locale_dir" | sed "s|$FASTLANE_DIR/||" | sed 's|/images/phoneScreenshots||')
|
||||||
|
count=$(find "$locale_dir" -name "*.png" | wc -l)
|
||||||
|
expected=${#SCREEN_MAP[@]}
|
||||||
|
if (( count != expected )); then
|
||||||
|
echo " INCOMPLETE: $locale has $count/${expected} screenshots"
|
||||||
|
(( INCOMPLETE++ )) || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( INCOMPLETE == 0 )); then
|
||||||
|
echo " All locales have complete screenshot sets."
|
||||||
|
else
|
||||||
|
echo " $INCOMPLETE locale(s) have incomplete screenshot sets."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Generates localized Play Store screenshots in batches to work around
|
||||||
|
# layoutlib ImagePoolImpl memory leak (accumulates rendered images without release).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./fastlane/generate_screenshots.sh # Full run (all locales, ~12 batches)
|
||||||
|
# ./fastlane/generate_screenshots.sh --smoke # Smoke test (6 locales, 1 batch)
|
||||||
|
# ./fastlane/generate_screenshots.sh --batch-size 10 # Custom batch size
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
LOCALES_FILE="$PROJECT_DIR/app/src/screenshotTest/kotlin/eu/darken/capod/screenshots/PlayStoreLocales.kt"
|
||||||
|
REF_DIR="$PROJECT_DIR/app/src/screenshotTestGplayDebug/reference"
|
||||||
|
|
||||||
|
# Default batch size — 6 locales × 7 composables = 42 renders per batch.
|
||||||
|
# Kept small to avoid layoutlib OOM (leaks ~4MB per rendered image at 720p; more at 1080p).
|
||||||
|
BATCH_SIZE=4
|
||||||
|
SMOKE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--smoke) SMOKE=true; shift ;;
|
||||||
|
--batch-size) BATCH_SIZE="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Each entry: "android_locale:fastlane_name"
|
||||||
|
ALL_LOCALES=(
|
||||||
|
"en:en-US"
|
||||||
|
"af:af"
|
||||||
|
"am:am"
|
||||||
|
"ar:ar"
|
||||||
|
"az:az-AZ"
|
||||||
|
"be:be"
|
||||||
|
"bg:bg"
|
||||||
|
"bn-BD:bn-BD"
|
||||||
|
"ca:ca"
|
||||||
|
"cs:cs-CZ"
|
||||||
|
"da:da-DK"
|
||||||
|
"de:de-DE"
|
||||||
|
"el:el-GR"
|
||||||
|
"es:es-ES"
|
||||||
|
"es-MX:es-419"
|
||||||
|
"et-EE:et"
|
||||||
|
"eu-ES:eu-ES"
|
||||||
|
"fa:fa"
|
||||||
|
"fi:fi-FI"
|
||||||
|
"fil:fil"
|
||||||
|
"fr:fr-FR"
|
||||||
|
"gl-ES:gl-ES"
|
||||||
|
"hi-IN:hi-IN"
|
||||||
|
"hr:hr"
|
||||||
|
"hu:hu-HU"
|
||||||
|
"hy-AM:hy-AM"
|
||||||
|
"in:id"
|
||||||
|
"is:is-IS"
|
||||||
|
"it:it-IT"
|
||||||
|
"iw:iw-IL"
|
||||||
|
"ja:ja-JP"
|
||||||
|
"ka-GE:ka-GE"
|
||||||
|
"km-KH:km-KH"
|
||||||
|
"kn-IN:kn-IN"
|
||||||
|
"ko:ko-KR"
|
||||||
|
"ky-KG:ky-KG"
|
||||||
|
"lo-LA:lo-LA"
|
||||||
|
"lt:lt"
|
||||||
|
"lv:lv"
|
||||||
|
"mk-MK:mk-MK"
|
||||||
|
"ml-IN:ml-IN"
|
||||||
|
"mn-MN:mn-MN"
|
||||||
|
"mr-IN:mr-IN"
|
||||||
|
"ms:ms"
|
||||||
|
"my-MM:my-MM"
|
||||||
|
"nb:no-NO"
|
||||||
|
"ne-NP:ne-NP"
|
||||||
|
"nl:nl-NL"
|
||||||
|
"pl:pl-PL"
|
||||||
|
"pt:pt-PT"
|
||||||
|
"pt-BR:pt-BR"
|
||||||
|
"rm:rm"
|
||||||
|
"ro:ro"
|
||||||
|
"ru:ru-RU"
|
||||||
|
"sk:sk"
|
||||||
|
"sl:sl"
|
||||||
|
"sr:sr"
|
||||||
|
"sv:sv-SE"
|
||||||
|
"sw:sw"
|
||||||
|
"ta-IN:ta-IN"
|
||||||
|
"te-IN:te-IN"
|
||||||
|
"th:th"
|
||||||
|
"tr:tr-TR"
|
||||||
|
"uk:uk"
|
||||||
|
"vi:vi"
|
||||||
|
"zh-CN:zh-CN"
|
||||||
|
"zh-HK:zh-HK"
|
||||||
|
"zh-TW:zh-TW"
|
||||||
|
)
|
||||||
|
|
||||||
|
SMOKE_LOCALES=(
|
||||||
|
"en:en-US"
|
||||||
|
"de:de-DE"
|
||||||
|
"ja:ja-JP"
|
||||||
|
"ar:ar"
|
||||||
|
"zh-CN:zh-CN"
|
||||||
|
"pt-BR:pt-BR"
|
||||||
|
)
|
||||||
|
|
||||||
|
if $SMOKE; then
|
||||||
|
LOCALES=("${SMOKE_LOCALES[@]}")
|
||||||
|
BATCH_SIZE=${#LOCALES[@]} # Single batch for smoke
|
||||||
|
else
|
||||||
|
LOCALES=("${ALL_LOCALES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOTAL=${#LOCALES[@]}
|
||||||
|
NUM_BATCHES=$(( (TOTAL + BATCH_SIZE - 1) / BATCH_SIZE ))
|
||||||
|
|
||||||
|
echo "=== Localized Screenshot Generation ==="
|
||||||
|
echo "Locales: $TOTAL | Batch size: $BATCH_SIZE | Batches: $NUM_BATCHES"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Back up the original file
|
||||||
|
cp "$LOCALES_FILE" "$LOCALES_FILE.bak"
|
||||||
|
trap 'mv "$LOCALES_FILE.bak" "$LOCALES_FILE"; echo "Restored original PlayStoreLocales.kt"' EXIT
|
||||||
|
|
||||||
|
# Clean reference directory from previous runs
|
||||||
|
rm -rf "$REF_DIR"
|
||||||
|
echo "Cleaned reference directory"
|
||||||
|
|
||||||
|
generate_locales_file() {
|
||||||
|
local -n batch_locales=$1
|
||||||
|
local file="$2"
|
||||||
|
|
||||||
|
cat > "$file" << 'HEADER'
|
||||||
|
package eu.darken.capod.screenshots
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-preview annotation generating one preview per Play Store-supported locale (light mode).
|
||||||
|
* Each [name] is the fastlane metadata directory name for direct use in the copy script.
|
||||||
|
*/
|
||||||
|
HEADER
|
||||||
|
|
||||||
|
# Light annotations
|
||||||
|
for entry in "${batch_locales[@]}"; do
|
||||||
|
local locale="${entry%%:*}"
|
||||||
|
local name="${entry##*:}"
|
||||||
|
echo "@Preview(locale = \"$locale\", name = \"$name\", device = DS)" >> "$file"
|
||||||
|
done
|
||||||
|
echo "annotation class PlayStoreLocales" >> "$file"
|
||||||
|
echo "" >> "$file"
|
||||||
|
|
||||||
|
# Dark annotations
|
||||||
|
echo "/**" >> "$file"
|
||||||
|
echo " * Same locales but with night mode enabled for dark theme screenshots." >> "$file"
|
||||||
|
echo " */" >> "$file"
|
||||||
|
for entry in "${batch_locales[@]}"; do
|
||||||
|
local locale="${entry%%:*}"
|
||||||
|
local name="${entry##*:}"
|
||||||
|
echo "@Preview(locale = \"$locale\", name = \"$name\", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES)" >> "$file"
|
||||||
|
done
|
||||||
|
echo "annotation class PlayStoreLocalesDark" >> "$file"
|
||||||
|
echo "" >> "$file"
|
||||||
|
|
||||||
|
# Smoke annotation (single entry placeholder)
|
||||||
|
echo "/**" >> "$file"
|
||||||
|
echo " * Smoke test subset for fast iteration (6 locales covering LTR, RTL, CJK)." >> "$file"
|
||||||
|
echo " */" >> "$file"
|
||||||
|
echo "@Preview(locale = \"en\", name = \"en-US\", device = DS)" >> "$file"
|
||||||
|
echo "annotation class PlayStoreLocalesSmoke" >> "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
for (( batch=0; batch < NUM_BATCHES; batch++ )); do
|
||||||
|
start=$(( batch * BATCH_SIZE ))
|
||||||
|
end=$(( start + BATCH_SIZE ))
|
||||||
|
if (( end > TOTAL )); then
|
||||||
|
end=$TOTAL
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract batch slice
|
||||||
|
BATCH_SLICE=("${LOCALES[@]:$start:$((end - start))}")
|
||||||
|
batch_num=$(( batch + 1 ))
|
||||||
|
|
||||||
|
echo "--- Batch $batch_num/$NUM_BATCHES (locales $((start+1))-$end of $TOTAL) ---"
|
||||||
|
|
||||||
|
# Generate locales file for this batch
|
||||||
|
generate_locales_file BATCH_SLICE "$LOCALES_FILE"
|
||||||
|
|
||||||
|
# Stop daemon to release memory from previous batch
|
||||||
|
echo "Stopping Gradle daemon..."
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
./gradlew --stop 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run screenshot generation
|
||||||
|
echo "Generating screenshots..."
|
||||||
|
if ! ./gradlew updateGplayDebugScreenshotTest --no-daemon 2>&1; then
|
||||||
|
echo "ERROR: Batch $batch_num failed! Check output above."
|
||||||
|
echo "Generated images so far are preserved in: $REF_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
count=$(find "$REF_DIR" -name "*.png" 2>/dev/null | wc -l)
|
||||||
|
echo "Batch $batch_num complete. Total images so far: $count"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Count final results
|
||||||
|
FINAL_COUNT=$(find "$REF_DIR" -name "*.png" 2>/dev/null | wc -l)
|
||||||
|
EXPECTED=$(( TOTAL * 7 )) # 7 composables per locale
|
||||||
|
|
||||||
|
echo "=== Generation Complete ==="
|
||||||
|
echo "Generated: $FINAL_COUNT images (expected: $EXPECTED)"
|
||||||
|
|
||||||
|
if (( FINAL_COUNT != EXPECTED )); then
|
||||||
|
echo "WARNING: Count mismatch! Some screenshots may be missing."
|
||||||
|
echo "Check $REF_DIR for details."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Next step: run ./fastlane/copy_screenshots.sh to sort into fastlane directories."
|
||||||
|
Before Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 938 KiB |
|
Before Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,5 +1,5 @@
|
|||||||
# Project-wide Gradle settings.
|
# Project-wide Gradle settings.
|
||||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Required by androidx.navigation.safeargs plugin
|
# Required by androidx.navigation.safeargs plugin
|
||||||
@@ -7,3 +7,5 @@ android.useAndroidX=true
|
|||||||
# Temporary AGP 9 opt-outs (must migrate before AGP 10)
|
# Temporary AGP 9 opt-outs (must migrate before AGP 10)
|
||||||
android.builtInKotlin=false
|
android.builtInKotlin=false
|
||||||
android.newDsl=false
|
android.newDsl=false
|
||||||
|
# Compose Preview Screenshot Testing
|
||||||
|
android.experimental.enableScreenshotTest=true
|
||||||
|
|||||||
@@ -1,2 +1,10 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = "Companion App for AirPods"
|
rootProject.name = "Companion App for AirPods"
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|||||||