feat: local question content, question header, daily question illustration, iOS question/wheel views
This commit is contained in:
parent
299b2eb8ca
commit
324b051834
|
|
@ -6,6 +6,7 @@ import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
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
|
||||||
|
|
@ -27,8 +28,6 @@ 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.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|
@ -37,20 +36,17 @@ import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.closer.R
|
||||||
import app.closer.domain.model.ChoiceAnswerConfigImpl
|
import app.closer.domain.model.ChoiceAnswerConfigImpl
|
||||||
import app.closer.domain.model.Question
|
import app.closer.domain.model.Question
|
||||||
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
|
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
|
||||||
|
|
@ -121,12 +117,11 @@ fun LocalQuestionContent(
|
||||||
val reducedMotion = Settings.Global.getFloat(
|
val reducedMotion = Settings.Global.getFloat(
|
||||||
context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f
|
context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f
|
||||||
) == 0f
|
) == 0f
|
||||||
var helpExpanded by remember(question.id) { mutableStateOf(false) }
|
|
||||||
QuestionMetaRow(question = question)
|
|
||||||
QuestionHeader(
|
QuestionHeader(
|
||||||
question = question,
|
question = question,
|
||||||
helpExpanded = helpExpanded,
|
helpExpanded = false,
|
||||||
onToggleHelp = { helpExpanded = !helpExpanded },
|
onToggleHelp = {},
|
||||||
|
showHelp = false
|
||||||
)
|
)
|
||||||
QuestionAnswerInput(
|
QuestionAnswerInput(
|
||||||
question = question,
|
question = question,
|
||||||
|
|
@ -205,50 +200,47 @@ private fun LocalQuestionHeader(
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String
|
subtitle: String
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = subtitle,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun QuestionMetaRow(question: Question) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
|
||||||
) {
|
|
||||||
MetaPill(label = question.category.displayCategoryName())
|
|
||||||
MetaPill(label = "Depth ${question.depthLevel}")
|
|
||||||
MetaPill(label = question.type.displayQuestionType())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MetaPill(label: String) {
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
color = Color.White.copy(alpha = 0.72f),
|
shape = RoundedCornerShape(28.dp),
|
||||||
shadowElevation = 0.dp
|
color = Color.White.copy(alpha = 0.82f),
|
||||||
|
shadowElevation = 5.dp
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
text = label,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
style = MaterialTheme.typography.labelMedium,
|
) {
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
Image(
|
||||||
maxLines = 1,
|
painter = painterResource(R.drawable.illustration_daily_question),
|
||||||
overflow = TextOverflow.Ellipsis
|
contentDescription = null,
|
||||||
)
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(166.dp)
|
||||||
|
.clip(RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp))
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 20.dp, end = 20.dp, bottom = 20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
|
|
@ -22,21 +23,22 @@ fun QuestionHeader(
|
||||||
question: Question,
|
question: Question,
|
||||||
helpExpanded: Boolean,
|
helpExpanded: Boolean,
|
||||||
onToggleHelp: () -> Unit,
|
onToggleHelp: () -> Unit,
|
||||||
|
showHelp: Boolean = true,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(26.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
containerColor = Color(0xFFFFF8FC).copy(alpha = 0.94f)
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp, vertical = 28.dp)
|
.padding(horizontal = 24.dp, vertical = 30.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = question.text,
|
text = question.text,
|
||||||
|
|
@ -45,19 +47,21 @@ fun QuestionHeader(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 34.sp
|
lineHeight = 34.sp
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 6,
|
maxLines = 6,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QuestionHelpExpandable(
|
if (showHelp) {
|
||||||
question = question,
|
QuestionHelpExpandable(
|
||||||
expanded = helpExpanded,
|
question = question,
|
||||||
onToggle = onToggleHelp,
|
expanded = helpExpanded,
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
onToggle = onToggleHelp,
|
||||||
)
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 462 KiB |
|
|
@ -11,14 +11,16 @@ struct DailyQuestionView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: CloserSpacing.xxl) {
|
VStack(spacing: CloserSpacing.lg) {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
LoadingView(message: "Loading today's question...")
|
LoadingView(message: "Loading today's question...")
|
||||||
.padding(.top, CloserSpacing.xxxl)
|
.padding(.top, CloserSpacing.xxxl)
|
||||||
} else if let question = question {
|
} else if let question = question {
|
||||||
// Question card
|
TodayQuestionHeroView()
|
||||||
|
.closerPadding()
|
||||||
|
|
||||||
VStack(spacing: CloserSpacing.lg) {
|
VStack(spacing: CloserSpacing.lg) {
|
||||||
Text("Today's Question")
|
Text("Today's question")
|
||||||
.font(CloserFont.subheadline)
|
.font(CloserFont.subheadline)
|
||||||
.foregroundColor(.closerTextSecondary)
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
|
||||||
|
|
@ -143,6 +145,35 @@ struct DailyQuestionView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct TodayQuestionHeroView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.md) {
|
||||||
|
Image("illustration-daily-question")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 174)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: CloserSpacing.xs) {
|
||||||
|
Text("One question, enough space")
|
||||||
|
.font(CloserFont.title2)
|
||||||
|
.foregroundColor(.closerText)
|
||||||
|
|
||||||
|
Text("Answer privately first, then reveal when you are both ready.")
|
||||||
|
.font(CloserFont.callout)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(CloserSpacing.md)
|
||||||
|
.background(Color.closerSurface)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: CloserRadius.xlarge, style: .continuous))
|
||||||
|
.closerShadow(level: .small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Question Answer
|
// MARK: - Question Answer
|
||||||
|
|
||||||
struct QuestionAnswerView: View {
|
struct QuestionAnswerView: View {
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 462 KiB |
|
|
@ -58,6 +58,35 @@ struct CategoryPickerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Wheel Copy
|
||||||
|
|
||||||
|
private enum WheelCopy {
|
||||||
|
static let titles = [
|
||||||
|
"Let the Prompt Find You",
|
||||||
|
"Spin for Tonight's Prompt",
|
||||||
|
"Let Fate Pick",
|
||||||
|
"The Wheel Has Opinions",
|
||||||
|
"Find Our Prompt",
|
||||||
|
"Tiny Moment, Big Maybe",
|
||||||
|
]
|
||||||
|
static let subtitles = [
|
||||||
|
"Spin for tonight's prompt.",
|
||||||
|
"Let fate pick the vibe.",
|
||||||
|
"Not sure? Spin.",
|
||||||
|
"Tiny moment. Big maybe.",
|
||||||
|
"No pressure. Just spin.",
|
||||||
|
"Let the wheel choose.",
|
||||||
|
]
|
||||||
|
static let buttonLabels = ["Spin", "Let's Spin"]
|
||||||
|
static let results = [
|
||||||
|
"The wheel has spoken.",
|
||||||
|
"This one found you.",
|
||||||
|
"Fate picked this one.",
|
||||||
|
"Prompt unlocked.",
|
||||||
|
"This is the one.",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Spin Wheel
|
// MARK: - Spin Wheel
|
||||||
|
|
||||||
struct SpinWheelView: View {
|
struct SpinWheelView: View {
|
||||||
|
|
@ -67,6 +96,10 @@ struct SpinWheelView: View {
|
||||||
@State private var selectedSlice: Int?
|
@State private var selectedSlice: Int?
|
||||||
@State private var showResult = false
|
@State private var showResult = false
|
||||||
@State private var navigateToSession = false
|
@State private var navigateToSession = false
|
||||||
|
@State private var titleIndex: Int = 0
|
||||||
|
@State private var subtitleIndex: Int = 0
|
||||||
|
@State private var buttonIndex: Int = 0
|
||||||
|
@State private var resultIndex: Int = 0
|
||||||
|
|
||||||
let slices: [(label: String, color: Color)] = [
|
let slices: [(label: String, color: Color)] = [
|
||||||
("Question", .closerPrimary),
|
("Question", .closerPrimary),
|
||||||
|
|
@ -81,9 +114,23 @@ struct SpinWheelView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: CloserSpacing.xl) {
|
VStack(spacing: CloserSpacing.xl) {
|
||||||
Text("Category: \(category)")
|
VStack(spacing: CloserSpacing.xs) {
|
||||||
.font(CloserFont.title2)
|
Text(WheelCopy.titles[titleIndex])
|
||||||
.foregroundColor(.closerText)
|
.font(CloserFont.title2)
|
||||||
|
.foregroundColor(.closerText)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Text(WheelCopy.subtitles[subtitleIndex])
|
||||||
|
.font(CloserFont.subheadline)
|
||||||
|
.foregroundColor(.closerTextSecondary)
|
||||||
|
Text(category)
|
||||||
|
.font(CloserFont.caption)
|
||||||
|
.foregroundColor(.closerPrimary)
|
||||||
|
.padding(.horizontal, CloserSpacing.sm)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.closerPrimary.opacity(0.1))
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.padding(.top, CloserSpacing.sm)
|
||||||
|
|
||||||
// Wheel
|
// Wheel
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
@ -114,8 +161,8 @@ struct SpinWheelView: View {
|
||||||
|
|
||||||
// Result
|
// Result
|
||||||
if let slice = selectedSlice, showResult {
|
if let slice = selectedSlice, showResult {
|
||||||
VStack(spacing: CloserSpacing.sm) {
|
VStack(spacing: CloserSpacing.xs) {
|
||||||
Text("You got:")
|
Text(WheelCopy.results[resultIndex])
|
||||||
.font(CloserFont.subheadline)
|
.font(CloserFont.subheadline)
|
||||||
.foregroundColor(.closerTextSecondary)
|
.foregroundColor(.closerTextSecondary)
|
||||||
Text(slices[slice].label)
|
Text(slices[slice].label)
|
||||||
|
|
@ -132,7 +179,7 @@ struct SpinWheelView: View {
|
||||||
Button(action: spin) {
|
Button(action: spin) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "arrow.triangle.2.circlepath")
|
Image(systemName: "arrow.triangle.2.circlepath")
|
||||||
Text(isSpinning ? "Spinning..." : "Spin!")
|
Text(isSpinning ? "Spinning…" : WheelCopy.buttonLabels[buttonIndex])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(PrimaryButtonStyle(isDisabled: isSpinning))
|
.buttonStyle(PrimaryButtonStyle(isDisabled: isSpinning))
|
||||||
|
|
@ -152,6 +199,11 @@ struct SpinWheelView: View {
|
||||||
.navigationDestination(isPresented: $navigateToSession) {
|
.navigationDestination(isPresented: $navigateToSession) {
|
||||||
WheelSessionView(sessionId: UUID().uuidString, category: category, slice: slices[selectedSlice ?? 0].label)
|
WheelSessionView(sessionId: UUID().uuidString, category: category, slice: slices[selectedSlice ?? 0].label)
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
titleIndex = Int.random(in: 0..<WheelCopy.titles.count)
|
||||||
|
subtitleIndex = Int.random(in: 0..<WheelCopy.subtitles.count)
|
||||||
|
buttonIndex = Int.random(in: 0..<WheelCopy.buttonLabels.count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func spin() {
|
private func spin() {
|
||||||
|
|
@ -174,6 +226,7 @@ struct SpinWheelView: View {
|
||||||
let index = Int(pointerAngle / sliceAngle)
|
let index = Int(pointerAngle / sliceAngle)
|
||||||
|
|
||||||
selectedSlice = min(max(index, 0), slices.count - 1)
|
selectedSlice = min(max(index, 0), slices.count - 1)
|
||||||
|
resultIndex = Int.random(in: 0..<WheelCopy.results.count)
|
||||||
showResult = true
|
showResult = true
|
||||||
isSpinning = false
|
isSpinning = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue