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