Created
June 10, 2025 00:03
-
-
Save insearching/3de4021c009ef57f30a91ae5bc97ce0c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.google.voice.plchallangesactivity.presentation.egg_hunt | |
import android.content.Context | |
import androidx.annotation.DrawableRes | |
import androidx.compose.animation.core.Animatable | |
import androidx.compose.animation.core.LinearEasing | |
import androidx.compose.animation.core.tween | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.border | |
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxHeight | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.layout.width | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.itemsIndexed | |
import androidx.compose.foundation.shape.RoundedCornerShape | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Scaffold | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableIntStateOf | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.draw.clip | |
import androidx.compose.ui.graphics.Brush | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.res.painterResource | |
import androidx.compose.ui.text.font.FontWeight | |
import androidx.compose.ui.text.style.TextAlign | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import androidx.compose.ui.window.Dialog | |
import androidx.datastore.core.DataStore | |
import androidx.datastore.preferences.core.Preferences | |
import androidx.datastore.preferences.core.edit | |
import androidx.datastore.preferences.core.stringPreferencesKey | |
import androidx.datastore.preferences.preferencesDataStore | |
import com.google.voice.plchallangesactivity.R | |
import com.google.voice.plchallangesactivity.ui.theme.PlChallengesTheme | |
import kotlinx.coroutines.flow.first | |
import kotlinx.coroutines.launch | |
import kotlinx.serialization.Serializable | |
import kotlinx.serialization.json.Json | |
import kotlin.math.roundToInt | |
private val titles = listOf<String>( | |
"Behind the TV stand", | |
"Behind the TV stand", | |
"Under the picnic table", | |
"Inside the birdhouse", | |
"Behind the garden bushes", | |
"Inside the flower pot", | |
"In the mailbox", | |
"In the kitchen pantry", | |
"Inside the flower pot", | |
"Inside the bedroom", | |
) | |
private const val EASTER_KEY = "easter_key" | |
@Composable | |
fun EggHunt( | |
modifier: Modifier = Modifier, | |
) { | |
val context = LocalContext.current | |
val easterFacts = remember { | |
context.resources.getStringArray(R.array.easter_facts).toList() | |
} | |
var eggsFound by remember { mutableIntStateOf(0) } | |
val scope = rememberCoroutineScope() | |
var showEasterFactDialog by remember { mutableStateOf(false) } | |
var showEggRollAwayDialog by remember { mutableStateOf(false) } | |
var easterLocations by remember { | |
mutableStateOf(titles.map { | |
EasterLocation( | |
title = it, | |
isChecked = false | |
) | |
}) | |
} | |
LaunchedEffect(Unit) { | |
val locations = readLocations(context) | |
if (locations.isNotEmpty()) { | |
easterLocations = locations | |
eggsFound = locations.count { it.isChecked } | |
} | |
} | |
Box( | |
modifier = Modifier.fillMaxSize(), | |
contentAlignment = Alignment.Center | |
) { | |
Scaffold { innerPadding -> | |
Column( | |
modifier = modifier | |
.background(Color(0xFF0F1241)) | |
.background( | |
brush = Brush.verticalGradient( | |
listOf( | |
Color(0x0005061D), | |
Color(0xFF05061D) | |
) | |
) | |
) | |
.padding(innerPadding) | |
.padding(horizontal = 16.dp), | |
verticalArrangement = Arrangement.spacedBy(4.dp), | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
GradientText( | |
text = "Egg Hunt Checklist" | |
) | |
Spacer(Modifier.height(16.dp)) | |
Text( | |
"Pick locations, where you’ve found eggs", | |
fontWeight = FontWeight.Black, | |
fontSize = 20.sp, | |
color = Color(0xFFFFFFFF) | |
) | |
Text( | |
"$eggsFound/10 eggs found", | |
fontWeight = FontWeight.Black, | |
fontSize = 20.sp, | |
color = Color(0xFFFFF583) | |
) | |
Spacer(Modifier.height(16.dp)) | |
LazyColumn( | |
modifier = Modifier.weight(1f), | |
verticalArrangement = Arrangement.spacedBy(4.dp), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
) { | |
itemsIndexed(items = easterLocations) { index, location -> | |
LocationRowItem( | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(vertical = 4.dp), | |
easterLocation = location, | |
onCheckedChange = { isChecked -> | |
if (isChecked) { | |
eggsFound++ | |
showEasterFactDialog = true | |
} else { | |
eggsFound-- | |
showEggRollAwayDialog = true | |
} | |
easterLocations = easterLocations.toMutableList().apply { | |
set(index, location.copy(isChecked = isChecked)) | |
} | |
scope.launch { | |
saveLocations(context, easterLocations) | |
} | |
} | |
) | |
} | |
} | |
Box( | |
modifier = Modifier | |
.fillMaxWidth() | |
.clip(RoundedCornerShape(100.dp)) | |
.background( | |
Brush.verticalGradient( | |
colors = listOf( | |
Color(0xFFFFC441), | |
Color(0xFFCA7500), | |
) | |
) | |
) | |
.clickable { | |
easterLocations = easterLocations.toMutableList().map { | |
it.copy(isChecked = false) | |
} | |
scope.launch { | |
saveLocations(context, easterLocations) | |
} | |
} | |
.padding(vertical = 16.dp), | |
contentAlignment = Alignment.Center | |
) { | |
Text( | |
text = "Reset", | |
fontWeight = FontWeight.Medium, | |
fontSize = 20.sp, | |
color = Color.White | |
) | |
} | |
} | |
} | |
if (showEasterFactDialog) { | |
EggHuntDialog( | |
title = "Easter fact!", | |
description = easterFacts.random(), | |
drawableRes = R.drawable.ic_easter_rabbit, | |
onDismiss = { showEasterFactDialog = false } | |
) | |
} | |
if (showEggRollAwayDialog) { | |
EggHuntDialog( | |
title = "Oops, the egg rolled away!", | |
drawableRes = R.drawable.ic_easter_egg, | |
onDismiss = { showEggRollAwayDialog = false } | |
) | |
} | |
} | |
} | |
@Serializable | |
data class EasterLocation( | |
val title: String, | |
val isChecked: Boolean, | |
) | |
@Composable | |
private fun LocationRowItem( | |
modifier: Modifier = Modifier, | |
easterLocation: EasterLocation, | |
onCheckedChange: (Boolean) -> Unit, | |
) { | |
Row( | |
modifier = modifier | |
.clip(RoundedCornerShape(16.dp)) | |
.then( | |
if (easterLocation.isChecked) Modifier.background( | |
Brush.verticalGradient( | |
colors = listOf( | |
Color(0xFFFFC441), | |
Color(0xFFCA7500), | |
) | |
) | |
) | |
else Modifier.background(Color(0xFF8D8EA1)) | |
) | |
.clickable(onClick = { | |
onCheckedChange(!easterLocation.isChecked) | |
}) | |
.padding(16.dp), | |
verticalAlignment = Alignment.CenterVertically, | |
horizontalArrangement = Arrangement.spacedBy(16.dp) | |
) { | |
CheckBox( | |
isChecked = easterLocation.isChecked, | |
onCheckedChange = onCheckedChange | |
) | |
Text( | |
text = easterLocation.title, | |
fontWeight = FontWeight.Black, | |
fontSize = 16.sp, | |
color = Color(0xFFFFFFFF) | |
) | |
} | |
} | |
@Composable | |
private fun CheckBox( | |
modifier: Modifier = Modifier, | |
isChecked: Boolean, | |
onCheckedChange: (Boolean) -> Unit, | |
) { | |
Box( | |
modifier = modifier | |
.size(24.dp) | |
.clip(RoundedCornerShape(8.dp)) | |
.background(Color.White) | |
.clickable { onCheckedChange(!isChecked) }, | |
contentAlignment = Alignment.Center | |
) { | |
if (isChecked) { | |
Image( | |
painter = painterResource(R.drawable.ic_checkbox), | |
contentDescription = "checkbox", | |
modifier = Modifier.size(14.dp) | |
) | |
} | |
} | |
} | |
@Composable | |
fun EggHuntDialog( | |
modifier: Modifier = Modifier, | |
title: String, | |
description: String? = null, | |
@DrawableRes drawableRes: Int, | |
onDismiss: () -> Unit, | |
) { | |
Dialog( | |
onDismissRequest = onDismiss, | |
) { | |
Column( | |
verticalArrangement = Arrangement.spacedBy(16.dp), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
modifier = modifier | |
.clip(RoundedCornerShape(16.dp)) | |
.background(Color(0xFF10122C)) | |
.padding(24.dp) | |
) { | |
AnimatedGradientProgressBar( | |
modifier = Modifier.fillMaxWidth(), | |
onProgressFinished = onDismiss | |
) | |
Text( | |
text = title, | |
fontWeight = FontWeight.Black, | |
fontSize = 16.sp, | |
color = Color.White | |
) | |
Image( | |
painter = painterResource(drawableRes), | |
contentDescription = "easter rabbit", | |
modifier = Modifier.size(115.dp) | |
) | |
description?.let { | |
GradientText( | |
text = description, | |
fontSize = 16.sp, | |
textAlign = TextAlign.Center, | |
) | |
} | |
EasterButton( | |
modifier = Modifier.fillMaxWidth(), | |
text = "Dismiss", | |
onClick = onDismiss | |
) | |
} | |
} | |
} | |
@Composable | |
fun AnimatedGradientProgressBar( | |
modifier: Modifier = Modifier, | |
onProgressFinished: () -> Unit, | |
) { | |
val totalTime = 4000 // in ms | |
val progress = remember { Animatable(1f) } | |
var timeLeft by remember { mutableIntStateOf(4) } | |
LaunchedEffect(Unit) { | |
progress.animateTo( | |
targetValue = 0f, | |
animationSpec = tween(durationMillis = totalTime, easing = LinearEasing) | |
) | |
// Final fallback to ensure time shows 0 | |
timeLeft = 0 | |
onProgressFinished() | |
} | |
LaunchedEffect(progress.value) { | |
timeLeft = (progress.value * 4).roundToInt() | |
} | |
val backgroundColor = Color(0xFF894621) | |
val gradient = Brush.horizontalGradient( | |
colors = listOf(Color(0xFFFFC441), Color(0xFFCA7500)) | |
) | |
Row( | |
modifier = modifier, | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Box( | |
modifier = Modifier | |
.weight(1f) | |
.height(6.dp) | |
.background(backgroundColor, RoundedCornerShape(8.dp)) | |
) { | |
Box( | |
modifier = Modifier | |
.fillMaxHeight() | |
.fillMaxWidth(1f - progress.value) | |
.background(gradient, RoundedCornerShape(8.dp)) | |
) | |
} | |
Spacer(modifier = Modifier.width(8.dp)) | |
Text( | |
text = "${timeLeft}s", | |
style = MaterialTheme.typography.bodyLarge, | |
fontWeight = FontWeight.Bold, | |
color = Color(0xFFFFF583) | |
) | |
} | |
} | |
@Composable | |
private fun EasterButton( | |
modifier: Modifier = Modifier, | |
text: String, | |
onClick: () -> Unit, | |
) { | |
Box( | |
modifier = modifier | |
.clip(RoundedCornerShape(8.dp)) | |
.border(2.dp, Color.White.copy(.4f), RoundedCornerShape(8.dp)) | |
.background( | |
Brush.verticalGradient( | |
colors = listOf( | |
Color(0xFFFFC441), | |
Color(0xFFCA7500), | |
) | |
) | |
) | |
.clickable(onClick = onClick) | |
.padding(vertical = 16.dp), | |
contentAlignment = Alignment.Center | |
) { | |
Text( | |
text = text, | |
fontWeight = FontWeight.Black, | |
fontSize = 16.sp, | |
color = Color.White | |
) | |
} | |
} | |
@Preview | |
@Composable | |
private fun EggHuntDialogPreview() { | |
PlChallengesTheme { | |
EggHuntDialog( | |
title = "Easter fact!", | |
description = "The tradition of decorating eggs dates back to ancient civilizations,\n" + | |
"including Egyptians and Persians.", | |
drawableRes = R.drawable.ic_easter_rabbit, | |
onDismiss = {} | |
) | |
} | |
} | |
@Preview | |
@Composable | |
private fun EggHuntRollAwayDialogPreview() { | |
PlChallengesTheme { | |
EggHuntDialog( | |
title = "Oops, the egg rolled away!", | |
drawableRes = R.drawable.ic_easter_egg, | |
onDismiss = {} | |
) | |
} | |
} | |
@Preview | |
@Composable | |
private fun EggHuntPreview() { | |
PlChallengesTheme { | |
EggHunt( | |
modifier = Modifier.fillMaxSize() | |
) | |
} | |
} | |
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "easter_prefs") | |
suspend fun saveLocations(context: Context, locations: List<EasterLocation>) { | |
val json = Json.encodeToString(locations) | |
context.dataStore.edit { prefs -> | |
prefs[stringPreferencesKey(EASTER_KEY)] = json | |
} | |
} | |
suspend fun readLocations(context: Context): List<EasterLocation> { | |
val prefs = context.dataStore.data.first() | |
val json = prefs[stringPreferencesKey(EASTER_KEY)] | |
return if (!json.isNullOrEmpty()) { | |
Json.decodeFromString(json) | |
} else { | |
emptyList() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment