feat: couple intimacy seed data, "How Well" screen, ThisOrThat + wheel polish

This commit is contained in:
null 2026-06-17 21:57:40 -05:00
parent 254652cb86
commit 45058fdd5f
11 changed files with 5052 additions and 16 deletions

View File

@ -48,6 +48,7 @@ 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.howwell.HowWellScreen
import app.closer.ui.thisorthat.ThisOrThatScreen
import app.closer.ui.questions.DailyQuestionScreen
import app.closer.ui.questions.QuestionCategoryScreen
@ -302,6 +303,9 @@ fun AppNavigation(
composable(route = AppRoute.THIS_OR_THAT) {
ThisOrThatScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.HOW_WELL) {
HowWellScreen(onNavigate = navigateRoute)
}
// Dates
composable(route = AppRoute.DATE_MATCH) {
@ -378,6 +382,7 @@ private val shellBackRoutes = setOf(
AppRoute.DATE_BUILDER,
AppRoute.BUCKET_LIST,
AppRoute.THIS_OR_THAT,
AppRoute.HOW_WELL,
AppRoute.ACCOUNT,
AppRoute.SUBSCRIPTION,
AppRoute.PAYWALL

View File

@ -39,6 +39,7 @@ object AppRoute {
const val DATE_BUILDER = "date_builder"
const val BUCKET_LIST = "bucket_list"
const val THIS_OR_THAT = "this_or_that"
const val HOW_WELL = "how_well"
// Question thread: coupleId and questionId are required; prevId and nextId are optional.
const val QUESTION_THREAD =
@ -87,7 +88,8 @@ object AppRoute {
Definition(DATE_MATCHES, "Matches", "dates"),
Definition(DATE_BUILDER, "Plan a Date", "dates"),
Definition(BUCKET_LIST, "Our Bucket List", "dates"),
Definition(THIS_OR_THAT, "This or That", "play")
Definition(THIS_OR_THAT, "This or That", "play"),
Definition(HOW_WELL, "How Well Do You Know Me", "play")
)
val topLevelRoutes = setOf(

View File

@ -24,6 +24,9 @@ interface QuestionDao {
@Query("SELECT * FROM question WHERE type = :type AND status = 'active'")
suspend fun getQuestionsByType(type: String): List<QuestionEntity>
@Query("SELECT * FROM question WHERE type IN ('single_choice', 'this_or_that', 'scale') AND status = 'active'")
suspend fun getQuestionsForPrediction(): List<QuestionEntity>
@Query("SELECT * FROM question WHERE is_premium = 0 AND status = 'active'")
suspend fun getFreeQuestions(): List<QuestionEntity>

View File

@ -18,4 +18,6 @@ class FakeQuestionRepository : QuestionRepository {
override suspend fun getQuestionCountByCategory(categoryId: String): Int = 0
override suspend fun getQuestionsByType(type: String): List<Question> = emptyList()
override suspend fun getQuestionsForPrediction(): List<Question> = emptyList()
}

View File

@ -42,4 +42,8 @@ class RoomQuestionRepository @Inject constructor(
override suspend fun getQuestionsByType(type: String): List<Question> {
return questionDao.getQuestionsByType(type).map { it.toQuestion() }
}
override suspend fun getQuestionsForPrediction(): List<Question> {
return questionDao.getQuestionsForPrediction().map { it.toQuestion() }
}
}

View File

@ -11,4 +11,5 @@ interface QuestionRepository {
suspend fun getCategoryById(id: String): QuestionCategory?
suspend fun getQuestionCountByCategory(categoryId: String): Int
suspend fun getQuestionsByType(type: String): List<Question>
suspend fun getQuestionsForPrediction(): List<Question>
}

View File

@ -0,0 +1,918 @@
package app.closer.ui.howwell
import android.util.Log
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
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
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute
import app.closer.domain.model.AnswerConfig
import app.closer.domain.model.ChoiceAnswerConfigImpl
import app.closer.domain.model.ChoiceOption
import app.closer.domain.model.Question
import app.closer.domain.model.ScaleAnswerConfigImpl
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
import app.closer.domain.repository.QuestionRepository
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.math.abs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
// ── Domain ────────────────────────────────────────────────────────────────────
data class HowWellAnswer(
val selectedOptionId: String? = null,
val scaleValue: Int? = null
) {
val isEmpty get() = selectedOptionId == null && scaleValue == null
}
fun HowWellAnswer.isMatch(other: HowWellAnswer): Boolean = when {
selectedOptionId != null -> selectedOptionId == other.selectedOptionId
scaleValue != null && other.scaleValue != null -> scaleValue == other.scaleValue
else -> false
}
fun HowWellAnswer.isClose(other: HowWellAnswer): Boolean =
scaleValue != null && other.scaleValue != null &&
!isMatch(other) && abs(scaleValue - other.scaleValue) == 1
fun HowWellAnswer.displayText(config: AnswerConfig?): String = when {
selectedOptionId != null -> when (config) {
is ChoiceAnswerConfigImpl -> config.config.options.find { it.id == selectedOptionId }?.text ?: selectedOptionId
is ThisOrThatAnswerConfigImpl -> when (selectedOptionId) {
config.config.optionA.id -> config.config.optionA.text
config.config.optionB.id -> config.config.optionB.text
else -> selectedOptionId
}
else -> selectedOptionId
}
scaleValue != null -> "$scaleValue"
else -> ""
}
data class HowWellResult(
val question: Question,
val playerAAnswer: HowWellAnswer,
val playerBAnswer: HowWellAnswer,
val isMatch: Boolean,
val isClose: Boolean
)
enum class HowWellPhase {
LOADING, PLAYER_A_INTRO, PLAYER_A_TURN, HANDOFF,
PLAYER_B_TURN, REVEALING, COMPLETE
}
data class HowWellUiState(
val phase: HowWellPhase = HowWellPhase.LOADING,
val questions: List<Question> = emptyList(),
val currentIndex: Int = 0,
val playerAAnswers: List<HowWellAnswer> = emptyList(),
val results: List<HowWellResult> = emptyList(),
val selectedOptionId: String? = null,
val selectedScale: Int? = null,
val score: Int = 0,
val error: String? = null
)
// ── ViewModel ─────────────────────────────────────────────────────────────────
@HiltViewModel
class HowWellViewModel @Inject constructor(
private val repository: QuestionRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(HowWellUiState())
val uiState: StateFlow<HowWellUiState> = _uiState.asStateFlow()
init { load() }
private fun load() {
viewModelScope.launch {
val questions = runCatching {
repository.getQuestionsForPrediction().shuffled().take(SESSION_SIZE)
}
.onFailure { Log.w(TAG, "Failed to load prediction questions", it) }
.getOrElse { emptyList() }
_uiState.update {
it.copy(
phase = if (questions.isEmpty()) HowWellPhase.LOADING else HowWellPhase.PLAYER_A_INTRO,
questions = questions,
error = if (questions.isEmpty()) "No questions available." else null
)
}
}
}
fun selectOption(id: String) = _uiState.update { it.copy(selectedOptionId = id, selectedScale = null) }
fun selectScale(v: Int) = _uiState.update { it.copy(selectedScale = v, selectedOptionId = null) }
fun startPlayerA() = _uiState.update { it.copy(phase = HowWellPhase.PLAYER_A_TURN, currentIndex = 0) }
fun confirmAnswer() {
val s = _uiState.value
val answer = HowWellAnswer(s.selectedOptionId, s.selectedScale)
if (answer.isEmpty) return
val newAnswers = s.playerAAnswers + answer
val next = s.currentIndex + 1
_uiState.update {
it.copy(
playerAAnswers = newAnswers,
selectedOptionId = null,
selectedScale = null,
currentIndex = if (next < it.questions.size) next else it.currentIndex,
phase = if (next >= it.questions.size) HowWellPhase.HANDOFF else HowWellPhase.PLAYER_A_TURN
)
}
}
fun readyForPlayerB() = _uiState.update {
it.copy(phase = HowWellPhase.PLAYER_B_TURN, currentIndex = 0, selectedOptionId = null, selectedScale = null)
}
fun confirmPrediction() {
val s = _uiState.value
val prediction = HowWellAnswer(s.selectedOptionId, s.selectedScale)
if (prediction.isEmpty) return
val actual = s.playerAAnswers.getOrNull(s.currentIndex) ?: return
val question = s.questions.getOrNull(s.currentIndex) ?: return
val match = prediction.isMatch(actual)
val close = prediction.isClose(actual)
_uiState.update {
it.copy(
results = it.results + HowWellResult(question, actual, prediction, match, close),
selectedOptionId = null,
selectedScale = null,
score = if (match) it.score + 1 else it.score,
phase = HowWellPhase.REVEALING
)
}
}
fun nextQuestion() {
val next = _uiState.value.currentIndex + 1
val total = _uiState.value.questions.size
_uiState.update {
it.copy(
currentIndex = if (next < total) next else it.currentIndex,
phase = if (next >= total) HowWellPhase.COMPLETE else HowWellPhase.PLAYER_B_TURN
)
}
}
fun restart() {
_uiState.value = HowWellUiState()
load()
}
companion object {
const val SESSION_SIZE = 10
private const val TAG = "HowWellViewModel"
}
}
// ── Root screen ───────────────────────────────────────────────────────────────
@Composable
fun HowWellScreen(
onNavigate: (String) -> Unit = {},
viewModel: HowWellViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
Box(
modifier = Modifier
.fillMaxSize()
.background(closerBackgroundBrush())
) {
when (state.phase) {
HowWellPhase.LOADING -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = CloserPalette.PurpleDeep
)
HowWellPhase.PLAYER_A_INTRO -> PlayerIntroScreen(
playerNumber = 1,
total = state.questions.size,
onReady = viewModel::startPlayerA
)
HowWellPhase.PLAYER_A_TURN -> {
val q = state.questions.getOrNull(state.currentIndex) ?: return@Box
AnswerScreen(
question = q,
index = state.currentIndex,
total = state.questions.size,
isPlayerB = false,
selectedOptionId = state.selectedOptionId,
selectedScale = state.selectedScale,
onSelectOption = viewModel::selectOption,
onSelectScale = viewModel::selectScale,
onConfirm = viewModel::confirmAnswer,
onQuit = { onNavigate(AppRoute.PLAY) }
)
}
HowWellPhase.HANDOFF -> HandoffScreen(onReady = viewModel::readyForPlayerB)
HowWellPhase.PLAYER_B_TURN -> {
val q = state.questions.getOrNull(state.currentIndex) ?: return@Box
AnswerScreen(
question = q,
index = state.currentIndex,
total = state.questions.size,
isPlayerB = true,
selectedOptionId = state.selectedOptionId,
selectedScale = state.selectedScale,
onSelectOption = viewModel::selectOption,
onSelectScale = viewModel::selectScale,
onConfirm = viewModel::confirmPrediction,
onQuit = { onNavigate(AppRoute.PLAY) }
)
}
HowWellPhase.REVEALING -> {
val result = state.results.lastOrNull() ?: return@Box
RevealScreen(
result = result,
questionNumber = state.currentIndex + 1,
total = state.questions.size,
score = state.score,
onNext = viewModel::nextQuestion
)
}
HowWellPhase.COMPLETE -> CompleteScreen(
score = state.score,
total = state.questions.size,
results = state.results,
onPlayAgain = viewModel::restart,
onHome = { onNavigate(AppRoute.PLAY) }
)
}
}
}
// ── Phase screens ─────────────────────────────────────────────────────────────
@Composable
private fun PlayerIntroScreen(playerNumber: Int, total: Int, onReady: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 28.dp, vertical = 40.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = if (playerNumber == 1) "👤" else "🔮", fontSize = 56.sp, textAlign = TextAlign.Center)
Spacer(Modifier.height(20.dp))
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
Text(
text = "Player $playerNumber",
modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelLarge,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
)
}
Spacer(Modifier.height(16.dp))
Text(
text = if (playerNumber == 1)
"Answer $total questions honestly.\nYour partner will try to predict what you said."
else
"For each question, guess what your partner answered.\nNo peeking!",
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
textAlign = TextAlign.Center,
lineHeight = MaterialTheme.typography.headlineSmall.lineHeight
)
if (playerNumber == 1) {
Spacer(Modifier.height(10.dp))
Text(
text = "Ask your partner to look away.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
Spacer(Modifier.height(36.dp))
Button(
onClick = onReady,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { Text("I'm ready", color = Color.White) }
}
}
@Composable
private fun HandoffScreen(onReady: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 28.dp, vertical = 40.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("🎉", fontSize = 56.sp, textAlign = TextAlign.Center)
Spacer(Modifier.height(20.dp))
Text(
text = "Pass the phone!",
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
textAlign = TextAlign.Center
)
Spacer(Modifier.height(10.dp))
Text(
text = "Player 1 is done. Hand the phone to Player 2 — keep your answers secret!",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF5A5060),
textAlign = TextAlign.Center
)
Spacer(Modifier.height(36.dp))
Button(
onClick = onReady,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { Text("I'm Player 2, let's go!", color = Color.White) }
}
}
@Composable
private fun AnswerScreen(
question: Question,
index: Int,
total: Int,
isPlayerB: Boolean,
selectedOptionId: String?,
selectedScale: Int?,
onSelectOption: (String) -> Unit,
onSelectScale: (Int) -> Unit,
onConfirm: () -> Unit,
onQuit: () -> Unit
) {
val hasSelection = selectedOptionId != null || selectedScale != null
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
Text(
text = "Player ${if (isPlayerB) 2 else 1} · ${index + 1} / $total",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
)
}
TextButton(onClick = onQuit) {
Text("Quit", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
LinearProgressIndicator(
progress = { index.toFloat() / total },
modifier = Modifier.fillMaxWidth(),
color = CloserPalette.PurpleDeep,
trackColor = CloserPalette.PurpleMist
)
Card(
modifier = Modifier.fillMaxWidth().weight(1f),
shape = RoundedCornerShape(28.dp),
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.9f)),
elevation = CardDefaults.cardElevation(8.dp)
) {
Box(modifier = Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (isPlayerB) {
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
Text(
text = "What did Player 1 say?",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
)
}
}
Text(
text = question.text,
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
textAlign = TextAlign.Center,
maxLines = 5,
overflow = TextOverflow.Ellipsis
)
}
}
}
when (val config = question.answerConfig) {
is ChoiceAnswerConfigImpl -> SingleChoiceInput(
options = config.config.options,
selectedId = selectedOptionId,
onSelect = onSelectOption
)
is ThisOrThatAnswerConfigImpl -> BinaryChoiceInput(
optionA = config.config.optionA,
optionB = config.config.optionB,
selectedId = selectedOptionId,
onSelect = onSelectOption
)
is ScaleAnswerConfigImpl -> ScaleInput(
min = config.config.minScale,
max = config.config.maxScale,
minLabel = config.config.minLabel,
maxLabel = config.config.maxLabel,
selected = selectedScale,
onSelect = onSelectScale
)
else -> {}
}
Button(
onClick = onConfirm,
enabled = hasSelection,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) {
Text(
text = if (index + 1 >= total && !isPlayerB) "Done →" else "Confirm →",
color = Color.White
)
}
}
}
@Composable
private fun RevealScreen(
result: HowWellResult,
questionNumber: Int,
total: Int,
score: Int,
onNext: () -> Unit
) {
val matchColor = Color(0xFF2E7D32)
val closeColor = Color(0xFFF57F17)
val missColor = Color(0xFFC62828)
val accentColor = when {
result.isMatch -> matchColor
result.isClose -> closeColor
else -> missColor
}
val bgColor = when {
result.isMatch -> Color(0xFFE8F5E9)
result.isClose -> Color(0xFFFFF8E1)
else -> Color(0xFFFCE4EC)
}
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "$questionNumber / $total",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Surface(shape = RoundedCornerShape(999.dp), color = CloserPalette.PurpleMist) {
Text(
text = "Score: $score / $questionNumber",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.SemiBold
)
}
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = bgColor),
elevation = CardDefaults.cardElevation(0.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (result.isMatch) "" else if (result.isClose) "" else "",
fontSize = 28.sp,
color = accentColor
)
Text(
text = if (result.isMatch) "Match!" else if (result.isClose) "So close!" else "Not quite",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = accentColor
)
}
}
Text(
text = result.question.text,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
AnswerRevealCard(
label = "Player 1 said",
text = result.playerAAnswer.displayText(result.question.answerConfig),
isMatch = result.isMatch,
modifier = Modifier.weight(1f)
)
AnswerRevealCard(
label = "Player 2 guessed",
text = result.playerBAnswer.displayText(result.question.answerConfig),
isMatch = result.isMatch,
modifier = Modifier.weight(1f)
)
}
Spacer(Modifier.weight(1f))
Button(
onClick = onNext,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) {
Text(
text = if (questionNumber >= total) "See results" else "Next →",
color = Color.White
)
}
}
}
@Composable
private fun AnswerRevealCard(
label: String,
text: String,
isMatch: Boolean,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.heightIn(min = 90.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (isMatch) Color(0xFFE8F5E9) else MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = if (isMatch) Color(0xFF2E7D32) else MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold),
color = if (isMatch) Color(0xFF1B5E20) else MaterialTheme.colorScheme.onSurface,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
private fun CompleteScreen(
score: Int,
total: Int,
results: List<HowWellResult>,
onPlayAgain: () -> Unit,
onHome: () -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.padding(horizontal = 20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
item {
Column(
modifier = Modifier.fillMaxWidth().padding(top = 32.dp, bottom = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text("🎯", fontSize = 56.sp, textAlign = TextAlign.Center)
Text(
text = "$score / $total",
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
color = CloserPalette.PurpleDeep,
textAlign = TextAlign.Center
)
Text(
text = scoreLabel(score, total),
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold),
color = Color(0xFF261D2E),
textAlign = TextAlign.Center
)
}
}
item {
Text(
text = "Breakdown",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onBackground
)
}
items(results) { result -> BreakdownRow(result) }
item {
Column(
modifier = Modifier.padding(top = 8.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(
onClick = onPlayAgain,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep)
) { Text("Play again", color = Color.White) }
OutlinedButton(
onClick = onHome,
modifier = Modifier.fillMaxWidth().heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp)
) { Text("Back to Play") }
}
}
}
}
@Composable
private fun BreakdownRow(result: HowWellResult) {
val matchColor = Color(0xFF2E7D32)
val closeColor = Color(0xFFF57F17)
val missColor = Color(0xFFC62828)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(
containerColor = if (result.isMatch) Color(0xFFE8F5E9) else MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(2.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (result.isMatch) "" else if (result.isClose) "" else "",
fontSize = 18.sp,
color = if (result.isMatch) matchColor else if (result.isClose) closeColor else missColor
)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = result.question.text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
val config = result.question.answerConfig
if (result.isMatch) {
Text(
text = result.playerAAnswer.displayText(config),
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold),
color = matchColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
} else {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = result.playerAAnswer.displayText(config),
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (result.isClose) closeColor else missColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
)
Text(
text = "${result.playerBAnswer.displayText(config)}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
)
}
}
}
}
}
}
// ── Answer input widgets ──────────────────────────────────────────────────────
@Composable
private fun SingleChoiceInput(
options: List<ChoiceOption>,
selectedId: String?,
onSelect: (String) -> Unit
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
options.forEach { option ->
val selected = selectedId == option.id
Card(
onClick = { onSelect(option.id) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(
containerColor = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
) {
Text(
text = option.text,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium),
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@Composable
private fun BinaryChoiceInput(
optionA: ChoiceOption,
optionB: ChoiceOption,
selectedId: String?,
onSelect: (String) -> Unit
) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
listOf(optionA to "A", optionB to "B").forEach { (option, label) ->
val selected = selectedId == option.id
Card(
onClick = { onSelect(option.id) },
modifier = Modifier.weight(1f).heightIn(min = 80.dp),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(
containerColor = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(if (selected) 6.dp else 2.dp)
) {
Column(
modifier = Modifier.fillMaxWidth().padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = if (selected) Color.White.copy(alpha = 0.7f) else MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = option.text,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
@Composable
private fun ScaleInput(
min: Int,
max: Int,
minLabel: String,
maxLabel: String,
selected: Int?,
onSelect: (Int) -> Unit
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
(min..max).forEach { value ->
val isSelected = selected == value
Surface(
onClick = { onSelect(value) },
shape = CircleShape,
color = if (isSelected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.surface,
modifier = Modifier.size(52.dp),
shadowElevation = if (isSelected) 6.dp else 2.dp
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = "$value",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = if (isSelected) Color.White else MaterialTheme.colorScheme.onSurface
)
}
}
}
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(minLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(maxLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────────
private fun scoreLabel(score: Int, total: Int): String = when {
score == total -> "Mind reader! 🤯"
score >= total * 0.8 -> "You really know each other 💜"
score >= total * 0.6 -> "Pretty good!"
score >= total * 0.4 -> "Getting there!"
else -> "Room to grow 🌱"
}

View File

@ -102,6 +102,12 @@ private fun PlayHubContent(
)
}
item {
HowWellCard(
onClick = { onNavigate(AppRoute.HOW_WELL) }
)
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
@ -231,6 +237,84 @@ private fun ThisOrThatCard(
}
}
@Composable
private fun HowWellCard(
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(18.dp),
color = CloserPalette.Romantic.copy(alpha = 0.12f),
modifier = Modifier.size(52.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = "💜",
style = MaterialTheme.typography.titleMedium
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "How Well Do You Know Me",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Surface(
shape = RoundedCornerShape(999.dp),
color = CloserPalette.Romantic.copy(alpha = 0.12f)
) {
Text(
text = "10 rounds",
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = CloserPalette.Romantic,
fontWeight = FontWeight.SemiBold
)
}
}
Text(
text = "One answers, the other predicts. Find out how well you really know each other.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = CloserPalette.Romantic,
modifier = Modifier.size(18.dp)
)
}
}
}
@Composable
private fun FeaturedPlayCard(
onClick: () -> Unit

View File

@ -2,11 +2,18 @@ package app.closer.ui.thisorthat
import android.util.Log
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -18,30 +25,34 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
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
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
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
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -196,6 +207,12 @@ private fun ThisOrThatContent(
onSelect: (String) -> Unit,
onBack: () -> Unit
) {
val animatedProgress by animateFloatAsState(
targetValue = ((currentIndex + 1).toFloat() / total.coerceAtLeast(1)).coerceIn(0f, 1f),
animationSpec = tween(durationMillis = 260),
label = "this_or_that_progress"
)
Column(
modifier = Modifier
.fillMaxSize()
@ -226,6 +243,8 @@ private fun ThisOrThatContent(
}
}
ThisOrThatProgress(progress = animatedProgress)
Card(
modifier = Modifier
.fillMaxWidth()
@ -240,6 +259,9 @@ private fun ThisOrThatContent(
.padding(24.dp),
contentAlignment = Alignment.Center
) {
ChoicePromptBackdrop(
modifier = Modifier.matchParentSize()
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
@ -278,12 +300,8 @@ private fun ThisOrThatContent(
onSelect = onSelect
)
Text(
text = "or",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
VersusBadge(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
OptionCard(
@ -298,6 +316,69 @@ private fun ThisOrThatContent(
}
}
@Composable
private fun ThisOrThatProgress(
progress: Float
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PinkMist)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(progress.coerceIn(0f, 1f))
.clip(RoundedCornerShape(999.dp))
.background(CloserPalette.PurpleDeep)
)
}
}
@Composable
private fun ChoicePromptBackdrop(
modifier: Modifier = Modifier
) {
Canvas(modifier = modifier) {
val minDimension = size.minDimension
val radius = minDimension * 0.16f
val left = Offset(size.width * 0.18f, size.height * 0.18f)
val right = Offset(size.width * 0.82f, size.height * 0.82f)
val strokeWidth = 3.dp.toPx()
drawLine(
color = CloserPalette.PurpleMist.copy(alpha = 0.9f),
start = left,
end = right,
strokeWidth = strokeWidth
)
drawCircle(
color = CloserPalette.PurpleGlow.copy(alpha = 0.58f),
radius = radius,
center = left
)
drawCircle(
color = CloserPalette.PinkSoft.copy(alpha = 0.76f),
radius = radius * 0.94f,
center = right
)
drawCircle(
color = CloserPalette.PurpleDeep.copy(alpha = 0.12f),
radius = radius * 0.56f,
center = left,
style = Stroke(width = strokeWidth)
)
drawCircle(
color = CloserPalette.PinkAccentDeep.copy(alpha = 0.1f),
radius = radius * 0.54f,
center = right,
style = Stroke(width = strokeWidth)
)
}
}
@Composable
private fun OptionCard(
text: String,
@ -310,6 +391,15 @@ private fun OptionCard(
val isSelected = pendingSelection == optionId
val isOtherSelected = pendingSelection != null && !isSelected
val cardScale by animateFloatAsState(
targetValue = when {
isSelected -> 1.02f
isOtherSelected -> 0.98f
else -> 1f
},
animationSpec = tween(durationMillis = 180),
label = "scale_$optionId"
)
val background by animateColorAsState(
targetValue = when {
isSelected -> accentColor
@ -333,7 +423,8 @@ private fun OptionCard(
onClick = { if (pendingSelection == null) onSelect(optionId) },
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 72.dp),
.heightIn(min = 72.dp)
.scale(cardScale),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = background),
elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 10.dp else 4.dp)
@ -370,6 +461,37 @@ private fun OptionCard(
}
}
@Composable
private fun VersusBadge(
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "versus_badge")
val pulse by infiniteTransition.animateFloat(
initialValue = 0.96f,
targetValue = 1.04f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1100),
repeatMode = RepeatMode.Reverse
),
label = "versus_badge_pulse"
)
Surface(
modifier = modifier.scale(pulse),
shape = RoundedCornerShape(999.dp),
color = CloserPalette.PurpleMist,
shadowElevation = 4.dp
) {
Text(
text = "OR",
modifier = Modifier.padding(horizontal = 18.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelLarge,
color = CloserPalette.PurpleDeep,
fontWeight = FontWeight.Bold
)
}
}
@Composable
private fun ThisOrThatComplete(
aCount: Int,
@ -389,7 +511,7 @@ private fun ThisOrThatComplete(
) {
Spacer(Modifier.weight(1f))
Text("", fontSize = 64.sp, color = CloserPalette.PurpleDeep)
ChoiceCompleteBadge()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@ -424,7 +546,7 @@ private fun ThisOrThatComplete(
verticalAlignment = Alignment.CenterVertically
) {
TallyItem(label = "A", count = aCount, color = CloserPalette.PurpleDeep)
Divider(
VerticalDivider(
modifier = Modifier
.height(48.dp)
.width(1.dp),
@ -459,6 +581,41 @@ private fun ThisOrThatComplete(
}
}
@Composable
private fun ChoiceCompleteBadge() {
val infiniteTransition = rememberInfiniteTransition(label = "choice_complete")
val pulse by infiniteTransition.animateFloat(
initialValue = 0.96f,
targetValue = 1.04f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1300),
repeatMode = RepeatMode.Reverse
),
label = "choice_complete_pulse"
)
Surface(
modifier = Modifier
.size(104.dp)
.scale(pulse),
shape = CircleShape,
color = CloserPalette.PinkMist,
shadowElevation = 10.dp
) {
Box(
modifier = Modifier.background(CloserPalette.PurpleMist.copy(alpha = 0.42f)),
contentAlignment = Alignment.Center
) {
Text(
text = "A/B",
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
color = CloserPalette.PurpleDeep,
textAlign = TextAlign.Center
)
}
}
}
@Composable
private fun TallyItem(label: String, count: Int, color: Color) {
Column(

View File

@ -3,9 +3,11 @@ package app.closer.ui.wheel
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -34,6 +36,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
@ -176,7 +179,11 @@ private fun SpinWheelContent(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp),
shape = RoundedCornerShape(18.dp)
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, CloserPalette.PurpleDeep.copy(alpha = 0.44f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = CloserPalette.PurpleDeep
)
) {
Text("Spin again")
}
@ -214,9 +221,31 @@ private fun WheelSpinner(
rotation: Float
) {
val segmentColors = closerWheelSegmentColors()
val idleTransition = rememberInfiniteTransition(label = "wheel_idle")
val idlePulse by idleTransition.animateFloat(
initialValue = 0.98f,
targetValue = 1.02f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1400),
repeatMode = RepeatMode.Reverse
),
label = "wheel_idle_pulse"
)
val centerScale by animateFloatAsState(
targetValue = if (spunAndReady) 1.08f else 1f,
animationSpec = tween(durationMillis = 220),
label = "wheel_center_scale"
)
val pointerScale by animateFloatAsState(
targetValue = if (isSpinning) 1.12f else 1f,
animationSpec = tween(durationMillis = 160),
label = "wheel_pointer_scale"
)
Box(
modifier = Modifier.size(270.dp),
modifier = Modifier
.size(270.dp)
.scale(if (isSpinning) 1f else idlePulse),
contentAlignment = Alignment.Center
) {
Canvas(
@ -247,6 +276,7 @@ private fun WheelSpinner(
modifier = Modifier
.align(Alignment.TopCenter)
.size(width = 34.dp, height = 28.dp)
.scale(pointerScale)
) {
val path = Path().apply {
moveTo(size.width / 2f, size.height)
@ -258,9 +288,11 @@ private fun WheelSpinner(
}
Surface(
modifier = Modifier.size(94.dp),
modifier = Modifier
.size(94.dp)
.scale(centerScale),
shape = CircleShape,
color = MaterialTheme.colorScheme.surface,
color = if (spunAndReady) CloserPalette.PinkMist else MaterialTheme.colorScheme.surface,
shadowElevation = 10.dp
) {
Box(contentAlignment = Alignment.Center) {

File diff suppressed because it is too large Load Diff