feat: wheel screen, play hub, storage data source, iOS wheel/play views
This commit is contained in:
parent
be781adbde
commit
4dcafa688e
|
|
@ -27,6 +27,9 @@ BUILD_SUMMARY.md
|
||||||
SCRIPTS.md
|
SCRIPTS.md
|
||||||
.learnings/
|
.learnings/
|
||||||
|
|
||||||
|
serviceAccount.json
|
||||||
|
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
*.apk
|
*.apk
|
||||||
*.aab
|
*.aab
|
||||||
|
|
@ -60,3 +63,5 @@ closer_partner_proof_reveal_privacy.md
|
||||||
app/google-services.json.bk
|
app/google-services.json.bk
|
||||||
app/GoogleService-Info.plist
|
app/GoogleService-Info.plist
|
||||||
docs/SUBSCRIPTION_GO_LIVE.md
|
docs/SUBSCRIPTION_GO_LIVE.md
|
||||||
|
ios_encrypt.md
|
||||||
|
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package app.closer.data.remote
|
package app.closer.data.remote
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.firebase.storage.FirebaseStorage
|
import com.google.firebase.storage.FirebaseStorage
|
||||||
|
import com.google.firebase.storage.StorageMetadata
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -9,14 +12,22 @@ import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class FirebaseStorageDataSource @Inject constructor() {
|
class FirebaseStorageDataSource @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
private val storage = FirebaseStorage.getInstance()
|
private val storage = FirebaseStorage.getInstance()
|
||||||
|
|
||||||
suspend fun uploadProfilePhoto(uid: String, uri: Uri): String =
|
suspend fun uploadProfilePhoto(uid: String, uri: Uri): String =
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
val ref = storage.reference.child("users/$uid/profile.jpg")
|
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 }
|
.continueWithTask { ref.downloadUrl }
|
||||||
.addOnSuccessListener { cont.resume(it.toString()) }
|
.addOnSuccessListener { cont.resume(it.toString()) }
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
.addOnFailureListener { cont.resumeWithException(it) }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package app.closer.ui.play
|
package app.closer.ui.play
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -20,7 +21,6 @@ import androidx.compose.material.icons.filled.Done
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.Lock
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.filled.Star
|
import androidx.compose.material.icons.filled.Star
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -31,10 +31,13 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.closer.R
|
||||||
import app.closer.core.navigation.AppRoute
|
import app.closer.core.navigation.AppRoute
|
||||||
import app.closer.ui.components.CategoryGlyph
|
import app.closer.ui.components.CategoryGlyph
|
||||||
import app.closer.ui.components.CloserActionButton
|
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.components.CloserRadii
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.theme.closerBrandGlyphBrush
|
|
||||||
import app.closer.ui.theme.closerPlayCardBrush
|
import app.closer.ui.theme.closerPlayCardBrush
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -685,17 +687,17 @@ private fun WheelGlyph(
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = RoundedCornerShape(26.dp),
|
shape = RoundedCornerShape(26.dp),
|
||||||
color = CloserPalette.PurpleDeep
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.62f)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier.background(closerBrandGlyphBrush())
|
modifier = Modifier.padding(5.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Image(
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
painter = painterResource(R.drawable.illustration_spin_wheel),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.surface,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier.size(34.dp)
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
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
|
||||||
|
|
@ -43,15 +44,17 @@ import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
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.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
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import app.closer.R
|
||||||
import app.closer.ui.theme.CloserPalette
|
import app.closer.ui.theme.CloserPalette
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
import app.closer.ui.theme.closerWheelSegmentColors
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpinWheelScreen(
|
fun SpinWheelScreen(
|
||||||
|
|
@ -231,7 +234,6 @@ private fun WheelSpinner(
|
||||||
spunAndReady: Boolean,
|
spunAndReady: Boolean,
|
||||||
rotation: Float
|
rotation: Float
|
||||||
) {
|
) {
|
||||||
val segmentColors = closerWheelSegmentColors()
|
|
||||||
val idleTransition = rememberInfiniteTransition(label = "wheel_idle")
|
val idleTransition = rememberInfiniteTransition(label = "wheel_idle")
|
||||||
val idlePulse by idleTransition.animateFloat(
|
val idlePulse by idleTransition.animateFloat(
|
||||||
initialValue = 0.98f,
|
initialValue = 0.98f,
|
||||||
|
|
@ -260,24 +262,18 @@ private fun WheelSpinner(
|
||||||
.scale(if (isSpinning) 1f else idlePulse),
|
.scale(if (isSpinning) 1f else idlePulse),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Canvas(
|
Image(
|
||||||
|
painter = painterResource(R.drawable.illustration_spin_wheel),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(236.dp)
|
.size(236.dp)
|
||||||
.rotate(if (isSpinning) rotation else 0f)
|
.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(
|
drawCircle(
|
||||||
color = wheelRingColor,
|
color = wheelRingColor,
|
||||||
style = Stroke(width = 7.dp.toPx())
|
style = Stroke(width = 7.dp.toPx())
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 748 KiB |
|
|
@ -90,9 +90,16 @@ struct GameCard: View {
|
||||||
RoundedRectangle(cornerRadius: CloserRadius.medium)
|
RoundedRectangle(cornerRadius: CloserRadius.medium)
|
||||||
.fill(color.opacity(0.15))
|
.fill(color.opacity(0.15))
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
Image(systemName: icon)
|
if gameType == .wheel {
|
||||||
.font(.title2)
|
Image("illustration-spin-wheel")
|
||||||
.foregroundColor(color)
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 52, height: 52)
|
||||||
|
} else {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
|
@ -716,4 +723,4 @@ extension String {
|
||||||
func replacing(_ target: String, with replacement: String) -> String {
|
func replacing(_ target: String, with replacement: String) -> String {
|
||||||
replacingOccurrences(of: target, with: replacement)
|
replacingOccurrences(of: target, with: replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 748 KiB |
|
|
@ -87,29 +87,29 @@ struct SpinWheelView: View {
|
||||||
|
|
||||||
// Wheel
|
// Wheel
|
||||||
ZStack {
|
ZStack {
|
||||||
ForEach(slices.indices, id: \.self) { index in
|
Image("illustration-spin-wheel")
|
||||||
WheelSlice(
|
.resizable()
|
||||||
label: slices[index].label,
|
.scaledToFit()
|
||||||
color: slices[index].color,
|
.frame(width: 320, height: 320)
|
||||||
startAngle: Double(index) * (360.0 / Double(slices.count)),
|
.rotationEffect(.degrees(rotation))
|
||||||
endAngle: Double(index + 1) * (360.0 / Double(slices.count))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center circle
|
// Center circle
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Color.closerBackground)
|
.fill(Color.closerBackground)
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
.closerShadow(level: .medium)
|
.closerShadow(level: .medium)
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.closerSurface.opacity(0.9), lineWidth: 8)
|
||||||
|
.frame(width: 320, height: 320)
|
||||||
|
|
||||||
// Pointer (top)
|
// Pointer (top)
|
||||||
Image(systemName: "arrowtriangle.down.fill")
|
Image(systemName: "arrowtriangle.down.fill")
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.foregroundColor(.closerDanger)
|
.foregroundColor(.closerPrimary)
|
||||||
.offset(y: -160)
|
.offset(y: -160)
|
||||||
}
|
}
|
||||||
.frame(width: 320, height: 320)
|
.frame(width: 320, height: 320)
|
||||||
.rotationEffect(.degrees(rotation))
|
|
||||||
.animation(isSpinning ? .spring(response: 1.5, dampingFraction: 0.6) : .default, value: rotation)
|
.animation(isSpinning ? .spring(response: 1.5, dampingFraction: 0.6) : .default, value: rotation)
|
||||||
|
|
||||||
// Result
|
// 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
|
// MARK: - Wheel Session
|
||||||
|
|
||||||
struct WheelSessionView: View {
|
struct WheelSessionView: View {
|
||||||
|
|
@ -375,4 +334,3 @@ struct WheelHistoryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue