feat: wheel screen, play hub, storage data source, iOS wheel/play views

This commit is contained in:
null 2026-06-22 10:25:58 -05:00
parent a2068f2dca
commit ecc41a77d2
8 changed files with 62 additions and 83 deletions

5
.gitignore vendored
View File

@ -27,6 +27,9 @@ BUILD_SUMMARY.md
SCRIPTS.md
.learnings/
serviceAccount.json
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json
# Build artifacts
*.apk
*.aab
@ -60,3 +63,5 @@ closer_partner_proof_reveal_privacy.md
app/google-services.json.bk
app/GoogleService-Info.plist
docs/SUBSCRIPTION_GO_LIVE.md
ios_encrypt.md
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json

View File

@ -1,7 +1,10 @@
package app.closer.data.remote
import android.content.Context
import android.net.Uri
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageMetadata
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import javax.inject.Singleton
@ -9,14 +12,22 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Singleton
class FirebaseStorageDataSource @Inject constructor() {
class FirebaseStorageDataSource @Inject constructor(
@ApplicationContext private val context: Context
) {
private val storage = FirebaseStorage.getInstance()
suspend fun uploadProfilePhoto(uid: String, uri: Uri): String =
suspendCancellableCoroutine { cont ->
val ref = storage.reference.child("users/$uid/profile.jpg")
ref.putFile(uri)
val mimeType = context.contentResolver.getType(uri)
?.takeIf { it.startsWith("image/") }
?: "image/jpeg"
val metadata = StorageMetadata.Builder()
.setContentType(mimeType)
.build()
ref.putFile(uri, metadata)
.continueWithTask { ref.downloadUrl }
.addOnSuccessListener { cont.resume(it.toString()) }
.addOnFailureListener { cont.resumeWithException(it) }

View File

@ -1,5 +1,6 @@
package app.closer.ui.play
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -20,7 +21,6 @@ import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -31,10 +31,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.closer.R
import app.closer.core.navigation.AppRoute
import app.closer.ui.components.CategoryGlyph
import app.closer.ui.components.CloserActionButton
@ -45,7 +48,6 @@ import app.closer.ui.components.CloserPill
import app.closer.ui.components.CloserRadii
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerBrandGlyphBrush
import app.closer.ui.theme.closerPlayCardBrush
@Composable
@ -685,17 +687,17 @@ private fun WheelGlyph(
Surface(
modifier = modifier,
shape = RoundedCornerShape(26.dp),
color = CloserPalette.PurpleDeep
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.62f)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.background(closerBrandGlyphBrush())
modifier = Modifier.padding(5.dp)
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
Image(
painter = painterResource(R.drawable.illustration_spin_wheel),
contentDescription = null,
tint = MaterialTheme.colorScheme.surface,
modifier = Modifier.size(34.dp)
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}

View File

@ -10,6 +10,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -43,15 +44,17 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.R
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.closerBackgroundBrush
import app.closer.ui.theme.closerWheelSegmentColors
@Composable
fun SpinWheelScreen(
@ -231,7 +234,6 @@ private fun WheelSpinner(
spunAndReady: Boolean,
rotation: Float
) {
val segmentColors = closerWheelSegmentColors()
val idleTransition = rememberInfiniteTransition(label = "wheel_idle")
val idlePulse by idleTransition.animateFloat(
initialValue = 0.98f,
@ -260,24 +262,18 @@ private fun WheelSpinner(
.scale(if (isSpinning) 1f else idlePulse),
contentAlignment = Alignment.Center
) {
Canvas(
Image(
painter = painterResource(R.drawable.illustration_spin_wheel),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.size(236.dp)
.rotate(if (isSpinning) rotation else 0f)
)
Canvas(
modifier = Modifier.size(236.dp)
) {
val sweep = 360f / segmentColors.size
segmentColors.forEachIndexed { index, color ->
drawArc(
color = color,
startAngle = -90f + (index * sweep),
sweepAngle = sweep,
useCenter = true
)
}
drawCircle(
color = Color.White.copy(alpha = 0.78f),
radius = size.minDimension * 0.24f
)
drawCircle(
color = wheelRingColor,
style = Stroke(width = 7.dp.toPx())

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

View File

@ -90,9 +90,16 @@ struct GameCard: View {
RoundedRectangle(cornerRadius: CloserRadius.medium)
.fill(color.opacity(0.15))
.frame(width: 60, height: 60)
Image(systemName: icon)
.font(.title2)
.foregroundColor(color)
if gameType == .wheel {
Image("illustration-spin-wheel")
.resizable()
.scaledToFit()
.frame(width: 52, height: 52)
} else {
Image(systemName: icon)
.font(.title2)
.foregroundColor(color)
}
}
VStack(alignment: .leading, spacing: 4) {
@ -716,4 +723,4 @@ extension String {
func replacing(_ target: String, with replacement: String) -> String {
replacingOccurrences(of: target, with: replacement)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

View File

@ -87,29 +87,29 @@ struct SpinWheelView: View {
// Wheel
ZStack {
ForEach(slices.indices, id: \.self) { index in
WheelSlice(
label: slices[index].label,
color: slices[index].color,
startAngle: Double(index) * (360.0 / Double(slices.count)),
endAngle: Double(index + 1) * (360.0 / Double(slices.count))
)
}
Image("illustration-spin-wheel")
.resizable()
.scaledToFit()
.frame(width: 320, height: 320)
.rotationEffect(.degrees(rotation))
// Center circle
Circle()
.fill(Color.closerBackground)
.frame(width: 60, height: 60)
.closerShadow(level: .medium)
Circle()
.stroke(Color.closerSurface.opacity(0.9), lineWidth: 8)
.frame(width: 320, height: 320)
// Pointer (top)
Image(systemName: "arrowtriangle.down.fill")
.font(.title)
.foregroundColor(.closerDanger)
.foregroundColor(.closerPrimary)
.offset(y: -160)
}
.frame(width: 320, height: 320)
.rotationEffect(.degrees(rotation))
.animation(isSpinning ? .spring(response: 1.5, dampingFraction: 0.6) : .default, value: rotation)
// Result
@ -180,47 +180,6 @@ struct SpinWheelView: View {
}
}
// MARK: - Wheel Slice
struct WheelSlice: View {
let label: String
let color: Color
let startAngle: Double
let endAngle: Double
var body: some View {
GeometryReader { geo in
let center = CGPoint(x: geo.size.width / 2, y: geo.size.height / 2)
let radius = min(geo.size.width, geo.size.height) / 2
Path { path in
path.move(to: center)
path.addArc(
center: center,
radius: radius,
startAngle: .degrees(startAngle - 90),
endAngle: .degrees(endAngle - 90),
clockwise: false
)
path.closeSubpath()
}
.fill(color.opacity(0.3))
// Label
let midAngle = (startAngle + endAngle) / 2 - 90
let labelRadius = radius * 0.7
let x = center.x + labelRadius * cos(CGFloat(midAngle) * .pi / 180)
let y = center.y + labelRadius * sin(CGFloat(midAngle) * .pi / 180)
Text(label)
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.closerText)
.position(x: x, y: y)
.rotationEffect(.degrees(midAngle + 90))
}
}
}
// MARK: - Wheel Session
struct WheelSessionView: View {
@ -375,4 +334,3 @@ struct WheelHistoryView: View {
}
}
}