Browse Source

Registration upload profile

master
osman160128 1 month ago
parent
commit
ad258eb831
  1. 12
      .idea/deploymentTargetSelector.xml
  2. 3
      app/build.gradle.kts
  3. 24
      app/src/main/AndroidManifest.xml
  4. 8
      app/src/main/java/com/example/dattingapp/RegistrationScreen.kt
  5. 439
      app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt
  6. 417
      app/src/main/java/com/example/dattingapp/UploadProfileImageActivity.kt
  7. 44
      app/src/main/java/com/example/dattingapp/registration/ApiService.kt
  8. 19
      app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt
  9. 76
      app/src/main/java/com/example/dattingapp/registration/NetworkModule.kt
  10. 45
      app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt
  11. 46
      app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt
  12. 96
      app/src/main/java/com/example/dattingapp/registration/RegistrationModels.kt
  13. 54
      app/src/main/java/com/example/dattingapp/registration/RegistrationModule.kt
  14. 187
      app/src/main/java/com/example/dattingapp/registration/RegistrationRepository.kt
  15. 960
      app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt
  16. 103
      app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt
  17. 51
      app/src/main/java/com/example/dattingapp/registration/TokenManager.kt
  18. 1201
      app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt
  19. 172
      app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt
  20. 10
      app/src/main/java/com/example/dattingapp/ui/theme/Color.kt
  21. 5
      app/src/main/res/xml/file_paths.xml

12
.idea/deploymentTargetSelector.xml

@ -6,6 +6,18 @@
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="main">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="unitTest">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="androidTest">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>

3
app/build.gradle.kts

@ -81,6 +81,9 @@ dependencies {
implementation(libs.media3.exoplayer) implementation(libs.media3.exoplayer)
implementation(libs.media3.ui) implementation(libs.media3.ui)
implementation("androidx.security:security-crypto:1.1.0-alpha06")
} }

24
app/src/main/AndroidManifest.xml

@ -123,30 +123,20 @@
android:label="@string/title_activity_meet" android:label="@string/title_activity_meet"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<!-- FileProvider declaration -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.dattingapp.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity <activity
android:name=".registration.UploadProfileImageActivity" android:name=".registration.UploadProfileImageActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_upload_profile_image" android:label="@string/title_activity_meet"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".registration.UploadIntroVideoActivity" android:name=".registration.UploadIntroVideoActivity"
android:exported="false" android:exported="false" />
android:label="@string/title_activity_upload_intro_video"
android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" />
</application> </application>
</manifest> </manifest>

8
app/src/main/java/com/example/dattingapp/RegistrationScreen.kt

@ -28,9 +28,6 @@ import androidx.compose.ui.unit.sp
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -38,12 +35,9 @@ import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.Group import androidx.compose.material.icons.outlined.Group
import androidx.compose.material.icons.outlined.LocalBar import androidx.compose.material.icons.outlined.LocalBar
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import com.example.dattingapp.registration.UploadProfileImageActivity
import androidx.compose.ui.unit.sp
// Global Email Validation Regex // Global Email Validation Regex
val EMAIL_PATTERN_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") val EMAIL_PATTERN_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")

439
app/src/main/java/com/example/dattingapp/UploadIntroVideoActivity.kt

@ -1,439 +0,0 @@
package com.example.dattingapp
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.FileProvider
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import coil.compose.rememberAsyncImagePainter
import com.example.dattingapp.ui.theme.DattingAPPTheme
import java.io.File
enum class DisplayMode {
IMAGE,
VIDEO,
BIO
}
class UploadIntroVideoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Retrieve registration data from previous step
val isPhone = intent.getBooleanExtra("EXTRA_IS_PHONE", false)
val countryCode = intent.getStringExtra("EXTRA_COUNTRY_CODE") ?: ""
val contactValue = intent.getStringExtra("EXTRA_CONTACT_VALUE") ?: ""
val password = intent.getStringExtra("EXTRA_PASSWORD") ?: ""
val name = intent.getStringExtra("EXTRA_NAME") ?: ""
val dob = intent.getStringExtra("EXTRA_DOB") ?: ""
val gender = intent.getStringExtra("EXTRA_GENDER") ?: ""
val interests = intent.getStringArrayListExtra("EXTRA_INTERESTS") ?: arrayListOf()
val vibe = intent.getStringExtra("EXTRA_VIBE") ?: ""
// Retrieve Profile Image URI string
val profileImageUriString = intent.getStringExtra("EXTRA_PROFILE_IMAGE_URI")
val profileImageUri: Uri? = profileImageUriString?.let { Uri.parse(it) }
setContent {
DattingAPPTheme {
UploadIntroVideoScreen(
profileImageUri = profileImageUri,
onBackClick = { finish() },
onSkipClick = { /* Handle skip */ },
onSubmit = { videoUri, bioText ->
// Final registration submission logic
Toast.makeText(this, "Registration Data Ready!", Toast.LENGTH_SHORT).show()
}
)
}
}
}
}
@Composable
fun UploadIntroVideoScreen(
profileImageUri: Uri?,
onBackClick: () -> Unit = {},
onSkipClick: () -> Unit = {},
onSubmit: (videoUri: Uri?, bioText: String) -> Unit
) {
val context = LocalContext.current
var displayMode by remember { mutableStateOf(DisplayMode.IMAGE) }
var recordedVideoUri by remember { mutableStateOf<Uri?>(null) }
var bioText by remember { mutableStateOf("") }
// Launchers remain the same...
val videoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
if (uri != null) {
recordedVideoUri = uri
displayMode = DisplayMode.VIDEO
}
}
val recordVideoLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CaptureVideo()
) { success ->
if (success && recordedVideoUri != null) {
displayMode = DisplayMode.VIDEO
}
}
var tempCameraPhotoUri by remember { mutableStateOf<Uri?>(null) }
var capturedPhotoUri by remember { mutableStateOf<Uri?>(null) }
val takePhotoLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success ->
if (success && tempCameraPhotoUri != null) {
capturedPhotoUri = tempCameraPhotoUri
displayMode = DisplayMode.IMAGE
}
}
val buttonGradient = Brush.horizontalGradient(
colors = listOf(Color(0xFFB70054), Color(0xFF6700C7))
)
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF9F9F9))
.systemBarsPadding()
// 👇 ADD imePadding() HERE TO RESIZE WHEN KEYBOARD OPENS 👇
.imePadding()
.padding(horizontal = 20.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
// --- Header Section ---
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBackClick) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = Color.Black)
}
Text(
text = "VibeCheck",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF9C27B0)
)
TextButton(onClick = onSkipClick) {
Text(text = "SKIP", color = Color.Gray, fontWeight = FontWeight.Bold, fontSize = 12.sp)
}
}
// --- Header Title ---
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = if (displayMode == DisplayMode.BIO) "Your Bio" else "Video Introduction",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1E293B)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (displayMode == DisplayMode.BIO)
"Write a short intro about yourself (up to 200 words)."
else
"Say hello! Share a bit about yourself in 60 seconds.",
fontSize = 13.sp,
color = Color.Gray
)
}
// --- Central Media / Text Container ---
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(vertical = 12.dp)
.clip(RoundedCornerShape(20.dp))
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
when (displayMode) {
DisplayMode.IMAGE -> {
if (capturedPhotoUri != null || profileImageUri != null) {
Image(
painter = rememberAsyncImagePainter(model = capturedPhotoUri ?: profileImageUri),
contentDescription = "Profile Image Preview",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
DisplayMode.VIDEO -> {
recordedVideoUri?.let { uri ->
VideoPlayer(videoUri = uri)
}
}
DisplayMode.BIO -> {
OutlinedTextField(
value = bioText,
onValueChange = { input ->
val words = input.trim().split("\\s+".toRegex())
if (input.isEmpty() || words.size <= 200) bioText = input
},
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(12.dp),
placeholder = { Text("Write something about yourself (max 200 words)...") },
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.White,
focusedContainerColor = Color.White
)
)
}
}
// Overlay timer & action icons (hidden during Bio input)
if (displayMode != DisplayMode.BIO) {
Box(
modifier = Modifier
.padding(12.dp)
.align(Alignment.TopEnd)
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(alpha = 0.5f))
.padding(horizontal = 10.dp, vertical = 4.dp)
) {
Text(
text = "00:00 / 01:00",
color = Color.White,
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold
)
}
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 20.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.85f))
.clickable { videoPickerLauncher.launch("video/*") },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.FileUpload,
contentDescription = "Upload Gallery Video",
tint = Color(0xFF475569),
modifier = Modifier.size(24.dp)
)
}
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.9f))
.border(2.dp, Color(0xFFE11D48), CircleShape)
.clickable {
val tempUri = createVideoUri(context)
recordedVideoUri = tempUri
recordVideoLauncher.launch(tempUri)
},
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color(0xFFE11D48)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Videocam,
contentDescription = "Record Video",
tint = Color.White,
modifier = Modifier.size(26.dp)
)
}
}
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.85f))
.clickable {
val tempUri = createImageUri(context)
tempCameraPhotoUri = tempUri
takePhotoLauncher.launch(tempUri)
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PhotoCamera,
contentDescription = "Take Photo",
tint = Color(0xFF475569),
modifier = Modifier.size(24.dp)
)
}
}
}
}
// --- Bottom Buttons ---
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(
onClick = { onSubmit(recordedVideoUri, bioText) },
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(buttonGradient, shape = RoundedCornerShape(12.dp)),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
shape = RoundedCornerShape(12.dp)
) {
Text(text = "UPLOAD", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
OutlinedButton(
onClick = {
displayMode = if (displayMode == DisplayMode.BIO) {
if (recordedVideoUri != null) DisplayMode.VIDEO else DisplayMode.IMAGE
} else {
DisplayMode.BIO
}
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = RoundedCornerShape(12.dp),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.horizontalGradient(listOf(Color(0xFFE11D48), Color(0xFFE11D48)))
)
) {
Text(
text = if (displayMode == DisplayMode.BIO) "VIDEO INTRO" else "ADD BIO",
color = Color(0xFFE11D48),
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
}
}
}
}
// Media3 ExoPlayer composable
@Composable
fun VideoPlayer(videoUri: Uri) {
val context = LocalContext.current
val exoPlayer = remember(videoUri) {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videoUri))
prepare()
playWhenReady = true
}
}
DisposableEffect(Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = { ctx ->
PlayerView(ctx).apply {
player = exoPlayer
useController = true
}
},
modifier = Modifier.fillMaxSize()
)
}
// Helper functions to create temporary File Uris for Video and Image captures
fun createVideoUri(context: Context): Uri {
val tempFile = File.createTempFile("intro_video_", ".mp4", context.cacheDir).apply {
createNewFile()
deleteOnExit()
}
return FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
tempFile
)
}
fun createImageUrl(context: Context): Uri {
val tempFile = File.createTempFile("intro_photo_", ".jpg", context.cacheDir).apply {
createNewFile()
deleteOnExit()
}
return FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
tempFile
)
}
// --- Composable Previews ---
@Preview(
name = "Upload Intro Screen - Full Preview",
showBackground = true,
showSystemUi = true
)
@Composable
fun UploadIntroVideoScreenPreview() {
DattingAPPTheme {
UploadIntroVideoScreen(
profileImageUri = null,
onSubmit = { _, _ -> }
)
}
}

417
app/src/main/java/com/example/dattingapp/UploadProfileImageActivity.kt

@ -1,417 +0,0 @@
package com.example.dattingapp
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.AddAPhoto
import androidx.compose.material.icons.outlined.CameraAlt
import androidx.compose.material.icons.outlined.PhotoLibrary
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import coil.compose.rememberAsyncImagePainter
import java.io.File
class UploadProfileImageActivity : ComponentActivity() {
private val TAG = "kk"
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// Retrieve intent extras passed from previous registration steps
val isPhone = intent.getBooleanExtra("EXTRA_IS_PHONE", false)
val countryCode = intent.getStringExtra("EXTRA_COUNTRY_CODE") ?: ""
val contactValue = intent.getStringExtra("EXTRA_CONTACT_VALUE") ?: ""
val password = intent.getStringExtra("EXTRA_PASSWORD") ?: ""
val name = intent.getStringExtra("EXTRA_NAME") ?: ""
val dob = intent.getStringExtra("EXTRA_DOB") ?: ""
val gender = intent.getStringExtra("EXTRA_GENDER") ?: ""
val interests = intent.getStringArrayListExtra("EXTRA_INTERESTS") ?: arrayListOf()
val vibe = intent.getStringExtra("EXTRA_VIBE") ?: ""
Log.d(TAG, "=== Received Registration Data ===")
Log.d(TAG, "Is Phone: $isPhone")
Log.d(TAG, "Country Code: $countryCode")
Log.d(TAG, "Contact Value: $contactValue")
Log.d(TAG, "Password: $password")
Log.d(TAG, "Name: $name")
Log.d(TAG, "Date of Birth: $dob")
Log.d(TAG, "Gender: $gender")
Log.d(TAG, "Interests: $interests")
Log.d(TAG, "Vibe: $vibe")
Log.d(TAG, "=================================")
setContent {
MaterialTheme {
UploadProfileImageScreen(
onBackClick = { finish() },
onContinueClick = { selectedUri ->
if (selectedUri == null) {
Toast.makeText(this@UploadProfileImageActivity, "Please select or take a photo first", Toast.LENGTH_SHORT).show()
return@UploadProfileImageScreen
}
val intent = Intent(
this@UploadProfileImageActivity,
UploadIntroVideoActivity::class.java
).apply {
// 1. Pass down all data received from previous steps
putExtra("EXTRA_IS_PHONE", isPhone)
putExtra("EXTRA_COUNTRY_CODE", countryCode)
putExtra("EXTRA_CONTACT_VALUE", contactValue)
putExtra("EXTRA_PASSWORD", password)
putExtra("EXTRA_NAME", name)
putExtra("EXTRA_DOB", dob)
putExtra("EXTRA_GENDER", gender)
putStringArrayListExtra("EXTRA_INTERESTS", interests)
putExtra("EXTRA_VIBE", vibe)
// 2. Pass the selected Profile Image Uri as a String
putExtra("EXTRA_PROFILE_IMAGE_URI", selectedUri.toString())
}
startActivity(intent)
}
)
}
}
}
}
// Helper function to create temporary File & Uri for Camera launcher
fun createImageUri(context: Context): Uri {
val tempFile = File.createTempFile("profile_camera_", ".jpg", context.cacheDir).apply {
createNewFile()
deleteOnExit()
}
return FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
tempFile
)
}
@Composable
fun UploadProfileImageScreen(
onBackClick: () -> Unit,
onContinueClick: (Uri?) -> Unit
) {
val context = LocalContext.current
// Single state variable to store image source (from either Gallery OR Camera)
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
var tempCameraUri by remember { mutableStateOf<Uri?>(null) }
// 1. Gallery Launcher
val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
if (uri != null) {
selectedImageUri = uri
}
}
// 2. Camera Launcher
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success: Boolean ->
if (success && tempCameraUri != null) {
selectedImageUri = tempCameraUri
}
}
// Design Gradients
val backgroundGradient = Brush.verticalGradient(
colors = listOf(
Color(0xFFFFF5F5),
Color(0xFFFAFAFA),
Color(0xFFF3E8FF)
)
)
val buttonGradient = Brush.horizontalGradient(
colors = listOf(
Color(0xFFC2185B),
Color(0xFF7B1FA2)
)
)
val circleBorderGradient = Brush.sweepGradient(
colors = listOf(
Color(0xFF7B1FA2),
Color(0xFFC2185B),
Color(0xFFE91E63),
Color(0xFF7B1FA2)
)
)
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundGradient)
.systemBarsPadding()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
// Header Bar
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
IconButton(
onClick = onBackClick,
modifier = Modifier.align(Alignment.CenterStart)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.Black
)
}
Text(
text = "VibeCheck",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFFC2185B),
modifier = Modifier.align(Alignment.Center)
)
}
// Central Content Area
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Upload profile photo",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Choose a photo that shows your best self. You can always change this later.",
fontSize = 13.sp,
color = Color.Gray,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 20.dp)
)
Spacer(modifier = Modifier.height(32.dp))
// Profile Circular Container
Box(
modifier = Modifier
.size(190.dp)
.clip(CircleShape)
.background(Color.White)
.border(3.dp, circleBorderGradient, CircleShape),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(165.dp)
.clip(CircleShape)
.drawWithContent {
drawContent()
drawCircle(
color = Color(0xFFF48FB1),
style = Stroke(
width = 3f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f)
)
)
},
contentAlignment = Alignment.Center
) {
if (selectedImageUri != null) {
Image(
painter = rememberAsyncImagePainter(selectedImageUri),
contentDescription = "Profile Photo",
modifier = Modifier
.fillMaxSize()
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Icon(
imageVector = Icons.Outlined.AddAPhoto,
contentDescription = "Camera Icon",
tint = Color(0xFFE57373),
modifier = Modifier.size(44.dp)
)
}
}
}
Spacer(modifier = Modifier.height(36.dp))
// Upload from gallery Button
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)),
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.clickable { galleryLauncher.launch("image/*") }
) {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.PhotoLibrary,
contentDescription = "Gallery",
tint = Color(0xFF673AB7),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = "Upload from gallery",
color = Color(0xFF673AB7),
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Take a photo Button
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)),
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.clickable {
val uri = createImageUri(context)
tempCameraUri = uri
cameraLauncher.launch(uri)
}
) {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.CameraAlt,
contentDescription = "Camera",
tint = Color(0xFF673AB7),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = "Take a photo",
color = Color(0xFF673AB7),
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
}
}
// Bottom Continue Section
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = { onContinueClick(selectedImageUri) },
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.background(buttonGradient, shape = RoundedCornerShape(26.dp)),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
shape = RoundedCornerShape(26.dp),
contentPadding = PaddingValues()
) {
Text(
text = "Continue",
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(14.dp))
Text(
text = buildAnnotatedString {
append("Need help? ")
withStyle(style = SpanStyle(color = Color(0xFFC2185B), fontWeight = FontWeight.Bold)) {
append("Contact support")
}
},
fontSize = 12.sp,
color = Color.Gray,
modifier = Modifier.clickable { }
)
}
}
}
}
// 1. Preview default state (No photo selected yet)
@Preview(
name = "Upload Photo - Empty State",
showBackground = true,
showSystemUi = true
)
@Composable
fun UploadProfileImageScreenPreview() {
MaterialTheme {
UploadProfileImageScreen(
onBackClick = {},
onContinueClick = {}
)
}
}

44
app/src/main/java/com/example/dattingapp/registration/ApiService.kt

@ -1,44 +0,0 @@
package com.example.dattingapp.registration
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface ApiService {
// ==========================================
// Upload Profile Image
// ==========================================
@Multipart
@POST("api/v1/users/me/image")
suspend fun uploadProfileImage(
@Header("Authorization") authorization: String,
@Part image: MultipartBody.Part
): Response<UploadResponse>
// ==========================================
// Upload Video
// ==========================================
@Multipart
@POST("api/v1/users/me/video")
suspend fun uploadProfileVideo(
@Header("Authorization") authorization: String,
@Part video: MultipartBody.Part
): Response<UploadResponse>
// ==========================================
// Final Registration
// ==========================================
@POST("api/v1/auth/register")
suspend fun register(
@Body request: RegisterRequest
): Response<RegisterResponse>
}

19
app/src/main/java/com/example/dattingapp/registration/AuthApiService.kt

@ -0,0 +1,19 @@
package com.example.dattingapp.registration
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface AuthApiService {
@POST("/api/v1/auth/register")
suspend fun registerUser(@Body request: RegisterRequest): Response<RegisterResponse>
@Multipart
@POST("api/v1/users/me/image")
suspend fun uploadProfileImage(
@Part image: MultipartBody.Part
): Response<ProfileImageUploadResponse>
}

76
app/src/main/java/com/example/dattingapp/registration/NetworkModule.kt

@ -0,0 +1,76 @@
package com.example.dattingapp.registration
import android.content.Context
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.asRequestBody
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.io.FileOutputStream
object NetworkModule {
private const val BASE_URL = "https://melove.youthome.com.cn"
@Volatile
private var apiService: AuthApiService? = null
fun getApiService(context: Context): AuthApiService {
return apiService ?: synchronized(this) {
apiService ?: buildRetrofit(context.applicationContext).create(AuthApiService::class.java).also {
apiService = it
}
}
}
private fun buildRetrofit(context: Context): Retrofit {
val tokenManager = TokenManager.getInstance(context)
val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val requestBuilder = chain.request().newBuilder()
val token = tokenManager.getAccessToken()
if (!token.isNullOrEmpty()) {
requestBuilder.addHeader("Authorization", "Bearer $token")
}
chain.proceed(requestBuilder.build())
}
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun prepareImagePart(context: Context, uri: Uri, partName: String = "image"): MultipartBody.Part? {
return try {
val contentResolver = context.contentResolver
val mimeType = contentResolver.getType(uri) ?: "image/jpeg"
val inputStream = contentResolver.openInputStream(uri) ?: return null
val extension = when (mimeType) {
"image/png" -> ".png"
"image/webp" -> ".webp"
else -> ".jpg"
}
val tempFile = File.createTempFile("upload_image_", extension, context.cacheDir)
FileOutputStream(tempFile).use { output ->
inputStream.copyTo(output)
}
val requestFile = tempFile.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData(partName, tempFile.name, requestFile)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}

45
app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt

@ -0,0 +1,45 @@
package com.example.dattingapp.registration
import com.google.gson.annotations.SerializedName
// Request Data Model matching your JSON specification
data class RegisterRequest(
@SerializedName("email_or_phone") val emailOrPhone: String,
@SerializedName("password") val password: String,
@SerializedName("confirm_password") val confirmPassword: String,
@SerializedName("full_name") val fullName: String,
@SerializedName("gender") val gender: String,
@SerializedName("birth_date") val birthDate: String,
@SerializedName("city") val city: String,
@SerializedName("bio") val bio: String? = null,
@SerializedName("video_intro") val videoIntro: String? = null,
@SerializedName("Profile_image") val profileImage: String? = null,
@SerializedName("interests") val interests: List<String>? = null
)
data class RegisterResponse(
@SerializedName("access_token")
val accessToken: String,
@SerializedName("refresh_token")
val refreshToken: String,
@SerializedName("token_type")
val tokenType: String
)
// Response data class representing the API response
data class ProfileImageUploadResponse(
val status: Boolean,
val message: String?,
val data: ProfileImageData?
)
data class ProfileImageData(
val url: String? // Adjust field name according to your API response JSON structure
)
data class ImageUploadResponse(
val message: String,
val imageUrl: String?
)

46
app/src/main/java/com/example/dattingapp/registration/RegistrationFileUtils.kt

@ -1,46 +0,0 @@
package com.example.dattingapp.registration
import android.content.Context
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
fun uriToMultipart(
context: Context,
uri: Uri,
fieldName: String,
fileName: String
): MultipartBody.Part {
val inputStream = context.contentResolver.openInputStream(uri)
?: throw Exception("Cannot open selected file")
val tempFile = File(
context.cacheDir,
fileName
)
inputStream.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType =
context.contentResolver.getType(uri)
?: "application/octet-stream"
val requestBody =
tempFile.asRequestBody(
mimeType.toMediaTypeOrNull()
)
return MultipartBody.Part.createFormData(
fieldName,
tempFile.name,
requestBody
)
}

96
app/src/main/java/com/example/dattingapp/registration/RegistrationModels.kt

@ -1,96 +0,0 @@
package com.example.dattingapp.registration
import com.google.gson.annotations.SerializedName
// ===============================
// Upload response
// ===============================
data class UploadResponse(
@SerializedName("url")
val url: String?,
@SerializedName("image_url")
val imageUrl: String?,
@SerializedName("video_url")
val videoUrl: String?
) {
fun getReturnedUrl(): String? {
return url ?: imageUrl ?: videoUrl
}
}
// ===============================
// Registration request
// ===============================
data class RegisterRequest(
@SerializedName("email_or_phone")
val emailOrPhone: String,
@SerializedName("password")
val password: String,
@SerializedName("confirm_password")
val confirmPassword: String,
@SerializedName("full_name")
val fullName: String,
@SerializedName("gender")
val gender: String,
@SerializedName("birth_date")
val birthDate: String,
@SerializedName("city")
val city: String,
@SerializedName("bio")
val bio: String,
@SerializedName("video_intro")
val videoIntro: String?,
@SerializedName("profile_image")
val profileImage: String?,
@SerializedName("interests")
val interests: List<String>
)
// ===============================
// Registration response
// ===============================
data class RegisterResponse(
@SerializedName("access_token")
val accessToken: String?,
@SerializedName("refresh_token")
val refreshToken: String?,
@SerializedName("token_type")
val tokenType: String?,
@SerializedName("user")
val user: User?
)
data class User(
@SerializedName("id")
val id: Int?,
@SerializedName("email_or_phone")
val emailOrPhone: String?,
@SerializedName("full_name")
val fullName: String?
)

54
app/src/main/java/com/example/dattingapp/registration/RegistrationModule.kt

@ -1,54 +0,0 @@
package com.example.dattingapp.registration
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NetworkModule {
private const val AUTH_BASE_URL =
"https://counts-indicates-stud-addressing.trycloudflare.com"
private const val UPLOAD_BASE_URL =
"https://counts-indicates-stud-addressing.trycloudflare.com/"
private fun createClient(): OkHttpClient {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build()
}
private fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(createClient())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
// Registration API
val authApi: ApiService by lazy {
createRetrofit(AUTH_BASE_URL)
.create(ApiService::class.java)
}
// Upload API
val uploadApi: ApiService by lazy {
createRetrofit(UPLOAD_BASE_URL)
.create(ApiService::class.java)
}
}

187
app/src/main/java/com/example/dattingapp/registration/RegistrationRepository.kt

@ -1,187 +0,0 @@
package com.example.dattingapp.registration
import android.content.Context
import android.net.Uri
import android.util.Log
class RegistrationRepository(
private val context: Context
) {
private val uploadApi =
NetworkModule.uploadApi
private val authApi =
NetworkModule.authApi
// ==========================================
// Upload Image
// ==========================================
suspend fun uploadImage(
token: String,
uri: Uri
): Result<String> {
return try {
val multipart = uriToMultipart(
context = context,
uri = uri,
fieldName = "image",
fileName = "profile_image.jpg"
)
val response =
uploadApi.uploadProfileImage(
authorization = token,
image = multipart
)
if (response.isSuccessful) {
val body = response.body()
val url = body?.getReturnedUrl()
if (!url.isNullOrEmpty()) {
Result.success(url)
} else {
Result.failure(
Exception(
"Image uploaded but backend did not return URL"
)
)
}
} else {
Result.failure(
Exception(
"Image upload failed: ${response.code()} ${response.message()}"
)
)
}
} catch (e: Exception) {
Result.failure(e)
}
}
// ==========================================
// Upload Video
// ==========================================
suspend fun uploadVideo(
token: String,
uri: Uri
): Result<String> {
return try {
val multipart = uriToMultipart(
context = context,
uri = uri,
fieldName = "video",
fileName = "intro_video.mp4"
)
val response = uploadApi.uploadProfileVideo(
authorization = token,
video = multipart
)
if (response.isSuccessful) {
val body = response.body()
val url = body?.getReturnedUrl()
if (!url.isNullOrEmpty()) {
Result.success(url)
} else {
Result.failure(
Exception(
"Video uploaded but backend did not return URL"
)
)
}
} else {
Result.failure(
Exception(
"Video upload failed: ${response.code()} ${response.message()}"
)
)
}
} catch (e: Exception) {
Log.e(
"UPLOAD_VIDEO",
"Exception while uploading video",
e
)
Result.failure(e)
}
}
// ==========================================
// Final Registration
// ==========================================
suspend fun register(
request: RegisterRequest
): Result<RegisterResponse> {
return try {
val response =
authApi.register(request)
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
Result.success(body)
} else {
Result.failure(
Exception(
"Registration response is empty"
)
)
}
} else {
Result.failure(
Exception(
"Registration failed: ${response.code()} ${response.message()}"
)
)
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

960
app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt

File diff suppressed because it is too large

103
app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt

@ -0,0 +1,103 @@
package com.example.dattingapp.registration
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.dattingapp.registration.NetworkModule.prepareImagePart
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
sealed class RegistrationUiState {
object Idle : RegistrationUiState()
object Loading : RegistrationUiState()
data class Success(val response: RegisterResponse) : RegistrationUiState()
data class ImageUploadSuccess(val response: ProfileImageUploadResponse) : RegistrationUiState()
data class Error(val message: String) : RegistrationUiState()
}
class RegistrationViewModel : ViewModel() {
private val TAG = "RegistrationViewModel"
private val _uiState = MutableStateFlow<RegistrationUiState>(RegistrationUiState.Idle)
val uiState: StateFlow<RegistrationUiState> = _uiState.asStateFlow()
fun registerUser(context: Context, request: RegisterRequest) {
viewModelScope.launch {
_uiState.value = RegistrationUiState.Loading
try {
val apiService = NetworkModule.getApiService(context)
val response = apiService.registerUser(request)
if (response.isSuccessful && response.body() != null) {
val body = response.body()!!
TokenManager.getInstance(context.applicationContext).saveTokens(
accessToken = body.accessToken,
refreshToken = body.refreshToken
)
Log.d(TAG, "Registration successful. Access token saved.")
_uiState.value = RegistrationUiState.Success(body)
} else {
val errorMsg = response.errorBody()?.string()
?: "Registration failed with code ${response.code()}"
Log.e(TAG, "Registration failed: $errorMsg")
_uiState.value = RegistrationUiState.Error(errorMsg)
}
} catch (e: Exception) {
Log.e(TAG, "Registration exception", e)
_uiState.value = RegistrationUiState.Error(e.localizedMessage ?: "An unexpected error occurred")
}
}
}
fun uploadProfileImage(context: Context, imageUri: Uri) {
val tokenManager = TokenManager.getInstance(context.applicationContext)
val token = tokenManager.getAccessToken()
if (token.isNullOrEmpty()) {
Log.e(TAG, "Upload failed: Access token is missing from storage.")
_uiState.value = RegistrationUiState.Error("Authentication token is missing.")
return
}
viewModelScope.launch {
_uiState.value = RegistrationUiState.Loading
Log.d(TAG, "Preparing image for upload from URI: $imageUri")
val imagePart = prepareImagePart(context, imageUri, partName = "image")
if (imagePart == null) {
Log.e(TAG, "Failed to create MultipartBody.Part from Uri.")
_uiState.value = RegistrationUiState.Error("Failed to prepare image file for upload.")
return@launch
}
try {
Log.d(TAG, "Uploading image via API...")
val apiService = NetworkModule.getApiService(context)
val response = apiService.uploadProfileImage(imagePart)
if (response.isSuccessful && response.body() != null) {
Log.d(TAG, "Image upload successful: ${response.body()}")
_uiState.value = RegistrationUiState.ImageUploadSuccess(response.body()!!)
} else {
val errorMsg = response.errorBody()?.string()
?: "Image upload failed with code ${response.code()}"
Log.e(TAG, "Image upload error [Code ${response.code()}]: $errorMsg")
_uiState.value = RegistrationUiState.Error(errorMsg)
}
} catch (e: Exception) {
Log.e(TAG, "Exception during image upload", e)
_uiState.value = RegistrationUiState.Error(e.localizedMessage ?: "Failed to upload image")
}
}
}
fun resetUiState() {
_uiState.value = RegistrationUiState.Idle
}
}

51
app/src/main/java/com/example/dattingapp/registration/TokenManager.kt

@ -0,0 +1,51 @@
package com.example.dattingapp.registration
import android.content.Context
import android.content.SharedPreferences
class TokenManager private constructor(context: Context) {
private val sharedPreferences: SharedPreferences = context.applicationContext.getSharedPreferences(
PREF_NAME,
Context.MODE_PRIVATE
)
fun saveTokens(accessToken: String, refreshToken: String?) {
sharedPreferences.edit().apply {
putString(KEY_ACCESS_TOKEN, accessToken)
putString(KEY_REFRESH_TOKEN, refreshToken)
apply()
}
}
fun getAccessToken(): String? {
return sharedPreferences.getString(KEY_ACCESS_TOKEN, null)
}
fun getRefreshToken(): String? {
return sharedPreferences.getString(KEY_REFRESH_TOKEN, null)
}
fun clearTokens() {
sharedPreferences.edit().apply {
remove(KEY_ACCESS_TOKEN)
remove(KEY_REFRESH_TOKEN)
apply()
}
}
companion object {
private const val PREF_NAME = "user_tokens"
private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_REFRESH_TOKEN = "refresh_token"
@Volatile
private var INSTANCE: TokenManager? = null
fun getInstance(context: Context): TokenManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: TokenManager(context.applicationContext).also { INSTANCE = it }
}
}
}
}

1201
app/src/main/java/com/example/dattingapp/registration/UploadIntroVideoActivity.kt

File diff suppressed because it is too large

172
app/src/main/java/com/example/dattingapp/registration/UploadProfileImageActivity.kt

@ -1,7 +1,9 @@
package com.example.dattingapp.registration package com.example.dattingapp.registration
import android.Manifest
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -11,6 +13,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -43,79 +46,73 @@ import androidx.compose.ui.text.withStyle
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.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import com.example.dattingapp.ui.theme.backgroundGradient
import com.example.dattingapp.ui.theme.buttonGradient
import com.example.dattingapp.ui.theme.circleBorderGradient
import java.io.File import java.io.File
class UploadProfileImageActivity : ComponentActivity() { class UploadProfileImageActivity : ComponentActivity() {
private val TAG = "kk" private val TAG = "UploadProfileImage"
private val viewModel: RegistrationViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Retrieve intent extras passed from previous registration steps
val isPhone = intent.getBooleanExtra("EXTRA_IS_PHONE", false)
val countryCode = intent.getStringExtra("EXTRA_COUNTRY_CODE") ?: ""
val contactValue = intent.getStringExtra("EXTRA_CONTACT_VALUE") ?: ""
val password = intent.getStringExtra("EXTRA_PASSWORD") ?: ""
val name = intent.getStringExtra("EXTRA_NAME") ?: ""
val dob = intent.getStringExtra("EXTRA_DOB") ?: ""
val gender = intent.getStringExtra("EXTRA_GENDER") ?: ""
val interests = intent.getStringArrayListExtra("EXTRA_INTERESTS") ?: arrayListOf()
val vibe = intent.getStringExtra("EXTRA_VIBE") ?: ""
Log.d(TAG, "=== Received Registration Data ===")
Log.d(TAG, "Is Phone: $isPhone")
Log.d(TAG, "Country Code: $countryCode")
Log.d(TAG, "Contact Value: $contactValue")
Log.d(TAG, "Password: $password")
Log.d(TAG, "Name: $name")
Log.d(TAG, "Date of Birth: $dob")
Log.d(TAG, "Gender: $gender")
Log.d(TAG, "Interests: $interests")
Log.d(TAG, "Vibe: $vibe")
Log.d(TAG, "=================================")
setContent { setContent {
MaterialTheme { MaterialTheme {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Observe UI state for upload status
LaunchedEffect(uiState) {
when (uiState) {
is RegistrationUiState.ImageUploadSuccess -> {
Toast.makeText(this@UploadProfileImageActivity, "Image uploaded successfully!", Toast.LENGTH_SHORT).show()
val intent = Intent(this@UploadProfileImageActivity, UploadIntroVideoActivity::class.java)
startActivity(intent)
}
is RegistrationUiState.Error -> {
val message = (uiState as RegistrationUiState.Error).message
Toast.makeText(this@UploadProfileImageActivity, message, Toast.LENGTH_LONG).show()
viewModel.resetUiState()
}
else -> {}
}
}
Box(modifier = Modifier.fillMaxSize()) {
UploadProfileImageScreen( UploadProfileImageScreen(
onBackClick = { finish() }, onBackClick = { finish() },
onContinueClick = { selectedUri -> onContinueClick = { selectedUri ->
if (selectedUri == null) { if (selectedUri == null) {
Toast.makeText(this@UploadProfileImageActivity, "Please select or take a photo first", Toast.LENGTH_SHORT).show() Toast.makeText(this@UploadProfileImageActivity, "Please select or take a photo first", Toast.LENGTH_SHORT).show()
return@UploadProfileImageScreen } else {
viewModel.uploadProfileImage(
context = this@UploadProfileImageActivity,
imageUri = selectedUri
)
} }
val intent = Intent(
this@UploadProfileImageActivity,
UploadIntroVideoActivity::class.java
).apply {
// 1. Pass down all data received from previous steps
putExtra("EXTRA_IS_PHONE", isPhone)
putExtra("EXTRA_COUNTRY_CODE", countryCode)
putExtra("EXTRA_CONTACT_VALUE", contactValue)
putExtra("EXTRA_PASSWORD", password)
putExtra("EXTRA_NAME", name)
putExtra("EXTRA_DOB", dob)
putExtra("EXTRA_GENDER", gender)
putStringArrayListExtra("EXTRA_INTERESTS", interests)
putExtra("EXTRA_VIBE", vibe)
// 2. Pass the selected Profile Image Uri as a String
putExtra("EXTRA_PROFILE_IMAGE_URI", selectedUri.toString())
} }
)
startActivity(intent) if (uiState is RegistrationUiState.Loading) {
} CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color(0xFFC2185B)
) )
} }
} }
} }
} }
}
}
// Helper function to create temporary File & Uri for Camera launcher
fun createImageUri(context: Context): Uri { fun createImageUri(context: Context): Uri {
val tempFile = File.createTempFile("profile_camera_", ".jpg", context.cacheDir).apply { val tempFile = File.createTempFile("profile_camera_", ".jpg", context.cacheDir).apply {
createNewFile() createNewFile()
@ -135,20 +132,17 @@ fun UploadProfileImageScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
// Single state variable to store image source (from either Gallery OR Camera)
var selectedImageUri by remember { mutableStateOf<Uri?>(null) } var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
var tempCameraUri by remember { mutableStateOf<Uri?>(null) } var tempCameraUri by remember { mutableStateOf<Uri?>(null) }
// 1. Gallery Launcher // Gallery Launcher
val galleryLauncher = rememberLauncherForActivityResult( val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent() contract = ActivityResultContracts.GetContent()
) { uri: Uri? -> ) { uri: Uri? ->
if (uri != null) { uri?.let { selectedImageUri = it }
selectedImageUri = uri
}
} }
// 2. Camera Launcher // Camera Launcher
val cameraLauncher = rememberLauncherForActivityResult( val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture() contract = ActivityResultContracts.TakePicture()
) { success: Boolean -> ) { success: Boolean ->
@ -157,30 +151,33 @@ fun UploadProfileImageScreen(
} }
} }
// Design Gradients // Camera Permission Launcher
val backgroundGradient = Brush.verticalGradient( val permissionLauncher = rememberLauncherForActivityResult(
colors = listOf( contract = ActivityResultContracts.RequestPermission()
Color(0xFFFFF5F5), ) { isGranted ->
Color(0xFFFAFAFA), if (isGranted) {
Color(0xFFF3E8FF) val uri = createImageUri(context)
) tempCameraUri = uri
) cameraLauncher.launch(uri)
} else {
Toast.makeText(context, "Camera permission required to take photos", Toast.LENGTH_SHORT).show()
}
}
val buttonGradient = Brush.horizontalGradient( val launchCameraWithPermission = {
colors = listOf( val hasPermission = ContextCompat.checkSelfPermission(
Color(0xFFC2185B), context,
Color(0xFF7B1FA2) Manifest.permission.CAMERA
) ) == PackageManager.PERMISSION_GRANTED
)
val circleBorderGradient = Brush.sweepGradient( if (hasPermission) {
colors = listOf( val uri = createImageUri(context)
Color(0xFF7B1FA2), tempCameraUri = uri
Color(0xFFC2185B), cameraLauncher.launch(uri)
Color(0xFFE91E63), } else {
Color(0xFF7B1FA2) permissionLauncher.launch(Manifest.permission.CAMERA)
) }
) }
Box( Box(
modifier = Modifier modifier = Modifier
@ -207,7 +204,7 @@ fun UploadProfileImageScreen(
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back", contentDescription = "Navigate back",
tint = Color.Black tint = Color.Black
) )
} }
@ -273,7 +270,7 @@ fun UploadProfileImageScreen(
if (selectedImageUri != null) { if (selectedImageUri != null) {
Image( Image(
painter = rememberAsyncImagePainter(selectedImageUri), painter = rememberAsyncImagePainter(selectedImageUri),
contentDescription = "Profile Photo", contentDescription = "Selected Profile Photo",
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(CircleShape), .clip(CircleShape),
@ -282,7 +279,7 @@ fun UploadProfileImageScreen(
} else { } else {
Icon( Icon(
imageVector = Icons.Outlined.AddAPhoto, imageVector = Icons.Outlined.AddAPhoto,
contentDescription = "Camera Icon", contentDescription = "Placeholder photo icon",
tint = Color(0xFFE57373), tint = Color(0xFFE57373),
modifier = Modifier.size(44.dp) modifier = Modifier.size(44.dp)
) )
@ -292,14 +289,14 @@ fun UploadProfileImageScreen(
Spacer(modifier = Modifier.height(36.dp)) Spacer(modifier = Modifier.height(36.dp))
// Upload from gallery Button // Upload from gallery Card
Card( Card(
onClick = { galleryLauncher.launch("image/*") },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)), colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(52.dp) .height(52.dp)
.clickable { galleryLauncher.launch("image/*") }
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -308,7 +305,7 @@ fun UploadProfileImageScreen(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.PhotoLibrary, imageVector = Icons.Outlined.PhotoLibrary,
contentDescription = "Gallery", contentDescription = null,
tint = Color(0xFF673AB7), tint = Color(0xFF673AB7),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@ -324,18 +321,14 @@ fun UploadProfileImageScreen(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// Take a photo Button // Take a photo Card
Card( Card(
onClick = launchCameraWithPermission,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)), colors = CardDefaults.cardColors(containerColor = Color(0xFFEDE7F6)),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(52.dp) .height(52.dp)
.clickable {
val uri = createImageUri(context)
tempCameraUri = uri
cameraLauncher.launch(uri)
}
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -344,7 +337,7 @@ fun UploadProfileImageScreen(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.CameraAlt, imageVector = Icons.Outlined.CameraAlt,
contentDescription = "Camera", contentDescription = null,
tint = Color(0xFF673AB7), tint = Color(0xFF673AB7),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
@ -393,16 +386,15 @@ fun UploadProfileImageScreen(
}, },
fontSize = 12.sp, fontSize = 12.sp,
color = Color.Gray, color = Color.Gray,
modifier = Modifier.clickable { } modifier = Modifier.clickable { /* Handle support click */ }
) )
} }
} }
} }
} }
// 1. Preview default state (No photo selected yet)
@Preview( @Preview(
name = "Upload Photo - Empty State", name = "Upload Profile Image Screen",
showBackground = true, showBackground = true,
showSystemUi = true showSystemUi = true
) )

10
app/src/main/java/com/example/dattingapp/ui/theme/Color.kt

@ -63,6 +63,16 @@ val avatarGradientBorder = Brush.sweepGradient(
) )
) )
val circleBorderGradient = Brush.sweepGradient(
colors = listOf(
Color(0xFF7B1FA2),
Color(0xFFC2185B),
Color(0xFFE91E63),
Color(0xFF7B1FA2)
)
)
// Color definitions matching the design mockup // Color definitions matching the design mockup
val BackgroundGradientTop = Color(0xFF916A8C) val BackgroundGradientTop = Color(0xFF916A8C)

5
app/src/main/res/xml/file_paths.xml

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache_images" path="." />
<external-cache-path name="external_cache_images" path="." />
</paths>
Loading…
Cancel
Save