From 433d04d23c501f87031da190f983b8d3baa88507 Mon Sep 17 00:00:00 2001 From: null Date: Mon, 22 Jun 2026 19:18:49 -0500 Subject: [PATCH] feat: answer reveal, auth screens, challenges, onboarding, pairing, paywall, wheel, settings, components --- .../closer/ui/answers/AnswerRevealScreen.kt | 6 +- .../closer/ui/auth/ForgotPasswordScreen.kt | 8 +- .../java/app/closer/ui/auth/LoginScreen.kt | 4 +- .../java/app/closer/ui/auth/SignUpScreen.kt | 4 +- .../challenges/ConnectionChallengesScreen.kt | 4 +- .../app/closer/ui/components/LoadingState.kt | 146 +++++++++------ .../closer/ui/desiresync/DesireSyncScreen.kt | 14 +- .../ui/games/WaitingForPartnerScreen.kt | 7 +- .../app/closer/ui/howwell/HowWellScreen.kt | 14 +- .../closer/ui/memorylane/MemoryLaneScreen.kt | 6 +- .../ui/onboarding/CreateProfileScreen.kt | 8 +- .../closer/ui/onboarding/OnboardingScreen.kt | 8 +- .../closer/ui/outcomes/YourProgressScreen.kt | 4 +- .../closer/ui/pairing/AcceptInviteScreen.kt | 8 +- .../closer/ui/pairing/CreateInviteScreen.kt | 4 +- .../ui/pairing/EncryptionUpgradeScreen.kt | 4 +- .../closer/ui/pairing/InviteConfirmScreen.kt | 10 +- .../app/closer/ui/pairing/RecoveryScreen.kt | 8 +- .../app/closer/ui/paywall/PaywallScreen.kt | 7 +- .../ui/questions/QuestionCategoryScreen.kt | 4 +- .../ui/questions/QuestionPackLibraryScreen.kt | 4 +- .../closer/ui/settings/DeleteAccountScreen.kt | 6 +- .../closer/ui/settings/EditProfileScreen.kt | 6 +- .../ui/settings/RelationshipSettingsScreen.kt | 4 +- .../app/closer/ui/settings/SettingsScreen.kt | 10 +- .../closer/ui/thisorthat/ThisOrThatScreen.kt | 19 +- .../closer/ui/wheel/CategoryPickerScreen.kt | 4 +- .../app/closer/ui/wheel/SpinWheelScreen.kt | 4 +- .../closer/ui/wheel/WheelCompleteScreen.kt | 9 +- .../app/closer/ui/wheel/WheelSessionScreen.kt | 4 +- iphone/Closer/Components/CommonViews.swift | 166 +++++++++++++++++- 31 files changed, 339 insertions(+), 175 deletions(-) diff --git a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt index c67220a6..57acf140 100644 --- a/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt +++ b/app/src/main/java/app/closer/ui/answers/AnswerRevealScreen.kt @@ -25,7 +25,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SnackbarHost @@ -141,7 +141,7 @@ private fun AnswerRevealContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - CircularProgressIndicator(color = Color(0xFFB98AF4)) + CloserHeartLoader(size = 32.dp) Text("Loading reveal") } } @@ -451,7 +451,7 @@ private fun ReleasingKeyState() { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - CircularProgressIndicator(color = Color(0xFFB98AF4)) + CloserHeartLoader(size = 32.dp) Text( text = "Opening reveal…", style = MaterialTheme.typography.bodyLarge, diff --git a/app/src/main/java/app/closer/ui/auth/ForgotPasswordScreen.kt b/app/src/main/java/app/closer/ui/auth/ForgotPasswordScreen.kt index 2826edf6..d2975038 100644 --- a/app/src/main/java/app/closer/ui/auth/ForgotPasswordScreen.kt +++ b/app/src/main/java/app/closer/ui/auth/ForgotPasswordScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -157,11 +157,7 @@ fun ForgotPasswordScreen( contentColor = AuthOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = AuthOnPrimary, - strokeWidth = 2.dp - ) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Send reset email", style = MaterialTheme.typography.labelLarge) } } diff --git a/app/src/main/java/app/closer/ui/auth/LoginScreen.kt b/app/src/main/java/app/closer/ui/auth/LoginScreen.kt index 97dc1017..01631300 100644 --- a/app/src/main/java/app/closer/ui/auth/LoginScreen.kt +++ b/app/src/main/java/app/closer/ui/auth/LoginScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -58,6 +57,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import app.closer.core.navigation.AppRoute import app.closer.ui.components.BrandMessageRotator +import app.closer.ui.components.CloserHeartLoader @Composable fun LoginScreen( @@ -176,7 +176,7 @@ fun LoginScreen( contentColor = AuthOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Sign in", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/auth/SignUpScreen.kt b/app/src/main/java/app/closer/ui/auth/SignUpScreen.kt index 5abc0031..4118250f 100644 --- a/app/src/main/java/app/closer/ui/auth/SignUpScreen.kt +++ b/app/src/main/java/app/closer/ui/auth/SignUpScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -179,7 +179,7 @@ fun SignUpScreen( contentColor = AuthOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Create account", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt b/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt index 919d9102..5df9724e 100644 --- a/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt +++ b/app/src/main/java/app/closer/ui/challenges/ConnectionChallengesScreen.kt @@ -29,7 +29,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -241,7 +241,7 @@ fun ConnectionChallengesScreen( @Composable private fun ChallengesLoadingScreen() { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator(color = CloserPalette.PurpleDeep) + CloserHeartLoader() } } diff --git a/app/src/main/java/app/closer/ui/components/LoadingState.kt b/app/src/main/java/app/closer/ui/components/LoadingState.kt index 39335e00..ba04bea3 100644 --- a/app/src/main/java/app/closer/ui/components/LoadingState.kt +++ b/app/src/main/java/app/closer/ui/components/LoadingState.kt @@ -1,29 +1,35 @@ package app.closer.ui.components -import app.closer.ui.theme.CloserPalette import app.closer.ui.theme.closerCardColor -import androidx.compose.animation.core.LinearEasing +import android.provider.Settings +import androidx.compose.animation.core.FastOutSlowInEasing 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.Canvas import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -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.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.PathParser +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable @@ -31,8 +37,6 @@ fun LoadingState( message: String = "Loading…", modifier: Modifier = Modifier ) { - val shimmer = closerSkeletonBrush() - CloserCard( modifier = modifier.fillMaxWidth(), containerColor = closerCardColor(alpha = 0.8f) @@ -41,23 +45,10 @@ fun LoadingState( modifier = Modifier .fillMaxWidth() .padding(CloserSpacing.Xxxl), + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(CloserSpacing.Md) ) { - SkeletonLine( - brush = shimmer, - modifier = Modifier.fillMaxWidth(0.42f), - height = 18.dp - ) - SkeletonLine( - brush = shimmer, - modifier = Modifier.fillMaxWidth(), - height = 13.dp - ) - SkeletonLine( - brush = shimmer, - modifier = Modifier.fillMaxWidth(0.72f), - height = 13.dp - ) + CloserHeartLoader() Text( text = message, style = MaterialTheme.typography.bodyMedium, @@ -73,39 +64,90 @@ fun LoadingState( } @Composable -private fun closerSkeletonBrush(): Brush { - val transition = rememberInfiniteTransition(label = "closerSkeleton") - val shimmerOffset = transition.animateFloat( - initialValue = -320f, - targetValue = 640f, +fun CloserHeartLoader( + modifier: Modifier = Modifier, + size: Dp = 76.dp +) { + val context = LocalContext.current + val reducedMotion = remember { + Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f + ) == 0f + } + val transition = rememberInfiniteTransition(label = "closerHeartLoader") + val animatedFill = transition.animateFloat( + initialValue = 0.08f, + targetValue = 1f, animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1200, easing = LinearEasing), + animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Restart ), - label = "closerSkeletonOffset" + label = "closerHeartFill" ) - - return Brush.linearGradient( - colors = listOf( - CloserPalette.PurpleMist.copy(alpha = 0.55f), - CloserPalette.PurpleSoft.copy(alpha = 0.95f), - CloserPalette.PinkMist.copy(alpha = 0.72f) + val animatedPulse = transition.animateFloat( + initialValue = 0.96f, + targetValue = 1.04f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 900, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse ), - start = Offset(shimmerOffset.value, 0f), - end = Offset(shimmerOffset.value + 280f, 0f) + label = "closerHeartPulse" ) -} -@Composable -private fun SkeletonLine( - brush: Brush, - modifier: Modifier = Modifier, - height: androidx.compose.ui.unit.Dp -) { - Box( + val fillProgress = if (reducedMotion) 1f else animatedFill.value + val pulse = if (reducedMotion) 1f else animatedPulse.value + val shadowPath = remember { + PathParser().parsePathString( + "M54,89C48,82 25,65 20,50C15,35 23,22 37,22C45,22 51,26 54,33C57,26 63,22 71,22C85,22 93,35 88,50C83,65 60,82 54,89Z" + ).toPath() + } + val leftPath = remember { + PathParser().parsePathString( + "M54,85C49,79 27,62 22,48C17,35 24,24 37,24C45,24 51,28 54,35Z" + ).toPath() + } + val rightPath = remember { + PathParser().parsePathString( + "M54,85C59,79 81,62 86,48C91,35 84,24 71,24C63,24 57,28 54,35Z" + ).toPath() + } + val leftHighlight = remember { + PathParser().parsePathString( + "M27,42C28,32 34,27 42,27C48,27 52,30 54,35L54,41C47,36 37,36 27,42Z" + ).toPath() + } + val rightHighlight = remember { + PathParser().parsePathString( + "M54,35C57,30 62,27 69,27C78,27 84,32 85,42C75,36 65,36 54,41Z" + ).toPath() + } + + Canvas( modifier = modifier - .height(height) - .clip(RoundedCornerShape(CloserRadii.Pill)) - .background(brush) - ) + .size(size) + .graphicsLayer { + scaleX = pulse + scaleY = pulse + } + .clearAndSetSemantics {} + ) { + val scaleX = this.size.width / 108f + val scaleY = this.size.height / 108f + + withTransform({ + scale(scaleX = scaleX, scaleY = scaleY, pivot = Offset.Zero) + }) { + drawPath(shadowPath, color = Color(0xFF24122F).copy(alpha = 0.10f)) + drawPath(leftPath, color = Color(0xFFF7C8E4).copy(alpha = 0.22f)) + drawPath(rightPath, color = Color(0xFFD9B8FF).copy(alpha = 0.22f)) + clipRect(top = 108f * (1f - fillProgress), bottom = 108f) { + drawPath(leftPath, color = Color(0xFFF7C8E4)) + drawPath(rightPath, color = Color(0xFFD9B8FF)) + drawPath(leftHighlight, color = Color(0xFFFFF4FA).copy(alpha = 0.68f)) + drawPath(rightHighlight, color = Color(0xFFF3E8FF).copy(alpha = 0.52f)) + } + } + } } diff --git a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt index a9a1cbce..2b166645 100644 --- a/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt +++ b/app/src/main/java/app/closer/ui/desiresync/DesireSyncScreen.kt @@ -33,7 +33,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface @@ -370,9 +370,8 @@ fun DesireSyncScreen( .background(closerBackgroundBrush()) ) { when (state.phase) { - DesireSyncPhase.LOADING -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.Romantic + DesireSyncPhase.LOADING -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) DesireSyncPhase.ERROR -> DSErrorScreen( message = state.error ?: "Something didn't load. Go back and try again.", @@ -553,7 +552,7 @@ private fun DSWaitingScreen(partnerName: String, onBack: () -> Unit, onAbandon: textAlign = TextAlign.Center ) Spacer(Modifier.height(4.dp)) - CircularProgressIndicator(color = CloserPalette.Romantic, strokeWidth = 3.dp) + CloserHeartLoader(size = 48.dp) BrandMessageRotator(style = MaterialTheme.typography.bodySmall) Spacer(Modifier.weight(1f)) OutlinedButton( @@ -1020,9 +1019,8 @@ fun DSReplayScreen( .background(closerBackgroundBrush()) ) { when (phase) { - is DSReplayPhase.Loading -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.Romantic + is DSReplayPhase.Loading -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) is DSReplayPhase.Error -> Column( modifier = Modifier diff --git a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt index 646a8eee..94fd69b1 100644 --- a/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt +++ b/app/src/main/java/app/closer/ui/games/WaitingForPartnerScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -130,10 +130,7 @@ fun WaitingForPartnerScreen( ) { when { state.isLoading -> { - CircularProgressIndicator( - modifier = Modifier.size(48.dp), - color = MaterialTheme.colorScheme.primary - ) + CloserHeartLoader(size = 64.dp) Text( text = "Loading...", style = MaterialTheme.typography.bodyLarge, diff --git a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt index 2c65d258..9916fc32 100644 --- a/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt +++ b/app/src/main/java/app/closer/ui/howwell/HowWellScreen.kt @@ -34,7 +34,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface @@ -414,9 +414,8 @@ fun HowWellScreen( .background(closerBackgroundBrush()) ) { when (state.phase) { - HowWellPhase.LOADING -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.PurpleDeep + HowWellPhase.LOADING -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) HowWellPhase.ERROR -> HowWellErrorScreen( message = state.error ?: "Something didn't load. Go back and try again.", @@ -622,7 +621,7 @@ private fun HowWellWaitingScreen(amSubject: Boolean, partnerName: String, onBack textAlign = TextAlign.Center ) Spacer(Modifier.height(4.dp)) - CircularProgressIndicator(color = CloserPalette.PurpleDeep, strokeWidth = 3.dp) + CloserHeartLoader(size = 48.dp) BrandMessageRotator(style = MaterialTheme.typography.bodySmall) Spacer(Modifier.weight(1f)) OutlinedButton( @@ -1202,9 +1201,8 @@ fun HowWellReplayScreen( .background(closerBackgroundBrush()) ) { when (phase) { - is HWReplayPhase.Loading -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.PurpleDeep + is HWReplayPhase.Loading -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) is HWReplayPhase.Error -> Column( modifier = Modifier diff --git a/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt b/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt index 965a2a46..bd14b58b 100644 --- a/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt +++ b/app/src/main/java/app/closer/ui/memorylane/MemoryLaneScreen.kt @@ -33,7 +33,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -231,7 +231,7 @@ fun MemoryLaneScreen( ) { when (state.phase) { MemoryLanePhase.LOADING -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator(color = CloserPalette.PurpleDeep) + CloserHeartLoader() } MemoryLanePhase.LIST -> CapsuleListScreen( capsules = state.capsules, @@ -494,7 +494,7 @@ private fun CapsuleCreateScreen( shape = RoundedCornerShape(18.dp), colors = ButtonDefaults.buttonColors(containerColor = CloserPalette.PurpleDeep) ) { - if (state.isSaving) CircularProgressIndicator(color = Color.White, modifier = Modifier.size(20.dp), strokeWidth = 2.dp) + if (state.isSaving) CloserHeartLoader(size = 22.dp) else Text("Seal the capsule 📦", color = Color.White, style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt b/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt index 91a1ffce..1b5c54c7 100644 --- a/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt +++ b/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt @@ -34,7 +34,7 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -255,7 +255,7 @@ private fun NameStep( contentColor = AuthOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Continue", style = MaterialTheme.typography.labelLarge) } } @@ -312,7 +312,7 @@ private fun SexStep( contentColor = AuthOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Continue", style = MaterialTheme.typography.labelLarge) } } @@ -440,7 +440,7 @@ private fun PhotoStep( contentColor = AuthOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator(color = AuthOnPrimary, strokeWidth = 2.dp) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Continue", style = MaterialTheme.typography.labelLarge) } } diff --git a/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt index 01660e33..790d97fd 100644 --- a/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/app/closer/ui/onboarding/OnboardingScreen.kt @@ -24,7 +24,6 @@ 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.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -55,6 +54,7 @@ import app.closer.ui.auth.AuthOnPrimary import app.closer.ui.auth.AuthPrimary import app.closer.ui.auth.AuthPrimaryDeep import app.closer.ui.components.BrandMessageRotator +import app.closer.ui.components.CloserHeartLoader import app.closer.ui.theme.CloserPalette import kotlinx.coroutines.launch @@ -85,11 +85,7 @@ fun OnboardingScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(20.dp) ) { - CircularProgressIndicator( - modifier = Modifier.size(36.dp), - color = AuthPrimary, - strokeWidth = 3.dp - ) + CloserHeartLoader() BrandMessageRotator(color = AuthMuted) } } else { diff --git a/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt b/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt index c464c540..d72d84af 100644 --- a/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt +++ b/app/src/main/java/app/closer/ui/outcomes/YourProgressScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.TrendingUp import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -98,7 +98,7 @@ fun YourProgressScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - CircularProgressIndicator() + CloserHeartLoader() } } state.error != null -> { diff --git a/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt index c0f11af9..4a1bf72f 100644 --- a/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt @@ -26,7 +26,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -166,11 +166,7 @@ fun AcceptInviteScreen( contentColor = SettingsOnPrimary ) ) { - if (state.isLoading) CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = SettingsOnPrimary, - strokeWidth = 2.dp - ) + if (state.isLoading) CloserHeartLoader(size = 22.dp) else Text("Continue", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt index b23a2b1d..5b17a9ce 100644 --- a/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt @@ -29,7 +29,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -125,7 +125,7 @@ fun CreateInviteScreen( ) { if (state.isLoading) { Spacer(Modifier.height(160.dp)) - CircularProgressIndicator(modifier = Modifier.size(40.dp)) + CloserHeartLoader(size = 64.dp) } else if (state.inviteCode != null) { Spacer(Modifier.height(24.dp)) diff --git a/app/src/main/java/app/closer/ui/pairing/EncryptionUpgradeScreen.kt b/app/src/main/java/app/closer/ui/pairing/EncryptionUpgradeScreen.kt index 2c3a035a..652be346 100644 --- a/app/src/main/java/app/closer/ui/pairing/EncryptionUpgradeScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/EncryptionUpgradeScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock 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.Text @@ -38,6 +37,7 @@ import app.closer.data.remote.FirestoreCoupleDataSource import app.closer.domain.repository.AuthRepository import app.closer.domain.repository.CoupleRepository import app.closer.ui.components.BrandMessageRotator +import app.closer.ui.components.CloserHeartLoader import app.closer.ui.components.StatusGlyph import app.closer.ui.settings.SettingsBackgroundBrush import app.closer.ui.settings.SettingsInk @@ -181,7 +181,7 @@ fun EncryptionUpgradeScreen( textAlign = TextAlign.Center ) Spacer(Modifier.height(24.dp)) - CircularProgressIndicator(color = SettingsPrimary) + CloserHeartLoader() Spacer(Modifier.height(16.dp)) BrandMessageRotator(style = MaterialTheme.typography.bodySmall) } diff --git a/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt b/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt index bf2090a5..85dce986 100644 --- a/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -107,7 +107,7 @@ fun InviteConfirmScreen( ) { if (state.isLoading) { Spacer(Modifier.height(160.dp)) - CircularProgressIndicator(modifier = Modifier.size(40.dp)) + CloserHeartLoader(size = 64.dp) } else { Spacer(Modifier.height(24.dp)) @@ -156,11 +156,7 @@ fun InviteConfirmScreen( contentColor = SettingsOnPrimary ) ) { - if (state.isConfirming) CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = SettingsOnPrimary, - strokeWidth = 2.dp - ) + if (state.isConfirming) CloserHeartLoader(size = 22.dp) else Text("Pair up", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/pairing/RecoveryScreen.kt b/app/src/main/java/app/closer/ui/pairing/RecoveryScreen.kt index f689522e..1abad798 100644 --- a/app/src/main/java/app/closer/ui/pairing/RecoveryScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/RecoveryScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults @@ -141,11 +141,7 @@ fun RecoveryScreen( ) ) { if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = SettingsOnPrimary, - strokeWidth = 2.dp - ) + CloserHeartLoader(size = 22.dp) } else { Text("Unlock answers", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt b/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt index 734aee5f..9c65168b 100644 --- a/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt +++ b/app/src/main/java/app/closer/ui/paywall/PaywallScreen.kt @@ -32,7 +32,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -133,10 +133,7 @@ fun PaywallScreen( BenefitsCard() if (uiState.purchaseState is BillingState.Loading) { - CircularProgressIndicator( - modifier = Modifier.size(28.dp), - color = CloserPalette.PurpleRich - ) + CloserHeartLoader(size = 40.dp) } ActionButtons( diff --git a/app/src/main/java/app/closer/ui/questions/QuestionCategoryScreen.kt b/app/src/main/java/app/closer/ui/questions/QuestionCategoryScreen.kt index 3c3c2fba..bd75ecc5 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionCategoryScreen.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionCategoryScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -401,7 +401,7 @@ private fun CategoryLoadingCard() { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - CircularProgressIndicator(color = Color(0xFFB98AF4)) + CloserHeartLoader(size = 32.dp) Text( text = "Loading prompts", style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt b/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt index 09eddbd8..53beffb3 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt @@ -26,7 +26,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -424,7 +424,7 @@ private fun LoadingPackCard() { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - CircularProgressIndicator(color = Color(0xFFB98AF4)) + CloserHeartLoader(size = 32.dp) Text( text = "Loading question packs", style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt b/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt index 21eab2fc..b1b32094 100644 --- a/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/DeleteAccountScreen.kt @@ -33,7 +33,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -251,7 +251,7 @@ fun DeleteAccountScreen( ) ) { if (state.isReauthing) { - CircularProgressIndicator(modifier = Modifier.size(16.dp), color = Color.White, strokeWidth = 2.dp) + CloserHeartLoader(size = 18.dp) } else { Text("Confirm", color = Color.White) } @@ -328,7 +328,7 @@ fun DeleteAccountScreen( ) { if (state.isDeleting) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp) + CloserHeartLoader(size = 20.dp) Text("Deleting…", color = Color.White) } } else { diff --git a/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt b/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt index 986221d5..dc5f6363 100644 --- a/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt @@ -34,7 +34,7 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -175,7 +175,7 @@ fun EditProfileContent( ) { if (state.isLoading) { Spacer(Modifier.height(64.dp)) - CircularProgressIndicator(color = SettingsPrimary) + CloserHeartLoader() } else { val imageUri = state.photoUri ?: state.photoUrl.takeIf { it.isNotBlank() } Box( @@ -356,7 +356,7 @@ fun EditProfileContent( contentColor = SettingsOnPrimary ) ) { - if (state.isSaving) CircularProgressIndicator(color = SettingsOnPrimary, strokeWidth = 2.dp) + if (state.isSaving) CloserHeartLoader(size = 22.dp) else Text("Save changes", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt index 16053a58..c634617e 100644 --- a/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/RelationshipSettingsScreen.kt @@ -32,7 +32,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -190,7 +190,7 @@ fun RelationshipSettingsScreen( ) { if (state.isLeaving) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp) + CloserHeartLoader(size = 20.dp) Text("Leaving…", color = Color.White) } } else { diff --git a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt index 33bc428e..154cd29b 100644 --- a/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/SettingsScreen.kt @@ -35,7 +35,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -226,7 +226,7 @@ fun SettingsScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - CircularProgressIndicator() + CloserHeartLoader() } } else { var showBaselineOutcomeDialog by remember { mutableStateOf(false) } @@ -499,11 +499,7 @@ fun SettingsScreen( contentColor = SettingsDanger ) ) { - if (state.isSigningOut) CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = SettingsDanger, - strokeWidth = 2.dp - ) + if (state.isSigningOut) CloserHeartLoader(size = 22.dp) else Text("Sign out", style = MaterialTheme.typography.labelLarge) } diff --git a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt index 6392f20e..2d1f6f22 100644 --- a/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt +++ b/app/src/main/java/app/closer/ui/thisorthat/ThisOrThatScreen.kt @@ -37,7 +37,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface @@ -417,9 +417,8 @@ fun ThisOrThatScreen( .background(closerBackgroundBrush()) ) { when (state.phase) { - TotPhase.LOADING -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.PurpleDeep + TotPhase.LOADING -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) TotPhase.ERROR -> ErrorState( message = state.error ?: "Something didn't load. Go back and try again.", @@ -447,9 +446,8 @@ fun ThisOrThatScreen( TotPhase.PLAYING -> { val question = state.questions.getOrNull(state.currentIndex) if (question == null) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.PurpleDeep + CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) } else { val config = question.answerConfig as? ThisOrThatAnswerConfigImpl @@ -909,7 +907,7 @@ private fun WaitingForRevealScreen( textAlign = TextAlign.Center ) Spacer(Modifier.height(4.dp)) - CircularProgressIndicator(color = CloserPalette.PurpleDeep, strokeWidth = 3.dp) + CloserHeartLoader(size = 48.dp) BrandMessageRotator(style = MaterialTheme.typography.bodySmall) Spacer(Modifier.weight(1f)) @@ -1272,9 +1270,8 @@ fun ThisOrThatReplayScreen( .background(closerBackgroundBrush()) ) { when (phase) { - is TotReplayPhase.Loading -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.PurpleDeep + is TotReplayPhase.Loading -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) is TotReplayPhase.Error -> Column( modifier = Modifier diff --git a/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt b/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt index 7a388601..0333d0c2 100644 --- a/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/CategoryPickerScreen.kt @@ -22,7 +22,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -104,7 +104,7 @@ private fun CategoryPickerContent( horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically ) { - CircularProgressIndicator(color = CloserPalette.PurpleDeep) + CloserHeartLoader(size = 28.dp) Text( "Loading categories…", style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt b/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt index 326c160b..b3063a81 100644 --- a/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/SpinWheelScreen.kt @@ -32,7 +32,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -262,7 +262,7 @@ private fun SpinWheelContent( } } } - state.isLoading -> CircularProgressIndicator(color = CloserPalette.PurpleDeep) + state.isLoading -> CloserHeartLoader() else -> { Text( text = "Spin to discover a random category and 10 questions", diff --git a/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt index b853c839..9ef83e38 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelCompleteScreen.kt @@ -26,7 +26,7 @@ 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 app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface @@ -197,9 +197,8 @@ fun WheelCompleteScreen( .background(closerBackgroundBrush()) ) { when (state.phase) { - WheelRevealPhase.LOADING -> CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = CloserPalette.PurpleDeep + WheelRevealPhase.LOADING -> CloserHeartLoader( + modifier = Modifier.align(Alignment.Center) ) WheelRevealPhase.WAITING -> WheelWaitingContent( partnerName = state.partnerName, @@ -254,7 +253,7 @@ private fun WheelWaitingContent( textAlign = TextAlign.Center ) Spacer(Modifier.height(4.dp)) - CircularProgressIndicator(color = CloserPalette.PurpleDeep, strokeWidth = 3.dp) + CloserHeartLoader(size = 48.dp) Spacer(Modifier.weight(1f)) diff --git a/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt b/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt index 56222843..6edd1320 100644 --- a/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt +++ b/app/src/main/java/app/closer/ui/wheel/WheelSessionScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.CircularProgressIndicator +import app.closer.ui.components.CloserHeartLoader import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -114,7 +114,7 @@ private fun WheelSessionContent( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(color = Color(0xFF56306F)) + CloserHeartLoader() } return@Column } diff --git a/iphone/Closer/Components/CommonViews.swift b/iphone/Closer/Components/CommonViews.swift index 5a784ad3..4977bae3 100644 --- a/iphone/Closer/Components/CommonViews.swift +++ b/iphone/Closer/Components/CommonViews.swift @@ -7,9 +7,7 @@ struct LoadingView: View { var body: some View { VStack(spacing: CloserSpacing.lg) { - ProgressView() - .tint(.closerPrimary) - .scaleEffect(1.2) + CloserHeartLoader() Text(message) .font(CloserFont.callout) .foregroundColor(.closerTextSecondary) @@ -18,6 +16,168 @@ struct LoadingView: View { } } +struct CloserHeartLoader: View { + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var fillProgress: CGFloat = 0.08 + @State private var isPulsing = false + + var size: CGFloat = 76 + + var body: some View { + ZStack { + CloserHeartShape() + .fill( + LinearGradient( + colors: [ + Color(hex: "F7C8E4").opacity(0.22), + Color(hex: "D9B8FF").opacity(0.22) + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + + HStack(spacing: 0) { + Color(hex: "F7C8E4") + Color(hex: "D9B8FF") + } + .mask(CloserHeartShape()) + .mask(alignment: .bottom) { + Rectangle() + .frame(height: size * (reduceMotion ? 1 : fillProgress)) + } + + CloserHeartHighlightShape(side: .left) + .fill(Color(hex: "FFF4FA").opacity(reduceMotion ? 0.68 : 0.68 * fillProgress)) + + CloserHeartHighlightShape(side: .right) + .fill(Color(hex: "F3E8FF").opacity(reduceMotion ? 0.52 : 0.52 * fillProgress)) + } + .frame(width: size, height: size) + .scaleEffect(reduceMotion ? 1 : (isPulsing ? 1.04 : 0.96)) + .accessibilityHidden(true) + .onAppear { + guard !reduceMotion else { + fillProgress = 1 + isPulsing = false + return + } + + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: false)) { + fillProgress = 1 + } + withAnimation(.easeInOut(duration: 0.9).repeatForever(autoreverses: true)) { + isPulsing = true + } + } + .onChange(of: reduceMotion) { _, newValue in + if newValue { + fillProgress = 1 + isPulsing = false + } + } + } +} + +private struct CloserHeartShape: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: point(54, 85, in: rect)) + path.addCurve( + to: point(22, 48, in: rect), + control1: point(49, 79, in: rect), + control2: point(27, 62, in: rect) + ) + path.addCurve( + to: point(37, 24, in: rect), + control1: point(17, 35, in: rect), + control2: point(24, 24, in: rect) + ) + path.addCurve( + to: point(54, 35, in: rect), + control1: point(45, 24, in: rect), + control2: point(51, 28, in: rect) + ) + path.addCurve( + to: point(71, 24, in: rect), + control1: point(57, 28, in: rect), + control2: point(63, 24, in: rect) + ) + path.addCurve( + to: point(86, 48, in: rect), + control1: point(84, 24, in: rect), + control2: point(91, 35, in: rect) + ) + path.addCurve( + to: point(54, 85, in: rect), + control1: point(81, 62, in: rect), + control2: point(59, 79, in: rect) + ) + path.closeSubpath() + return path + } +} + +private struct CloserHeartHighlightShape: Shape { + enum Side { + case left + case right + } + + let side: Side + + func path(in rect: CGRect) -> Path { + var path = Path() + switch side { + case .left: + path.move(to: point(27, 42, in: rect)) + path.addCurve( + to: point(42, 27, in: rect), + control1: point(28, 32, in: rect), + control2: point(34, 27, in: rect) + ) + path.addCurve( + to: point(54, 35, in: rect), + control1: point(48, 27, in: rect), + control2: point(52, 30, in: rect) + ) + path.addLine(to: point(54, 41, in: rect)) + path.addCurve( + to: point(27, 42, in: rect), + control1: point(47, 36, in: rect), + control2: point(37, 36, in: rect) + ) + path.closeSubpath() + case .right: + path.move(to: point(54, 35, in: rect)) + path.addCurve( + to: point(69, 27, in: rect), + control1: point(57, 30, in: rect), + control2: point(62, 27, in: rect) + ) + path.addCurve( + to: point(85, 42, in: rect), + control1: point(78, 27, in: rect), + control2: point(84, 32, in: rect) + ) + path.addCurve( + to: point(54, 41, in: rect), + control1: point(75, 36, in: rect), + control2: point(65, 36, in: rect) + ) + path.closeSubpath() + } + return path + } +} + +private func point(_ x: CGFloat, _ y: CGFloat, in rect: CGRect) -> CGPoint { + CGPoint( + x: rect.minX + rect.width * (x / 108), + y: rect.minY + rect.height * (y / 108) + ) +} + // MARK: - Error View struct ErrorView: View {