feat(wheel): ViewModel-backed wheel screens with local session store
This commit is contained in:
parent
577d39ea11
commit
011745e7d4
|
|
@ -1,36 +1,218 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
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.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.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.couplesconnect.app.core.navigation.AppRoute
|
||||
import com.couplesconnect.app.ui.components.PlaceholderAction
|
||||
import com.couplesconnect.app.ui.components.PlaceholderScreen
|
||||
import com.couplesconnect.app.domain.model.QuestionCategory
|
||||
import com.couplesconnect.app.ui.questions.displayCategoryName
|
||||
|
||||
@Composable
|
||||
fun CategoryPickerScreen(
|
||||
onNavigate: (String) -> Unit = {}
|
||||
onNavigate: (String) -> Unit = {},
|
||||
viewModel: CategoryPickerViewModel = hiltViewModel()
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Choose the weather",
|
||||
section = "Wheel",
|
||||
description = "A category picker for matching the conversation to the couple's energy in the moment.",
|
||||
route = AppRoute.CATEGORY_PICKER,
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFF6C8EA4),
|
||||
primaryAction = PlaceholderAction("Spin trust", AppRoute.spinWheel("trust")),
|
||||
secondaryAction = PlaceholderAction("Question packs", AppRoute.QUESTION_PACKS),
|
||||
chips = listOf("Categories", "Mood-aware", "Wheel entry"),
|
||||
details = listOf(
|
||||
"Seeded question categories can surface here",
|
||||
"The selected category stays with the flow",
|
||||
"The spin flow stays separate from daily questions"
|
||||
)
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
CategoryPickerContent(
|
||||
state = state,
|
||||
onCategorySelected = { item ->
|
||||
if (item.isLocked) onNavigate(AppRoute.PAYWALL)
|
||||
else onNavigate(AppRoute.spinWheel(item.category.id))
|
||||
},
|
||||
onRetry = viewModel::load
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryPickerContent(
|
||||
state: CategoryPickerUiState,
|
||||
onCategorySelected: (CategoryPickerItem) -> Unit,
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFFFFBFA), Color(0xFFF0EEF8), Color(0xFFEAF0F4)),
|
||||
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 = 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(0xFF27211F)
|
||||
)
|
||||
Text(
|
||||
text = "Pick a category that matches where you are tonight. The wheel picks the question.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF4E4642)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
state.isLoading -> item {
|
||||
Row(
|
||||
modifier = Modifier.padding(22.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CircularProgressIndicator(color = Color(0xFF7C6F9E))
|
||||
Text("Loading categories…", style = MaterialTheme.typography.bodyMedium, color = Color(0xFF4E4642))
|
||||
}
|
||||
}
|
||||
state.error != null -> item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.84f))
|
||||
) {
|
||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text("Categories unavailable", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = Color(0xFF27211F))
|
||||
Text(state.error, style = MaterialTheme.typography.bodyMedium, color = Color(0xFF4E4642))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> items(state.categories, key = { it.category.id }) { item ->
|
||||
CategoryCard(item = item, onClick = { onCategorySelected(item) })
|
||||
}
|
||||
}
|
||||
|
||||
item { Box(Modifier.padding(bottom = 8.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryCard(
|
||||
item: CategoryPickerItem,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val containerColor = if (item.isLocked) Color(0xFFF5F0EC).copy(alpha = 0.84f) else Color.White.copy(alpha = 0.86f)
|
||||
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(18.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.category.displayName.ifBlank { item.category.id.displayCategoryName() },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (item.isLocked) Color(0xFF9E9693) else Color(0xFF27211F),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
CategoryPill("${item.questionCount} prompts")
|
||||
if (item.isLocked) CategoryPill("Premium")
|
||||
}
|
||||
}
|
||||
if (item.isLocked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = "Locked",
|
||||
tint = Color(0xFFB0A9A6),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryPill(label: String) {
|
||||
Surface(shape = RoundedCornerShape(999.dp), color = Color(0xFFF0EDF9)) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color(0xFF4A3F7A)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CategoryPickerScreenPreview() {
|
||||
CategoryPickerScreen()
|
||||
CategoryPickerContent(
|
||||
state = CategoryPickerUiState(
|
||||
isLoading = false,
|
||||
categories = listOf(
|
||||
CategoryPickerItem(
|
||||
category = QuestionCategory("communication", "Communication", "Prompts for connection", "free", "chat"),
|
||||
questionCount = 250,
|
||||
isLocked = false
|
||||
),
|
||||
CategoryPickerItem(
|
||||
category = QuestionCategory("intimacy", "Intimacy", "Prompts for closeness", "premium", "heart"),
|
||||
questionCount = 180,
|
||||
isLocked = true
|
||||
)
|
||||
)
|
||||
),
|
||||
onCategorySelected = {},
|
||||
onRetry = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.couplesconnect.app.core.billing.EntitlementChecker
|
||||
import com.couplesconnect.app.domain.model.QuestionCategory
|
||||
import com.couplesconnect.app.domain.repository.QuestionRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class CategoryPickerItem(
|
||||
val category: QuestionCategory,
|
||||
val questionCount: Int,
|
||||
val isLocked: Boolean
|
||||
)
|
||||
|
||||
data class CategoryPickerUiState(
|
||||
val isLoading: Boolean = true,
|
||||
val error: String? = null,
|
||||
val categories: List<CategoryPickerItem> = emptyList()
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class CategoryPickerViewModel @Inject constructor(
|
||||
private val repository: QuestionRepository,
|
||||
private val entitlementChecker: EntitlementChecker
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(CategoryPickerUiState())
|
||||
val uiState: StateFlow<CategoryPickerUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
load()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = CategoryPickerUiState(isLoading = true)
|
||||
try {
|
||||
val hasPremium = entitlementChecker.hasPremium
|
||||
val items = repository.getCategories().map { category ->
|
||||
CategoryPickerItem(
|
||||
category = category,
|
||||
questionCount = repository.getQuestionCountByCategory(category.id),
|
||||
isLocked = category.access == "premium" && !hasPremium
|
||||
)
|
||||
}
|
||||
_uiState.value = CategoryPickerUiState(isLoading = false, categories = items)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = CategoryPickerUiState(
|
||||
isLoading = false,
|
||||
error = e.message ?: "Could not load categories."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
import com.couplesconnect.app.domain.model.Question
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
data class LocalWheelSession(
|
||||
val categoryId: String,
|
||||
val categoryName: String,
|
||||
val questions: List<Question>
|
||||
)
|
||||
|
||||
@Singleton
|
||||
class LocalWheelSessionStore @Inject constructor() {
|
||||
var activeSession: LocalWheelSession? = null
|
||||
var lastAnswered: Int = 0
|
||||
var lastTotal: Int = 0
|
||||
}
|
||||
|
|
@ -1,37 +1,220 @@
|
|||
package com.couplesconnect.app.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.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.rotate
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.couplesconnect.app.core.navigation.AppRoute
|
||||
import com.couplesconnect.app.ui.components.PlaceholderAction
|
||||
import com.couplesconnect.app.ui.components.PlaceholderScreen
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
@Composable
|
||||
fun SpinWheelScreen(
|
||||
categoryId: String,
|
||||
onNavigate: (String) -> Unit = {}
|
||||
onNavigate: (String) -> Unit = {},
|
||||
viewModel: SpinWheelViewModel = hiltViewModel()
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Let the prompt find you",
|
||||
section = "Wheel",
|
||||
description = "A playful selection surface for turning a chosen category into a short question session.",
|
||||
route = AppRoute.spinWheel(categoryId),
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFFF2A65A),
|
||||
primaryAction = PlaceholderAction("Start session", AppRoute.wheelSession("session-preview")),
|
||||
secondaryAction = PlaceholderAction("Categories", AppRoute.CATEGORY_PICKER),
|
||||
chips = listOf("Category $categoryId", "Motion", "Session"),
|
||||
details = listOf(
|
||||
"Wheel animation has room to become tactile",
|
||||
"The chosen category stays visible",
|
||||
"Session start feels like one continuous step"
|
||||
)
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(state.navigateTo) {
|
||||
state.navigateTo?.let {
|
||||
onNavigate(it)
|
||||
viewModel.onNavigated()
|
||||
}
|
||||
}
|
||||
|
||||
SpinWheelContent(
|
||||
state = state,
|
||||
onSpin = viewModel::spin,
|
||||
onStart = viewModel::startSession
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpinWheelContent(
|
||||
state: SpinWheelUiState,
|
||||
onSpin: () -> Unit,
|
||||
onStart: () -> Unit
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "wheel")
|
||||
val rotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1200, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "wheel_rotation"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFFFFBFA), Color(0xFFF0EEF8), Color(0xFFEAF0F4)),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 28.dp, vertical = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(28.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Let the prompt find you",
|
||||
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color(0xFF27211F),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
if (state.categoryName.isNotBlank()) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = Color(0xFFF0EDF9)
|
||||
) {
|
||||
Text(
|
||||
text = state.categoryName,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Color(0xFF4A3F7A)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
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(0xFF81B29A) else Color(0xFF7C6F9E)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
when {
|
||||
state.error != null -> Text(
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB5473A),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
state.spunAndReady -> {
|
||||
Text(
|
||||
text = "${SpinWheelViewModel.SESSION_SIZE} questions selected",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF4E4642),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = onStart,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C6F9E))
|
||||
) {
|
||||
Text("Start session", color = Color.White)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onSpin,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) {
|
||||
Text("Spin again")
|
||||
}
|
||||
}
|
||||
state.isLoading -> CircularProgressIndicator(color = Color(0xFF7C6F9E))
|
||||
else -> {
|
||||
Text(
|
||||
text = "Tap to select ${SpinWheelViewModel.SESSION_SIZE} questions at random",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF4E4642),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = onSpin,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C6F9E))
|
||||
) {
|
||||
Text("Spin", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SpinWheelScreenPreview() {
|
||||
SpinWheelScreen(categoryId = "trust")
|
||||
SpinWheelContent(
|
||||
state = SpinWheelUiState(isLoading = false, categoryName = "Trust"),
|
||||
onSpin = {},
|
||||
onStart = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.couplesconnect.app.core.navigation.AppRoute
|
||||
import com.couplesconnect.app.domain.repository.QuestionRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SpinWheelUiState(
|
||||
val isLoading: Boolean = true,
|
||||
val categoryName: String = "",
|
||||
val isSpinning: Boolean = false,
|
||||
val spunAndReady: Boolean = false,
|
||||
val error: String? = null,
|
||||
val navigateTo: String? = null
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class SpinWheelViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val repository: QuestionRepository,
|
||||
private val sessionStore: LocalWheelSessionStore
|
||||
) : ViewModel() {
|
||||
|
||||
private val categoryId: String = savedStateHandle["categoryId"] ?: ""
|
||||
|
||||
private val _uiState = MutableStateFlow(SpinWheelUiState())
|
||||
val uiState: StateFlow<SpinWheelUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadCategory()
|
||||
}
|
||||
|
||||
private fun loadCategory() {
|
||||
viewModelScope.launch {
|
||||
val category = runCatching { repository.getCategoryById(categoryId) }.getOrNull()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
categoryName = category?.displayName ?: categoryId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun spin() {
|
||||
if (_uiState.value.isSpinning) return
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isSpinning = true, error = null) }
|
||||
val questions = runCatching {
|
||||
repository.getQuestionsByCategory(categoryId).shuffled().take(SESSION_SIZE)
|
||||
}.getOrElse { emptyList() }
|
||||
|
||||
if (questions.isEmpty()) {
|
||||
_uiState.update {
|
||||
it.copy(isSpinning = false, error = "No questions found for this category.")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val category = runCatching { repository.getCategoryById(categoryId) }.getOrNull()
|
||||
sessionStore.activeSession = LocalWheelSession(
|
||||
categoryId = categoryId,
|
||||
categoryName = category?.displayName ?: categoryId,
|
||||
questions = questions
|
||||
)
|
||||
_uiState.update { it.copy(isSpinning = false, spunAndReady = true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun startSession() {
|
||||
_uiState.update { it.copy(navigateTo = AppRoute.wheelSession("local")) }
|
||||
}
|
||||
|
||||
fun onNavigated() {
|
||||
_uiState.update { it.copy(navigateTo = null) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SESSION_SIZE = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,176 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.couplesconnect.app.core.navigation.AppRoute
|
||||
import com.couplesconnect.app.ui.components.PlaceholderAction
|
||||
import com.couplesconnect.app.ui.components.PlaceholderScreen
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
@HiltViewModel
|
||||
class WheelCompleteViewModel @Inject constructor(
|
||||
private val sessionStore: LocalWheelSessionStore
|
||||
) : ViewModel() {
|
||||
val categoryName: String = sessionStore.activeSession?.categoryName ?: ""
|
||||
val answered: Int = sessionStore.lastAnswered
|
||||
val total: Int = sessionStore.lastTotal
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WheelCompleteScreen(
|
||||
sessionId: String,
|
||||
onNavigate: (String) -> Unit = {}
|
||||
onNavigate: (String) -> Unit = {},
|
||||
viewModel: WheelCompleteViewModel = hiltViewModel()
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Close the loop",
|
||||
section = "Wheel",
|
||||
description = "A completion surface for celebrating the ritual and offering the next gentle step.",
|
||||
route = AppRoute.wheelComplete(sessionId),
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFF81B29A),
|
||||
primaryAction = PlaceholderAction("Answer history", AppRoute.ANSWER_HISTORY),
|
||||
secondaryAction = PlaceholderAction("Home", AppRoute.HOME),
|
||||
chips = listOf("Session $sessionId", "Completion", "Reflect"),
|
||||
details = listOf(
|
||||
"The session id survives through the full wheel flow",
|
||||
"Reflection and history paths are ready",
|
||||
"Celebration can stay simple and sincere"
|
||||
)
|
||||
WheelCompleteContent(
|
||||
categoryName = viewModel.categoryName,
|
||||
answered = viewModel.answered,
|
||||
total = viewModel.total,
|
||||
onHome = { onNavigate(AppRoute.HOME) },
|
||||
onSpinAgain = { onNavigate(AppRoute.CATEGORY_PICKER) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WheelCompleteContent(
|
||||
categoryName: String,
|
||||
answered: Int,
|
||||
total: Int,
|
||||
onHome: () -> Unit,
|
||||
onSpinAgain: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFFFFBFA), Color(0xFFF0F5F2), Color(0xFFEAF0F4)),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 28.dp, vertical = 40.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "✓",
|
||||
fontSize = 64.sp,
|
||||
color = Color(0xFF81B29A)
|
||||
)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Session complete",
|
||||
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color(0xFF27211F),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
if (categoryName.isNotBlank()) {
|
||||
Text(
|
||||
text = categoryName,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF4E4642),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (total > 0) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.88f)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$answered",
|
||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color(0xFF81B29A)
|
||||
)
|
||||
Text(
|
||||
text = "of $total questions",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color(0xFF4E4642)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onHome,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF81B29A))
|
||||
) {
|
||||
Text("Back home", color = Color.White)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onSpinAgain,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
) {
|
||||
Text("Spin again")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun WheelCompleteScreenPreview() {
|
||||
WheelCompleteScreen(sessionId = "session-preview")
|
||||
WheelCompleteContent(
|
||||
categoryName = "Trust",
|
||||
answered = 8,
|
||||
total = 10,
|
||||
onHome = {},
|
||||
onSpinAgain = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,233 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
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.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.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.couplesconnect.app.core.navigation.AppRoute
|
||||
import com.couplesconnect.app.ui.components.PlaceholderAction
|
||||
import com.couplesconnect.app.ui.components.PlaceholderScreen
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.couplesconnect.app.domain.model.Question
|
||||
|
||||
@Composable
|
||||
fun WheelSessionScreen(
|
||||
sessionId: String,
|
||||
onNavigate: (String) -> Unit = {}
|
||||
onNavigate: (String) -> Unit = {},
|
||||
viewModel: WheelSessionViewModel = hiltViewModel()
|
||||
) {
|
||||
PlaceholderScreen(
|
||||
title = "Stay with the question",
|
||||
section = "Wheel",
|
||||
description = "A lightweight session space for a chosen prompt, timer, partner state, and completion moment.",
|
||||
route = AppRoute.wheelSession(sessionId),
|
||||
onNavigate = onNavigate,
|
||||
accent = Color(0xFFE07A5F),
|
||||
primaryAction = PlaceholderAction("Complete", AppRoute.wheelComplete(sessionId)),
|
||||
secondaryAction = PlaceholderAction("Home", AppRoute.HOME),
|
||||
chips = listOf("Session $sessionId", "Prompt flow", "Finish path"),
|
||||
details = listOf(
|
||||
"Session state can stay calm and readable",
|
||||
"Completion keeps continuity with the same moment",
|
||||
"The flow can return home at any point"
|
||||
)
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(state.navigateTo) {
|
||||
state.navigateTo?.let {
|
||||
onNavigate(it)
|
||||
viewModel.onNavigated()
|
||||
}
|
||||
}
|
||||
|
||||
WheelSessionContent(
|
||||
state = state,
|
||||
onNext = viewModel::next,
|
||||
onSkip = viewModel::skip,
|
||||
onEnd = viewModel::endEarly
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WheelSessionContent(
|
||||
state: WheelSessionUiState,
|
||||
onNext: () -> Unit,
|
||||
onSkip: () -> Unit,
|
||||
onEnd: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFFFFBFA), Color(0xFFF3F0F8), Color(0xFFEAF0F4)),
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.safeDrawingPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 24.dp, vertical = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
if (state.isEmpty) {
|
||||
EmptySessionCard()
|
||||
return@Column
|
||||
}
|
||||
|
||||
val total = state.questions.size
|
||||
val current = state.currentIndex
|
||||
val progress = if (total > 0) (current.toFloat() / total) else 0f
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (state.categoryName.isNotBlank()) {
|
||||
Surface(shape = RoundedCornerShape(999.dp), color = Color(0xFFF0EDF9)) {
|
||||
Text(
|
||||
text = state.categoryName,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color(0xFF4A3F7A)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = "${current + 1} / $total",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Color(0xFF9E9693)
|
||||
)
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color(0xFF7C6F9E),
|
||||
trackColor = Color(0xFFE8E4F0)
|
||||
)
|
||||
|
||||
val question = state.questions.getOrNull(current)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.88f)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(28.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = question?.text ?: "",
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color(0xFF27211F),
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = MaterialTheme.typography.titleLarge.lineHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onNext,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C6F9E))
|
||||
) {
|
||||
Text(
|
||||
text = if (current + 1 >= total) "Finish" else "Next question",
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onSkip,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
Text("Skip")
|
||||
}
|
||||
TextButton(
|
||||
onClick = onEnd,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("End session", color = Color(0xFF9E9693))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptySessionCard() {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.84f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
"No active session",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF27211F)
|
||||
)
|
||||
Text(
|
||||
"Go back to the category picker and spin the wheel to start.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF4E4642)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun WheelSessionScreenPreview() {
|
||||
WheelSessionScreen(sessionId = "session-preview")
|
||||
WheelSessionContent(
|
||||
state = WheelSessionUiState(
|
||||
questions = listOf(
|
||||
Question(id = "1", text = "When did you last feel truly seen by me?", category = "trust", depthLevel = 3),
|
||||
Question(id = "2", text = "What's one thing you wish we talked about more?", category = "communication", depthLevel = 2)
|
||||
),
|
||||
currentIndex = 0,
|
||||
categoryName = "Trust"
|
||||
),
|
||||
onNext = {},
|
||||
onSkip = {},
|
||||
onEnd = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package com.couplesconnect.app.ui.wheel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.couplesconnect.app.core.navigation.AppRoute
|
||||
import com.couplesconnect.app.domain.model.Question
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class WheelSessionUiState(
|
||||
val questions: List<Question> = emptyList(),
|
||||
val currentIndex: Int = 0,
|
||||
val skippedCount: Int = 0,
|
||||
val categoryName: String = "",
|
||||
val navigateTo: String? = null,
|
||||
val isEmpty: Boolean = false
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class WheelSessionViewModel @Inject constructor(
|
||||
private val sessionStore: LocalWheelSessionStore
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(WheelSessionUiState())
|
||||
val uiState: StateFlow<WheelSessionUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
val session = sessionStore.activeSession
|
||||
if (session == null) {
|
||||
_uiState.update { it.copy(isEmpty = true) }
|
||||
} else {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
questions = session.questions,
|
||||
categoryName = session.categoryName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun next() {
|
||||
val state = _uiState.value
|
||||
val nextIndex = state.currentIndex + 1
|
||||
if (nextIndex >= state.questions.size) {
|
||||
finishSession()
|
||||
} else {
|
||||
_uiState.update { it.copy(currentIndex = nextIndex) }
|
||||
}
|
||||
}
|
||||
|
||||
fun skip() {
|
||||
val state = _uiState.value
|
||||
val nextIndex = state.currentIndex + 1
|
||||
if (nextIndex >= state.questions.size) {
|
||||
finishSession()
|
||||
} else {
|
||||
_uiState.update { it.copy(currentIndex = nextIndex, skippedCount = state.skippedCount + 1) }
|
||||
}
|
||||
}
|
||||
|
||||
fun endEarly() {
|
||||
finishSession()
|
||||
}
|
||||
|
||||
private fun finishSession() {
|
||||
val state = _uiState.value
|
||||
sessionStore.lastAnswered = (state.currentIndex + 1).coerceAtMost(state.questions.size)
|
||||
sessionStore.lastTotal = state.questions.size
|
||||
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("local")) }
|
||||
}
|
||||
|
||||
fun onNavigated() {
|
||||
_uiState.update { it.copy(navigateTo = null) }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue