feat: category picker redesign, bucket list wiring, settings + nav updates

This commit is contained in:
null 2026-06-17 20:38:06 -05:00
parent c816033e74
commit a0fd1d56f6
9 changed files with 622 additions and 66 deletions

View File

@ -3,9 +3,9 @@ package app.closer.core.navigation
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
@ -47,6 +47,7 @@ import app.closer.ui.dates.DateMatchesScreen
import app.closer.ui.dates.DateBuilderScreen
import app.closer.ui.dates.BucketListScreen
import app.closer.ui.paywall.PaywallScreen
import app.closer.ui.play.PlayHubScreen
import app.closer.ui.questions.DailyQuestionScreen
import app.closer.ui.questions.QuestionCategoryScreen
import app.closer.ui.questions.QuestionComposerScreen
@ -170,6 +171,9 @@ fun AppNavigation(
composable(route = AppRoute.PARTNER_HOME) {
PartnerHomeScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.PLAY) {
PlayHubScreen(onNavigate = navigateRoute)
}
// Daily Question
composable(route = AppRoute.DAILY_QUESTION) {
@ -349,8 +353,8 @@ private data class TopLevelRoute(
private val topLevelRoutes = listOf(
TopLevelRoute(AppRoute.HOME, "Home", Icons.Filled.Home),
TopLevelRoute(AppRoute.DAILY_QUESTION, "Today", Icons.Filled.Favorite),
TopLevelRoute(AppRoute.PLAY, "Play", Icons.Filled.PlayArrow),
TopLevelRoute(AppRoute.QUESTION_PACKS, "Packs", Icons.Filled.Star),
TopLevelRoute(AppRoute.ANSWER_HISTORY, "Answers", Icons.Filled.Done),
TopLevelRoute(AppRoute.SETTINGS, "Settings", Icons.Filled.Settings)
)
@ -364,6 +368,11 @@ private val shellBackRoutes = setOf(
AppRoute.SPIN_WHEEL,
AppRoute.WHEEL_SESSION,
AppRoute.WHEEL_COMPLETE,
AppRoute.WHEEL_HISTORY,
AppRoute.DATE_MATCH,
AppRoute.DATE_MATCHES,
AppRoute.DATE_BUILDER,
AppRoute.BUCKET_LIST,
AppRoute.ACCOUNT,
AppRoute.SUBSCRIPTION,
AppRoute.PAYWALL

View File

@ -10,6 +10,7 @@ object AppRoute {
const val CREATE_PROFILE = "create_profile"
const val HOME = "home"
const val PARTNER_HOME = "partner_home"
const val PLAY = "play"
const val DAILY_QUESTION = "daily_question"
const val QUESTION_PACKS = "question_packs"
const val QUESTION_CATEGORY = "question_category/{categoryId}"
@ -56,6 +57,7 @@ object AppRoute {
Definition(FORGOT_PASSWORD, "Forgot Password", "auth"),
Definition(HOME, "Home", "home"),
Definition(PARTNER_HOME, "Partner", "home"),
Definition(PLAY, "Play", "play"),
Definition(DAILY_QUESTION, "Daily Question", "questions"),
Definition(QUESTION_PACKS, "Question Packs", "questions"),
Definition(QUESTION_CATEGORY, "Question Pack", "questions"),
@ -89,8 +91,8 @@ object AppRoute {
val topLevelRoutes = setOf(
HOME,
DAILY_QUESTION,
PLAY,
QUESTION_PACKS,
ANSWER_HISTORY,
SETTINGS
)

View File

@ -28,24 +28,22 @@ class BucketListRepositoryImpl @Inject constructor(
dataSource.updateItem(item.coupleId, item)
}
override suspend fun getItem(itemId: String): BucketListItem? {
// Need coupleId to fetch - in practice, this would be called from a context
// where the coupleId is known. For now, return null.
return null
override suspend fun getItem(coupleId: String, itemId: String): BucketListItem? {
return dataSource.getItem(coupleId, itemId)
}
override suspend fun getItems(coupleId: String): List<BucketListItem> {
return dataSource.getItems(coupleId)
}
override suspend fun deleteItem(itemId: String) {
// Need coupleId to delete - same limitation as getItem
override suspend fun deleteItem(coupleId: String, itemId: String) {
dataSource.deleteItem(coupleId, itemId)
}
// ─── Completion methods ──────────────────────────────────────────────────
override suspend fun completeItem(itemId: String, completedBy: String) {
// Need coupleId - same limitation
override suspend fun completeItem(coupleId: String, itemId: String, completedBy: String) {
dataSource.completeItem(coupleId, itemId, completedBy)
}
// ─── Category methods ────────────────────────────────────────────────────

View File

@ -22,18 +22,18 @@ interface BucketListRepository {
suspend fun updateItem(item: BucketListItem)
/** Get a bucket list item by ID. */
suspend fun getItem(itemId: String): BucketListItem?
suspend fun getItem(coupleId: String, itemId: String): BucketListItem?
/** Get all bucket list items for a couple. */
suspend fun getItems(coupleId: String): List<BucketListItem>
/** Delete a bucket list item. */
suspend fun deleteItem(itemId: String)
suspend fun deleteItem(coupleId: String, itemId: String)
// ─── Completion methods ──────────────────────────────────────────────────
/** Mark an item as completed. */
suspend fun completeItem(itemId: String, completedBy: String)
suspend fun completeItem(coupleId: String, itemId: String, completedBy: String)
// ─── Category methods ────────────────────────────────────────────────────

View File

@ -62,6 +62,8 @@ class BucketListViewModel @Inject constructor(
fun toggleComplete(itemId: String) {
val item = _uiState.value.items.find { it.id == itemId } ?: return
val coupleId = _uiState.value.coupleId
if (coupleId.isEmpty()) return
viewModelScope.launch {
if (item.isCompleted) {
@ -72,7 +74,7 @@ class BucketListViewModel @Inject constructor(
)
}
} else {
repository.completeItem(itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "")
repository.completeItem(coupleId, itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "")
_uiState.update {
it.copy(
items = it.items.map {
@ -86,8 +88,11 @@ class BucketListViewModel @Inject constructor(
}
fun deleteItem(itemId: String) {
val coupleId = _uiState.value.coupleId
if (coupleId.isEmpty()) return
viewModelScope.launch {
repository.deleteItem(itemId)
repository.deleteItem(coupleId, itemId)
_uiState.update {
it.copy(items = it.items.filter { it.id != itemId })
}

View File

@ -0,0 +1,356 @@
package app.closer.ui.play
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.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
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.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.closer.core.navigation.AppRoute
@Composable
fun PlayHubScreen(
onNavigate: (String) -> Unit = {}
) {
PlayHubContent(onNavigate = onNavigate)
}
@Composable
private fun PlayHubContent(
onNavigate: (String) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.linearGradient(
listOf(Color(0xFFFFFBFE), Color(0xFFF8F1FF), Color(0xFFFFEEF7)),
start = Offset.Zero,
end = Offset.Infinite
)
)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 20.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
item {
Column(
modifier = Modifier.padding(top = 22.dp, bottom = 4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Play",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Pick something light, useful, or a little surprising to do together.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF5A5060),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
item {
FeaturedPlayCard(
onClick = { onNavigate(AppRoute.CATEGORY_PICKER) }
)
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CompactPlayCard(
title = "Date Match",
subtitle = "Swipe ideas",
icon = Icons.Filled.Favorite,
tint = Color(0xFF9B1B5A),
modifier = Modifier.weight(1f),
onClick = { onNavigate(AppRoute.DATE_MATCH) }
)
CompactPlayCard(
title = "Plan Date",
subtitle = "Set the shape",
icon = Icons.Filled.Star,
tint = Color(0xFF6A4A00),
modifier = Modifier.weight(1f),
onClick = { onNavigate(AppRoute.DATE_BUILDER) }
)
}
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CompactPlayCard(
title = "Bucket List",
subtitle = "Save ideas",
icon = Icons.Filled.Done,
tint = Color(0xFF2F5B4F),
modifier = Modifier.weight(1f),
onClick = { onNavigate(AppRoute.BUCKET_LIST) }
)
CompactPlayCard(
title = "Wheel History",
subtitle = "Past sessions",
icon = Icons.Filled.Home,
tint = Color(0xFF56306F),
modifier = Modifier.weight(1f),
onClick = { onNavigate(AppRoute.WHEEL_HISTORY) }
)
}
}
}
}
}
@Composable
private fun FeaturedPlayCard(
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(30.dp),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.9f)),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
Column(
modifier = Modifier
.background(
Brush.linearGradient(
listOf(Color.White, Color(0xFFF3E8FF), Color(0xFFFFF7FB)),
start = Offset.Zero,
end = Offset.Infinite
)
)
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(18.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(999.dp),
color = Color.White.copy(alpha = 0.72f)
) {
Text(
text = "Wheel",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp),
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF56306F),
fontWeight = FontWeight.SemiBold
)
}
Surface(
shape = RoundedCornerShape(999.dp),
color = Color(0xFFFFE8F4)
) {
Text(
text = "10 prompts",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp),
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF6D2B55),
fontWeight = FontWeight.SemiBold
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically
) {
WheelGlyph(
modifier = Modifier.size(74.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = "Spin the Wheel",
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Choose a mood, spin once, and move through a short session together.",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF5A5060),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 54.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF56306F),
contentColor = Color.White
)
) {
Text("Choose category")
}
}
}
}
@Composable
private fun CompactPlayCard(
title: String,
subtitle: String,
icon: ImageVector,
tint: Color,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = modifier.heightIn(min = 154.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Surface(
shape = RoundedCornerShape(18.dp),
color = tint.copy(alpha = 0.14f),
modifier = Modifier.size(46.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
tint = tint,
modifier = Modifier.size(24.dp)
)
}
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF5A5060),
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = tint,
modifier = Modifier.size(18.dp)
)
}
}
}
}
}
@Composable
private fun WheelGlyph(
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(26.dp),
color = Color(0xFF56306F)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.background(
Brush.linearGradient(
listOf(Color(0xFFB98AF4), Color(0xFFE7A2D1), Color(0xFF56306F)),
start = Offset.Zero,
end = Offset.Infinite
)
)
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(34.dp)
)
}
}
}
@Preview
@Composable
fun PlayHubScreenPreview() {
PlayHubContent(onNavigate = {})
}

View File

@ -20,6 +20,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Lock
@ -330,6 +331,12 @@ fun SettingsScreen(
colors = CardDefaults.cardColors(containerColor = SettingsCard)
) {
Column {
SettingsRow(
icon = Icons.Filled.Done,
label = "Answer History",
onClick = { onNavigate(AppRoute.ANSWER_HISTORY) }
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
SettingsRow(
icon = Icons.Filled.Notifications,
label = "Notifications",

View File

@ -15,8 +15,13 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
@ -54,6 +59,7 @@ fun CategoryPickerScreen(
if (item.isLocked) onNavigate(AppRoute.PAYWALL)
else onNavigate(AppRoute.spinWheel(item.category.id))
},
onHistory = { onNavigate(AppRoute.WHEEL_HISTORY) },
onRetry = viewModel::load
)
}
@ -62,6 +68,7 @@ fun CategoryPickerScreen(
private fun CategoryPickerContent(
state: CategoryPickerUiState,
onCategorySelected: (CategoryPickerItem) -> Unit,
onHistory: () -> Unit,
onRetry: () -> Unit
) {
Box(
@ -84,25 +91,10 @@ private fun CategoryPickerContent(
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
item {
Column(
modifier = Modifier.padding(top = 20.dp, bottom = 4.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = "Choose the weather",
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Pick a category that matches where you are tonight. The wheel picks the question.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF5A5060),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
WheelPickerHeader(
onHistory = onHistory,
modifier = Modifier.padding(top = 20.dp, bottom = 4.dp)
)
}
when {
@ -125,6 +117,13 @@ private fun CategoryPickerContent(
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Categories unavailable", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = Color(0xFF261D2E))
Text(state.error, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF5A5060))
Button(
onClick = onRetry,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F))
) {
Text("Retry", color = Color.White)
}
}
}
}
@ -138,27 +137,138 @@ private fun CategoryPickerContent(
}
}
@Composable
private fun WheelPickerHeader(
onHistory: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Spin the Wheel",
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Choose a mood and let chance pick the next conversation.",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF5A5060),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
Button(
onClick = onHistory,
modifier = Modifier.heightIn(min = 48.dp),
shape = RoundedCornerShape(999.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White.copy(alpha = 0.82f),
contentColor = Color(0xFF56306F)
)
) {
Text("History")
}
}
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
color = Color(0xFFFFF8FC)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(18.dp),
color = Color(0xFFF0DFFF),
modifier = Modifier.size(46.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
tint = Color(0xFF56306F),
modifier = Modifier.size(24.dp)
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(3.dp)
) {
Text(
text = "Ten prompts per spin",
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "You can skip, continue, or end whenever the moment feels complete.",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF5A5060),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
@Composable
private fun CategoryCard(
item: CategoryPickerItem,
onClick: () -> Unit
) {
val containerColor = if (item.isLocked) Color(0xFFFFF8FC).copy(alpha = 0.84f) else Color.White.copy(alpha = 0.86f)
val containerColor = if (item.isLocked) Color(0xFFFFF8FC) else Color.White
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(containerColor = containerColor),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
elevation = CardDefaults.cardElevation(defaultElevation = if (item.isLocked) 2.dp else 6.dp)
) {
Row(
modifier = Modifier.padding(18.dp),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(18.dp),
color = if (item.isLocked) Color(0xFFF0EDF9) else Color(0xFFFFE8F4),
modifier = Modifier.size(48.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = if (item.isLocked) Icons.Default.Lock else Icons.Filled.Star,
contentDescription = null,
tint = if (item.isLocked) Color(0xFF9B8AA6) else Color(0xFF6D2B55),
modifier = Modifier.size(22.dp)
)
}
}
Column(
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
@ -174,14 +284,12 @@ private fun CategoryCard(
if (item.isLocked) CategoryPill("Premium")
}
}
if (item.isLocked) {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = "Locked",
tint = Color(0xFF9B8AA6),
modifier = Modifier.size(20.dp)
)
}
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = if (item.isLocked) "Unlock" else "Spin",
tint = if (item.isLocked) Color(0xFF9B8AA6) else Color(0xFF56306F),
modifier = Modifier.size(20.dp)
)
}
}
}
@ -224,6 +332,7 @@ fun CategoryPickerScreenPreview() {
)
),
onCategorySelected = {},
onHistory = {},
onRetry = {}
)
}

View File

@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -36,12 +37,13 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
@ -137,22 +139,11 @@ private fun SpinWheelContent(
contentAlignment = Alignment.Center,
modifier = Modifier.weight(1f)
) {
Surface(
modifier = Modifier
.size(220.dp)
.rotate(if (state.isSpinning) rotation else 0f),
shape = CircleShape,
color = Color(0xFFF0EDF9),
shadowElevation = 12.dp
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = if (state.spunAndReady) "" else "",
fontSize = 64.sp,
color = if (state.spunAndReady) Color(0xFFB98AF4) else Color(0xFF56306F)
)
}
}
WheelSpinner(
isSpinning = state.isSpinning,
spunAndReady = state.spunAndReady,
rotation = rotation
)
}
Column(
@ -212,7 +203,7 @@ private fun SpinWheelContent(
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F))
) {
Text("Spin", color = Color.White)
Text("Spin wheel", color = Color.White)
}
}
}
@ -221,6 +212,85 @@ private fun SpinWheelContent(
}
}
@Composable
private fun WheelSpinner(
isSpinning: Boolean,
spunAndReady: Boolean,
rotation: Float
) {
val segmentColors = listOf(
Color(0xFFB98AF4),
Color(0xFFFFC2DD),
Color(0xFFE7A2D1),
Color(0xFFF0DFFF),
Color(0xFF8F67C5),
Color(0xFFFFE8F4),
Color(0xFF56306F),
Color(0xFFF7C8E4)
)
Box(
modifier = Modifier.size(270.dp),
contentAlignment = Alignment.Center
) {
Canvas(
modifier = Modifier
.size(236.dp)
.rotate(if (isSpinning) rotation else 0f)
) {
val sweep = 360f / segmentColors.size
segmentColors.forEachIndexed { index, color ->
drawArc(
color = color,
startAngle = -90f + (index * sweep),
sweepAngle = sweep,
useCenter = true
)
}
drawCircle(
color = Color.White.copy(alpha = 0.78f),
radius = size.minDimension * 0.24f
)
drawCircle(
color = Color.White.copy(alpha = 0.84f),
style = Stroke(width = 7.dp.toPx())
)
}
Canvas(
modifier = Modifier
.align(Alignment.TopCenter)
.size(width = 34.dp, height = 28.dp)
) {
val path = Path().apply {
moveTo(size.width / 2f, size.height)
lineTo(0f, 0f)
lineTo(size.width, 0f)
close()
}
drawPath(path = path, color = Color(0xFF261D2E))
}
Surface(
modifier = Modifier.size(94.dp),
shape = CircleShape,
color = Color.White,
shadowElevation = 10.dp
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = if (spunAndReady) "Ready" else "Spin",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF56306F),
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@Preview
@Composable
fun SpinWheelScreenPreview() {