21 changed files with 1270 additions and 3188 deletions
@ -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 = { _, _ -> } |
|||
) |
|||
} |
|||
} |
|||
@ -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 = {} |
|||
) |
|||
} |
|||
} |
|||
@ -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> |
|||
} |
|||
|
|||
@ -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> |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
} |
|||
@ -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? |
|||
) |
|||
@ -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 |
|||
) |
|||
} |
|||
@ -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? |
|||
) |
|||
|
|||
|
|||
@ -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) |
|||
} |
|||
} |
|||
|
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
|
|||
File diff suppressed because it is too large
@ -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 |
|||
} |
|||
} |
|||
@ -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 } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -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…
Reference in new issue