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
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue