feat: add answer history filter pills with reveal/private badges, DesireSync progress meter and reveal UI, HowWell ring progress and privacy tiles, Paywall feature scavenger hunt, category screen grid polish
This commit is contained in:
parent
15c1fbdda0
commit
606d724f12
|
|
@ -2,6 +2,7 @@ package app.closer.ui.answers
|
|||
|
||||
import app.closer.ui.theme.closerBackgroundBrush
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -19,11 +20,15 @@ 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.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -48,8 +53,13 @@ import app.closer.core.navigation.AppRoute
|
|||
import app.closer.domain.model.LocalAnswer
|
||||
import app.closer.ui.components.CategoryGlyph
|
||||
import app.closer.ui.components.EmptyState
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
import app.closer.ui.questions.displayCategoryName
|
||||
|
||||
private enum class AnswerHistoryFilter {
|
||||
ALL, PRIVATE, REVEALED
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnswerHistoryScreen(
|
||||
onNavigate: (String) -> Unit = {},
|
||||
|
|
@ -73,6 +83,16 @@ private fun AnswerHistoryContent(
|
|||
onDelete: (String) -> Unit
|
||||
) {
|
||||
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
|
||||
var selectedFilter by remember { mutableStateOf(AnswerHistoryFilter.ALL) }
|
||||
val visibleAnswers = remember(state.answers, selectedFilter) {
|
||||
state.answers.filter { answer ->
|
||||
when (selectedFilter) {
|
||||
AnswerHistoryFilter.ALL -> true
|
||||
AnswerHistoryFilter.PRIVATE -> !answer.isRevealed
|
||||
AnswerHistoryFilter.REVEALED -> answer.isRevealed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingDelete?.let { answer ->
|
||||
AlertDialog(
|
||||
|
|
@ -146,7 +166,25 @@ private fun AnswerHistoryContent(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
items(state.answers, key = { it.questionId }) { answer ->
|
||||
item {
|
||||
HistoryFilterRow(
|
||||
selected = selectedFilter,
|
||||
privateCount = state.answers.count { !it.isRevealed },
|
||||
revealedCount = state.answers.count { it.isRevealed },
|
||||
onSelected = { selectedFilter = it }
|
||||
)
|
||||
}
|
||||
|
||||
if (visibleAnswers.isEmpty()) {
|
||||
item {
|
||||
EmptyState(
|
||||
title = "Nothing in this view",
|
||||
body = "Switch filters to see the rest of your saved answers."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(visibleAnswers, key = { it.questionId }) { answer ->
|
||||
AnswerHistoryCard(
|
||||
answer = answer,
|
||||
onClick = { onAnswerSelected(answer) },
|
||||
|
|
@ -158,6 +196,69 @@ private fun AnswerHistoryContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistoryFilterRow(
|
||||
selected: AnswerHistoryFilter,
|
||||
privateCount: Int,
|
||||
revealedCount: Int,
|
||||
onSelected: (AnswerHistoryFilter) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
HistoryFilterPill(
|
||||
label = "All",
|
||||
count = privateCount + revealedCount,
|
||||
selected = selected == AnswerHistoryFilter.ALL,
|
||||
onClick = { onSelected(AnswerHistoryFilter.ALL) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
HistoryFilterPill(
|
||||
label = "Private",
|
||||
count = privateCount,
|
||||
selected = selected == AnswerHistoryFilter.PRIVATE,
|
||||
onClick = { onSelected(AnswerHistoryFilter.PRIVATE) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
HistoryFilterPill(
|
||||
label = "Revealed",
|
||||
count = revealedCount,
|
||||
selected = selected == AnswerHistoryFilter.REVEALED,
|
||||
onClick = { onSelected(AnswerHistoryFilter.REVEALED) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistoryFilterPill(
|
||||
label: String,
|
||||
count: Int,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.heightIn(min = 44.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = if (selected) CloserPalette.PurpleMist else Color.White.copy(alpha = 0.74f),
|
||||
shadowElevation = if (selected) 2.dp else 0.dp
|
||||
) {
|
||||
Text(
|
||||
text = "$label $count",
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (selected) CloserPalette.PurpleDeep else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -192,7 +293,7 @@ private fun AnswerHistoryCard(
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
HistoryPill(if (answer.isRevealed) "Revealed" else "Private")
|
||||
HistoryStateBadge(isRevealed = answer.isRevealed)
|
||||
HistoryPill(answer.category.displayCategoryName())
|
||||
}
|
||||
Text(
|
||||
|
|
@ -235,6 +336,35 @@ private fun AnswerHistoryCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistoryStateBadge(isRevealed: Boolean) {
|
||||
val tint = if (isRevealed) CloserPalette.PurpleDeep else CloserPalette.Evergreen
|
||||
val container = if (isRevealed) CloserPalette.PurpleMist else CloserPalette.Evergreen.copy(alpha = 0.10f)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = container
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isRevealed) Icons.Filled.Visibility else Icons.Filled.Lock,
|
||||
contentDescription = null,
|
||||
tint = tint,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = if (isRevealed) "Revealed" else "Private",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = tint,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistoryPill(label: String) {
|
||||
Surface(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -21,6 +22,7 @@ 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.ui.draw.clip
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
|
|
@ -31,7 +33,6 @@ 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
|
||||
|
|
@ -499,11 +500,9 @@ private fun DSAnswerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { index.toFloat() / total },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = CloserPalette.Romantic,
|
||||
trackColor = CloserPalette.Romantic.copy(alpha = 0.15f)
|
||||
DesireProgressPill(
|
||||
progress = index.toFloat() / total,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Card(
|
||||
|
|
@ -605,6 +604,7 @@ private fun DSRevealScreen(
|
|||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
DesireRevealMeter(matches = matches.size, total = total)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -660,6 +660,116 @@ private fun DSRevealScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesireProgressPill(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CloserPalette.Romantic.copy(alpha = 0.15f))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CloserPalette.Romantic)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesireRevealMeter(
|
||||
matches: Int,
|
||||
total: Int
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.86f)),
|
||||
elevation = CardDefaults.cardElevation(0.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
DesirePrivacyTile(
|
||||
label = "Partner A",
|
||||
value = "$total private",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
StatusGlyph(
|
||||
icon = Icons.Filled.Sync,
|
||||
tint = CloserPalette.Romantic,
|
||||
container = CloserPalette.Romantic.copy(alpha = 0.12f),
|
||||
size = 38.dp,
|
||||
iconSize = 20.dp
|
||||
)
|
||||
DesirePrivacyTile(
|
||||
label = "Partner B",
|
||||
value = "$total private",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
DesireProgressPill(
|
||||
progress = matches.toFloat() / total.coerceAtLeast(1),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = "$matches shared, ${total - matches} kept private",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = CloserPalette.Romantic,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesirePrivacyTile(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = CloserPalette.Romantic.copy(alpha = 0.08f)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = CloserPalette.Romantic,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesireMatchCard(match: DesireMatch) {
|
||||
Card(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package app.closer.ui.howwell
|
|||
|
||||
import app.closer.ui.theme.closerCardColor
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Canvas
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -20,6 +22,7 @@ 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.ui.draw.clip
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Psychology
|
||||
|
|
@ -30,7 +33,6 @@ 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
|
||||
|
|
@ -43,6 +45,8 @@ 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.graphics.StrokeCap
|
||||
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
|
||||
|
|
@ -502,11 +506,9 @@ private fun AnswerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { index.toFloat() / total },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = CloserPalette.PurpleDeep,
|
||||
trackColor = CloserPalette.PurpleMist
|
||||
HowWellProgressPill(
|
||||
progress = index.toFloat() / total,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Card(
|
||||
|
|
@ -656,6 +658,12 @@ private fun RevealScreen(
|
|||
}
|
||||
}
|
||||
|
||||
HowWellScoreStrip(
|
||||
score = score,
|
||||
answered = questionNumber,
|
||||
total = total
|
||||
)
|
||||
|
||||
Text(
|
||||
text = result.question.text,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
|
|
@ -755,18 +763,10 @@ private fun CompleteScreen(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
StatusGlyph(
|
||||
icon = Icons.Filled.Timeline,
|
||||
tint = CloserPalette.PurpleDeep,
|
||||
container = CloserPalette.PurpleMist,
|
||||
size = 82.dp,
|
||||
iconSize = 40.dp
|
||||
)
|
||||
Text(
|
||||
text = "$score / $total",
|
||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
|
||||
color = CloserPalette.PurpleDeep,
|
||||
textAlign = TextAlign.Center
|
||||
HowWellScoreRing(
|
||||
score = score,
|
||||
total = total,
|
||||
modifier = Modifier.size(118.dp)
|
||||
)
|
||||
Text(
|
||||
text = scoreLabel(score, total),
|
||||
|
|
@ -808,6 +808,110 @@ private fun CompleteScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowWellProgressPill(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(8.dp)
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CloserPalette.PurpleMist)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(progress.coerceIn(0f, 1f))
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CloserPalette.PurpleDeep)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowWellScoreStrip(
|
||||
score: Int,
|
||||
answered: Int,
|
||||
total: Int
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = CloserPalette.PurpleMist
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HowWellProgressPill(
|
||||
progress = score.toFloat() / answered.coerceAtLeast(1),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "$score / $answered read",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = CloserPalette.PurpleDeep,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "$total total",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowWellScoreRing(
|
||||
score: Int,
|
||||
total: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val progress = if (total == 0) 0f else score.toFloat() / total
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val strokeWidth = 11.dp.toPx()
|
||||
drawArc(
|
||||
color = CloserPalette.PurpleMist,
|
||||
startAngle = -90f,
|
||||
sweepAngle = 360f,
|
||||
useCenter = false,
|
||||
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
drawArc(
|
||||
color = CloserPalette.PurpleDeep,
|
||||
startAngle = -90f,
|
||||
sweepAngle = 360f * progress.coerceIn(0f, 1f),
|
||||
useCenter = false,
|
||||
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "$score",
|
||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
|
||||
color = CloserPalette.PurpleDeep
|
||||
)
|
||||
Text(
|
||||
text = "of $total",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BreakdownRow(result: HowWellResult) {
|
||||
val matchColor = Color(0xFF2E7D32)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ 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.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -25,6 +27,7 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
|
|
@ -60,6 +63,8 @@ import app.closer.core.navigation.ExternalLinks
|
|||
import app.closer.domain.repository.BillingState
|
||||
import app.closer.ui.components.ErrorState
|
||||
import app.closer.ui.components.LoadingState
|
||||
import app.closer.ui.components.StatusGlyph
|
||||
import app.closer.ui.theme.CloserPalette
|
||||
import com.revenuecat.purchases.Package
|
||||
|
||||
private val BENEFITS = listOf(
|
||||
|
|
@ -103,7 +108,6 @@ fun PaywallScreen(
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
HeaderSection(onClose = { onNavigate("back") })
|
||||
BenefitsCard()
|
||||
|
||||
when {
|
||||
uiState.isLoading -> LoadingState(
|
||||
|
|
@ -124,10 +128,12 @@ fun PaywallScreen(
|
|||
)
|
||||
}
|
||||
|
||||
BenefitsCard()
|
||||
|
||||
if (uiState.purchaseState is BillingState.Loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(28.dp),
|
||||
color = Color(0xFFB98AF4)
|
||||
color = CloserPalette.PurpleRich
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -163,9 +169,16 @@ private fun HeaderSection(
|
|||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
StatusGlyph(
|
||||
icon = Icons.Filled.Star,
|
||||
tint = CloserPalette.PurpleDeep,
|
||||
container = CloserPalette.PurpleGlow,
|
||||
size = 58.dp,
|
||||
iconSize = 28.dp
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Go deeper together",
|
||||
|
|
@ -180,7 +193,7 @@ private fun HeaderSection(
|
|||
)
|
||||
}
|
||||
|
||||
TextButton(onClick = onClose) {
|
||||
TextButton(onClick = onClose, modifier = Modifier.size(48.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
|
|
@ -190,45 +203,61 @@ private fun HeaderSection(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BenefitsCard(modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
modifier = Modifier.padding(18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "What's included",
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
BENEFITS.forEach { benefit ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF56306F),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
text = benefit,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
BENEFITS.forEach { benefit ->
|
||||
BenefitPill(benefit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BenefitPill(label: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CloserPalette.PurpleMist)
|
||||
.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = CloserPalette.PurpleDeep,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlanOptions(
|
||||
packages: List<Package>,
|
||||
|
|
@ -239,8 +268,8 @@ private fun PlanOptions(
|
|||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF4E8FF)),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
colors = CardDefaults.cardColors(containerColor = CloserPalette.PurpleSoft),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(22.dp),
|
||||
|
|
@ -251,6 +280,11 @@ private fun PlanOptions(
|
|||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "Plans are selected here. Restore and legal links stay below the purchase action.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
if (packages.isEmpty()) {
|
||||
Text(
|
||||
|
|
@ -284,7 +318,7 @@ private fun PlanRow(
|
|||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(
|
||||
if (isSelected) Color(0xFFB98AF4).copy(alpha = 0.20f)
|
||||
if (isSelected) CloserPalette.PurpleRich.copy(alpha = 0.20f)
|
||||
else Color.White.copy(alpha = 0.64f)
|
||||
)
|
||||
.selectable(
|
||||
|
|
@ -344,20 +378,41 @@ private fun ActionButtons(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFB98AF4),
|
||||
containerColor = CloserPalette.PurpleDeep,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledContainerColor = Color(0xFFB98AF4).copy(alpha = 0.40f),
|
||||
disabledContainerColor = CloserPalette.PurpleRich.copy(alpha = 0.40f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.54f)
|
||||
)
|
||||
) {
|
||||
Text("Continue", fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
TextButton(onClick = onRestore) {
|
||||
Text(
|
||||
text = "Restore purchases",
|
||||
color = Color(0xFF9B8AA6)
|
||||
)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.54f)),
|
||||
elevation = CardDefaults.cardElevation(0.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Already subscribed?",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
TextButton(onClick = onRestore) {
|
||||
Text(
|
||||
text = "Restore",
|
||||
color = CloserPalette.PurpleDeep,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -288,23 +288,50 @@ private fun FilterPill(
|
|||
|
||||
@Composable
|
||||
private fun DepthHeader(depth: Int, count: Int) {
|
||||
Row(
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.68f),
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Text(
|
||||
text = "Depth $depth",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
CategoryPill("$count ${if (count == 1) "prompt" else "prompts"}")
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 11.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = depthLabel(depth),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "Depth $depth",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
CategoryPill("$count ${if (count == 1) "prompt" else "prompts"}", emphasis = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun depthLabel(depth: Int): String = when (depth) {
|
||||
1 -> "Light openers"
|
||||
2 -> "Closer prompts"
|
||||
3 -> "Deeper conversation"
|
||||
else -> "Depth $depth"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuestionListCard(
|
||||
question: Question,
|
||||
|
|
|
|||
Loading…
Reference in New Issue