Add onboarding image carousel
Bundle the rotating onboarding illustrations (light/dark) and wire them into the Android onboarding info component, matching the iOS carousel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -141,8 +141,8 @@ extension OnboardingInfoView.Configuration {
|
|||||||
subtitle: LocalizedStringKey("battery.onboarding.subtitle"),
|
subtitle: LocalizedStringKey("battery.onboarding.subtitle"),
|
||||||
primaryActionTitle: LocalizedStringKey("battery.overview.empty.create"),
|
primaryActionTitle: LocalizedStringKey("battery.overview.empty.create"),
|
||||||
primaryActionIcon: "plus",
|
primaryActionIcon: "plus",
|
||||||
secondaryActionTitle: nil,
|
secondaryActionTitle: LocalizedStringKey("loads.overview.empty.library"),
|
||||||
secondaryActionIcon: nil,
|
secondaryActionIcon: "books.vertical",
|
||||||
imageNames: [
|
imageNames: [
|
||||||
"battery-onboarding"
|
"battery-onboarding"
|
||||||
]
|
]
|
||||||
@@ -155,8 +155,8 @@ extension OnboardingInfoView.Configuration {
|
|||||||
subtitle: LocalizedStringKey("chargers.onboarding.subtitle"),
|
subtitle: LocalizedStringKey("chargers.onboarding.subtitle"),
|
||||||
primaryActionTitle: LocalizedStringKey("chargers.onboarding.primary"),
|
primaryActionTitle: LocalizedStringKey("chargers.onboarding.primary"),
|
||||||
primaryActionIcon: "plus",
|
primaryActionIcon: "plus",
|
||||||
secondaryActionTitle: nil,
|
secondaryActionTitle: LocalizedStringKey("loads.overview.empty.library"),
|
||||||
secondaryActionIcon: nil,
|
secondaryActionIcon: "books.vertical",
|
||||||
imageNames: [
|
imageNames: [
|
||||||
"charger-onboarding"
|
"charger-onboarding"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package app.voltplan.cable.ui.components
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-advancing onboarding illustration carousel. Mirrors iOS `OnboardingCarouselView`:
|
||||||
|
* cycles through the images every 8s with a horizontal slide. A single image stays static.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun OnboardingCarousel(
|
||||||
|
@DrawableRes images: List<Int>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (images.isEmpty()) return
|
||||||
|
var index by remember(images) { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
if (images.size > 1) {
|
||||||
|
LaunchedEffect(images) {
|
||||||
|
while (true) {
|
||||||
|
delay(8_000)
|
||||||
|
index = (index + 1) % images.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = index,
|
||||||
|
transitionSpec = {
|
||||||
|
slideInHorizontally(tween(800)) { it } togetherWith
|
||||||
|
slideOutHorizontally(tween(800)) { -it }
|
||||||
|
},
|
||||||
|
label = "onboarding-carousel",
|
||||||
|
modifier = modifier,
|
||||||
|
) { i ->
|
||||||
|
Image(
|
||||||
|
painter = painterResource(images[i]),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package app.voltplan.cable.ui.components
|
package app.voltplan.cable.ui.components
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -29,6 +31,7 @@ fun OnboardingInfo(
|
|||||||
onPrimary: () -> Unit,
|
onPrimary: () -> Unit,
|
||||||
secondaryLabel: String? = null,
|
secondaryLabel: String? = null,
|
||||||
onSecondary: (() -> Unit)? = null,
|
onSecondary: (() -> Unit)? = null,
|
||||||
|
@DrawableRes images: List<Int> = emptyList(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -36,7 +39,11 @@ fun OnboardingInfo(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(56.dp))
|
if (images.isNotEmpty()) {
|
||||||
|
OnboardingCarousel(images = images, modifier = Modifier.fillMaxWidth().height(200.dp))
|
||||||
|
} else {
|
||||||
|
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(56.dp))
|
||||||
|
}
|
||||||
Spacer(Modifier.size(16.dp))
|
Spacer(Modifier.size(16.dp))
|
||||||
Text(title, style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center)
|
Text(title, style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center)
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
|
|||||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/drawable-night-nodpi/onboarding_van.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_battery.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_boat.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_cabin.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_charger.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_coffee.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_router.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
android/app/src/main/res/drawable-nodpi/onboarding_van.png
Normal file
|
After Width: | Height: | Size: 64 KiB |