feat: category picker redesign, bucket list wiring, settings + nav updates
This commit is contained in:
parent
c816033e74
commit
a0fd1d56f6
|
|
@ -3,9 +3,9 @@ package app.closer.core.navigation
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.Favorite
|
||||||
import androidx.compose.material.icons.filled.Home
|
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.Settings
|
||||||
import androidx.compose.material.icons.filled.Star
|
import androidx.compose.material.icons.filled.Star
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
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.DateBuilderScreen
|
||||||
import app.closer.ui.dates.BucketListScreen
|
import app.closer.ui.dates.BucketListScreen
|
||||||
import app.closer.ui.paywall.PaywallScreen
|
import app.closer.ui.paywall.PaywallScreen
|
||||||
|
import app.closer.ui.play.PlayHubScreen
|
||||||
import app.closer.ui.questions.DailyQuestionScreen
|
import app.closer.ui.questions.DailyQuestionScreen
|
||||||
import app.closer.ui.questions.QuestionCategoryScreen
|
import app.closer.ui.questions.QuestionCategoryScreen
|
||||||
import app.closer.ui.questions.QuestionComposerScreen
|
import app.closer.ui.questions.QuestionComposerScreen
|
||||||
|
|
@ -170,6 +171,9 @@ fun AppNavigation(
|
||||||
composable(route = AppRoute.PARTNER_HOME) {
|
composable(route = AppRoute.PARTNER_HOME) {
|
||||||
PartnerHomeScreen(onNavigate = navigateRoute)
|
PartnerHomeScreen(onNavigate = navigateRoute)
|
||||||
}
|
}
|
||||||
|
composable(route = AppRoute.PLAY) {
|
||||||
|
PlayHubScreen(onNavigate = navigateRoute)
|
||||||
|
}
|
||||||
|
|
||||||
// Daily Question
|
// Daily Question
|
||||||
composable(route = AppRoute.DAILY_QUESTION) {
|
composable(route = AppRoute.DAILY_QUESTION) {
|
||||||
|
|
@ -349,8 +353,8 @@ private data class TopLevelRoute(
|
||||||
private val topLevelRoutes = listOf(
|
private val topLevelRoutes = listOf(
|
||||||
TopLevelRoute(AppRoute.HOME, "Home", Icons.Filled.Home),
|
TopLevelRoute(AppRoute.HOME, "Home", Icons.Filled.Home),
|
||||||
TopLevelRoute(AppRoute.DAILY_QUESTION, "Today", Icons.Filled.Favorite),
|
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.QUESTION_PACKS, "Packs", Icons.Filled.Star),
|
||||||
TopLevelRoute(AppRoute.ANSWER_HISTORY, "Answers", Icons.Filled.Done),
|
|
||||||
TopLevelRoute(AppRoute.SETTINGS, "Settings", Icons.Filled.Settings)
|
TopLevelRoute(AppRoute.SETTINGS, "Settings", Icons.Filled.Settings)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -364,6 +368,11 @@ private val shellBackRoutes = setOf(
|
||||||
AppRoute.SPIN_WHEEL,
|
AppRoute.SPIN_WHEEL,
|
||||||
AppRoute.WHEEL_SESSION,
|
AppRoute.WHEEL_SESSION,
|
||||||
AppRoute.WHEEL_COMPLETE,
|
AppRoute.WHEEL_COMPLETE,
|
||||||
|
AppRoute.WHEEL_HISTORY,
|
||||||
|
AppRoute.DATE_MATCH,
|
||||||
|
AppRoute.DATE_MATCHES,
|
||||||
|
AppRoute.DATE_BUILDER,
|
||||||
|
AppRoute.BUCKET_LIST,
|
||||||
AppRoute.ACCOUNT,
|
AppRoute.ACCOUNT,
|
||||||
AppRoute.SUBSCRIPTION,
|
AppRoute.SUBSCRIPTION,
|
||||||
AppRoute.PAYWALL
|
AppRoute.PAYWALL
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ object AppRoute {
|
||||||
const val CREATE_PROFILE = "create_profile"
|
const val CREATE_PROFILE = "create_profile"
|
||||||
const val HOME = "home"
|
const val HOME = "home"
|
||||||
const val PARTNER_HOME = "partner_home"
|
const val PARTNER_HOME = "partner_home"
|
||||||
|
const val PLAY = "play"
|
||||||
const val DAILY_QUESTION = "daily_question"
|
const val DAILY_QUESTION = "daily_question"
|
||||||
const val QUESTION_PACKS = "question_packs"
|
const val QUESTION_PACKS = "question_packs"
|
||||||
const val QUESTION_CATEGORY = "question_category/{categoryId}"
|
const val QUESTION_CATEGORY = "question_category/{categoryId}"
|
||||||
|
|
@ -56,6 +57,7 @@ object AppRoute {
|
||||||
Definition(FORGOT_PASSWORD, "Forgot Password", "auth"),
|
Definition(FORGOT_PASSWORD, "Forgot Password", "auth"),
|
||||||
Definition(HOME, "Home", "home"),
|
Definition(HOME, "Home", "home"),
|
||||||
Definition(PARTNER_HOME, "Partner", "home"),
|
Definition(PARTNER_HOME, "Partner", "home"),
|
||||||
|
Definition(PLAY, "Play", "play"),
|
||||||
Definition(DAILY_QUESTION, "Daily Question", "questions"),
|
Definition(DAILY_QUESTION, "Daily Question", "questions"),
|
||||||
Definition(QUESTION_PACKS, "Question Packs", "questions"),
|
Definition(QUESTION_PACKS, "Question Packs", "questions"),
|
||||||
Definition(QUESTION_CATEGORY, "Question Pack", "questions"),
|
Definition(QUESTION_CATEGORY, "Question Pack", "questions"),
|
||||||
|
|
@ -89,8 +91,8 @@ object AppRoute {
|
||||||
val topLevelRoutes = setOf(
|
val topLevelRoutes = setOf(
|
||||||
HOME,
|
HOME,
|
||||||
DAILY_QUESTION,
|
DAILY_QUESTION,
|
||||||
|
PLAY,
|
||||||
QUESTION_PACKS,
|
QUESTION_PACKS,
|
||||||
ANSWER_HISTORY,
|
|
||||||
SETTINGS
|
SETTINGS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,24 +28,22 @@ class BucketListRepositoryImpl @Inject constructor(
|
||||||
dataSource.updateItem(item.coupleId, item)
|
dataSource.updateItem(item.coupleId, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getItem(itemId: String): BucketListItem? {
|
override suspend fun getItem(coupleId: String, itemId: String): BucketListItem? {
|
||||||
// Need coupleId to fetch - in practice, this would be called from a context
|
return dataSource.getItem(coupleId, itemId)
|
||||||
// where the coupleId is known. For now, return null.
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getItems(coupleId: String): List<BucketListItem> {
|
override suspend fun getItems(coupleId: String): List<BucketListItem> {
|
||||||
return dataSource.getItems(coupleId)
|
return dataSource.getItems(coupleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteItem(itemId: String) {
|
override suspend fun deleteItem(coupleId: String, itemId: String) {
|
||||||
// Need coupleId to delete - same limitation as getItem
|
dataSource.deleteItem(coupleId, itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Completion methods ──────────────────────────────────────────────────
|
// ─── Completion methods ──────────────────────────────────────────────────
|
||||||
|
|
||||||
override suspend fun completeItem(itemId: String, completedBy: String) {
|
override suspend fun completeItem(coupleId: String, itemId: String, completedBy: String) {
|
||||||
// Need coupleId - same limitation
|
dataSource.completeItem(coupleId, itemId, completedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Category methods ────────────────────────────────────────────────────
|
// ─── Category methods ────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,18 @@ interface BucketListRepository {
|
||||||
suspend fun updateItem(item: BucketListItem)
|
suspend fun updateItem(item: BucketListItem)
|
||||||
|
|
||||||
/** Get a bucket list item by ID. */
|
/** 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. */
|
/** Get all bucket list items for a couple. */
|
||||||
suspend fun getItems(coupleId: String): List<BucketListItem>
|
suspend fun getItems(coupleId: String): List<BucketListItem>
|
||||||
|
|
||||||
/** Delete a bucket list item. */
|
/** Delete a bucket list item. */
|
||||||
suspend fun deleteItem(itemId: String)
|
suspend fun deleteItem(coupleId: String, itemId: String)
|
||||||
|
|
||||||
// ─── Completion methods ──────────────────────────────────────────────────
|
// ─── Completion methods ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Mark an item as completed. */
|
/** Mark an item as completed. */
|
||||||
suspend fun completeItem(itemId: String, completedBy: String)
|
suspend fun completeItem(coupleId: String, itemId: String, completedBy: String)
|
||||||
|
|
||||||
// ─── Category methods ────────────────────────────────────────────────────
|
// ─── Category methods ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ class BucketListViewModel @Inject constructor(
|
||||||
|
|
||||||
fun toggleComplete(itemId: String) {
|
fun toggleComplete(itemId: String) {
|
||||||
val item = _uiState.value.items.find { it.id == itemId } ?: return
|
val item = _uiState.value.items.find { it.id == itemId } ?: return
|
||||||
|
val coupleId = _uiState.value.coupleId
|
||||||
|
if (coupleId.isEmpty()) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (item.isCompleted) {
|
if (item.isCompleted) {
|
||||||
|
|
@ -72,7 +74,7 @@ class BucketListViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
repository.completeItem(itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "")
|
repository.completeItem(coupleId, itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "")
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
items = it.items.map {
|
items = it.items.map {
|
||||||
|
|
@ -86,8 +88,11 @@ class BucketListViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteItem(itemId: String) {
|
fun deleteItem(itemId: String) {
|
||||||
|
val coupleId = _uiState.value.coupleId
|
||||||
|
if (coupleId.isEmpty()) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.deleteItem(itemId)
|
repository.deleteItem(coupleId, itemId)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(items = it.items.filter { it.id != itemId })
|
it.copy(items = it.items.filter { it.id != itemId })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {})
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
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.Favorite
|
||||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||||
import androidx.compose.material.icons.filled.Lock
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
|
@ -330,6 +331,12 @@ fun SettingsScreen(
|
||||||
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
colors = CardDefaults.cardColors(containerColor = SettingsCard)
|
||||||
) {
|
) {
|
||||||
Column {
|
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(
|
SettingsRow(
|
||||||
icon = Icons.Filled.Notifications,
|
icon = Icons.Filled.Notifications,
|
||||||
label = "Notifications",
|
label = "Notifications",
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,13 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Lock
|
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.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
|
@ -54,6 +59,7 @@ fun CategoryPickerScreen(
|
||||||
if (item.isLocked) onNavigate(AppRoute.PAYWALL)
|
if (item.isLocked) onNavigate(AppRoute.PAYWALL)
|
||||||
else onNavigate(AppRoute.spinWheel(item.category.id))
|
else onNavigate(AppRoute.spinWheel(item.category.id))
|
||||||
},
|
},
|
||||||
|
onHistory = { onNavigate(AppRoute.WHEEL_HISTORY) },
|
||||||
onRetry = viewModel::load
|
onRetry = viewModel::load
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +68,7 @@ fun CategoryPickerScreen(
|
||||||
private fun CategoryPickerContent(
|
private fun CategoryPickerContent(
|
||||||
state: CategoryPickerUiState,
|
state: CategoryPickerUiState,
|
||||||
onCategorySelected: (CategoryPickerItem) -> Unit,
|
onCategorySelected: (CategoryPickerItem) -> Unit,
|
||||||
|
onHistory: () -> Unit,
|
||||||
onRetry: () -> Unit
|
onRetry: () -> Unit
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -84,25 +91,10 @@ private fun CategoryPickerContent(
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column(
|
WheelPickerHeader(
|
||||||
modifier = Modifier.padding(top = 20.dp, bottom = 4.dp),
|
onHistory = onHistory,
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
modifier = Modifier.padding(top = 20.dp, bottom = 4.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
|
@ -125,6 +117,13 @@ private fun CategoryPickerContent(
|
||||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Text("Categories unavailable", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = Color(0xFF261D2E))
|
Text("Categories unavailable", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = Color(0xFF261D2E))
|
||||||
Text(state.error, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF5A5060))
|
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
|
@Composable
|
||||||
private fun CategoryCard(
|
private fun CategoryCard(
|
||||||
item: CategoryPickerItem,
|
item: CategoryPickerItem,
|
||||||
onClick: () -> Unit
|
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(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(22.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = if (item.isLocked) 2.dp else 6.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(18.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -174,14 +284,12 @@ private fun CategoryCard(
|
||||||
if (item.isLocked) CategoryPill("Premium")
|
if (item.isLocked) CategoryPill("Premium")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item.isLocked) {
|
Icon(
|
||||||
Icon(
|
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
imageVector = Icons.Default.Lock,
|
contentDescription = if (item.isLocked) "Unlock" else "Spin",
|
||||||
contentDescription = "Locked",
|
tint = if (item.isLocked) Color(0xFF9B8AA6) else Color(0xFF56306F),
|
||||||
tint = Color(0xFF9B8AA6),
|
modifier = Modifier.size(20.dp)
|
||||||
modifier = Modifier.size(20.dp)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -224,6 +332,7 @@ fun CategoryPickerScreenPreview() {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
|
onHistory = {},
|
||||||
onRetry = {}
|
onRetry = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -137,22 +139,11 @@ private fun SpinWheelContent(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Surface(
|
WheelSpinner(
|
||||||
modifier = Modifier
|
isSpinning = state.isSpinning,
|
||||||
.size(220.dp)
|
spunAndReady = state.spunAndReady,
|
||||||
.rotate(if (state.isSpinning) rotation else 0f),
|
rotation = rotation
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -212,7 +203,7 @@ private fun SpinWheelContent(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF56306F))
|
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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun SpinWheelScreenPreview() {
|
fun SpinWheelScreenPreview() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue