Compare commits
2 Commits
75e6691d1e
...
a19e1891a7
| Author | SHA1 | Date |
|---|---|---|
|
|
a19e1891a7 | 4 weeks ago |
|
|
5b6ce5f751 | 4 weeks ago |
12 changed files with 910 additions and 2028 deletions
File diff suppressed because it is too large
@ -1,938 +0,0 @@ |
|||
package com.example.dattingapp |
|||
|
|||
import android.content.Intent |
|||
import android.os.Bundle |
|||
import androidx.activity.ComponentActivity |
|||
import androidx.activity.compose.setContent |
|||
import androidx.activity.enableEdgeToEdge |
|||
import androidx.compose.animation.* |
|||
import androidx.compose.animation.core.tween |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.clickable |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.text.KeyboardOptions |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.runtime.snapshots.SnapshotStateList |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.graphics.vector.ImageVector |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.input.KeyboardType |
|||
import androidx.compose.ui.text.input.PasswordVisualTransformation |
|||
import androidx.compose.ui.tooling.preview.Preview |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import java.text.SimpleDateFormat |
|||
import java.util.* |
|||
import androidx.compose.foundation.BorderStroke |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material.icons.Icons |
|||
import androidx.compose.material.icons.outlined.Explore |
|||
import androidx.compose.material.icons.outlined.FavoriteBorder |
|||
import androidx.compose.material.icons.outlined.Group |
|||
import androidx.compose.material.icons.outlined.LocalBar |
|||
import androidx.compose.ui.platform.LocalContext |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import com.example.dattingapp.registration.UploadProfileImageActivity |
|||
|
|||
// Global Email Validation Regex |
|||
val EMAIL_PATTERN_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") |
|||
|
|||
data class VibeOption( |
|||
val title: String, |
|||
val subtitle: String, |
|||
val icon: ImageVector, |
|||
val iconBgColor: Color, |
|||
val iconTintColor: Color |
|||
) |
|||
class RegistrationScreen : ComponentActivity() { |
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
enableEdgeToEdge() |
|||
super.onCreate(savedInstanceState) |
|||
setContent { |
|||
MaterialTheme { |
|||
FullscreenRegistrationWizard() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun FullscreenRegistrationWizard() { |
|||
// Active step state |
|||
var currentStep by remember { mutableIntStateOf(1) } |
|||
val totalSteps = 7 |
|||
|
|||
// Form Field States |
|||
var isPhoneSelected by remember { mutableStateOf(false) } |
|||
var countryCode by remember { mutableStateOf("+880") } |
|||
var contactInput by remember { mutableStateOf("") } |
|||
var contactError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
var passwordInput by remember { mutableStateOf("") } |
|||
var confirmPasswordInput by remember { mutableStateOf("") } |
|||
var passwordError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
var nameInput by remember { mutableStateOf("") } |
|||
var nameError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
var dobInput by remember { mutableStateOf("") } |
|||
var dobError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
var genderInput by remember { mutableStateOf("") } |
|||
var genderError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
val selectedInterests = remember { mutableStateListOf<String>() } |
|||
var interestsError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
|
|||
var vibeInput by remember { mutableStateOf("") } |
|||
var vibeError by remember { mutableStateOf<String?>(null) } |
|||
|
|||
// Inside your FullscreenRegistrationWizard composable: |
|||
val context = LocalContext.current |
|||
|
|||
// Validation & Navigation |
|||
fun validateAndProceed() { |
|||
when (currentStep) { |
|||
|
|||
|
|||
|
|||
1 -> { |
|||
if (nameInput.trim().isBlank()) { |
|||
nameError = "Please enter your name" |
|||
} else { |
|||
nameError = null |
|||
currentStep = 2 |
|||
} |
|||
|
|||
} |
|||
|
|||
2 -> { |
|||
if (isPhoneSelected) { |
|||
when { |
|||
contactInput.isBlank() -> contactError = "Please enter a phone number" |
|||
contactInput.length < 7 || contactInput.length > 15 -> contactError = "Enter 7-15 digits" |
|||
else -> { contactError = null; currentStep = 2 } |
|||
} |
|||
} else { |
|||
val trimmedEmail = contactInput.trim() |
|||
when { |
|||
trimmedEmail.isBlank() -> contactError = "Please enter an email address" |
|||
!EMAIL_PATTERN_REGEX.matches(trimmedEmail) -> contactError = "Invalid email format" |
|||
else -> { contactError = null; currentStep = 4 } |
|||
} |
|||
} |
|||
|
|||
} |
|||
3 -> { |
|||
|
|||
when { |
|||
passwordInput.length < 6 -> passwordError = "Password must be at least 6 characters" |
|||
passwordInput != confirmPasswordInput -> passwordError = "Passwords do not match" |
|||
else -> { passwordError = null; currentStep = 3 } |
|||
} |
|||
} |
|||
|
|||
4 -> { |
|||
if (dobInput.isBlank()) { |
|||
dobError = "Please select your date of birth" |
|||
} else { |
|||
dobError = null |
|||
currentStep = 5 |
|||
} |
|||
} |
|||
5 -> { |
|||
if (genderInput.isBlank()) { |
|||
genderError = "Please select your gender" |
|||
} else { |
|||
genderError = null |
|||
currentStep = 6 |
|||
} |
|||
} |
|||
6 -> { |
|||
if (selectedInterests.size < 2) { |
|||
interestsError = "Please select at least 2 interests" |
|||
} else { |
|||
interestsError = null |
|||
// Submit registration payload |
|||
currentStep = 7 |
|||
} |
|||
} |
|||
|
|||
|
|||
7 -> { |
|||
if (vibeInput.isBlank()) { |
|||
vibeError = "Please select your vibe to continue" |
|||
} else { |
|||
vibeError = null |
|||
|
|||
// Launch UploadImageActivity with all collected data |
|||
val intent = Intent(context, UploadProfileImageActivity::class.java).apply { |
|||
putExtra("EXTRA_IS_PHONE", isPhoneSelected) |
|||
putExtra("EXTRA_COUNTRY_CODE", countryCode) |
|||
putExtra("EXTRA_CONTACT_VALUE", contactInput) |
|||
putExtra("EXTRA_PASSWORD", passwordInput) |
|||
putExtra("EXTRA_NAME", nameInput) |
|||
putExtra("EXTRA_DOB", dobInput) |
|||
putExtra("EXTRA_GENDER", genderInput) |
|||
putExtra("EXTRA_INTERESTS", ArrayList(selectedInterests)) |
|||
putExtra("EXTRA_VIBE", vibeInput) |
|||
} |
|||
context.startActivity(intent) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF8F9FA)) |
|||
.systemBarsPadding() |
|||
) { |
|||
Column(modifier = Modifier.fillMaxSize()) { |
|||
// TOP BAR & PROGRESS |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 20.dp, vertical = 16.dp) |
|||
) { |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.SpaceBetween |
|||
) { |
|||
if (currentStep > 1) { |
|||
TextButton( |
|||
onClick = { currentStep-- }, |
|||
contentPadding = PaddingValues(0.dp) |
|||
) { |
|||
Text("Back", color = Color.Gray, fontSize = 16.sp) |
|||
} |
|||
} else { |
|||
Spacer(modifier = Modifier.width(48.dp)) |
|||
} |
|||
|
|||
Text( |
|||
text = "Step $currentStep of $totalSteps", |
|||
fontWeight = FontWeight.Bold, |
|||
fontSize = 16.sp, |
|||
color = Color.Black |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.width(48.dp)) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(12.dp)) |
|||
|
|||
LinearProgressIndicator( |
|||
progress = { currentStep / totalSteps.toFloat() }, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(6.dp), |
|||
color = Color.Black, |
|||
trackColor = Color(0xFFE0E0E0) |
|||
) |
|||
} |
|||
|
|||
// SLIDING CONTENT CONTAINER |
|||
Box( |
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 24.dp), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
AnimatedContent( |
|||
targetState = currentStep, |
|||
transitionSpec = { |
|||
if (targetState > initialState) { |
|||
slideIntoContainer( |
|||
AnimatedContentTransitionScope.SlideDirection.Left, |
|||
animationSpec = tween(400) |
|||
) + fadeIn(animationSpec = tween(400)) togetherWith |
|||
slideOutOfContainer( |
|||
AnimatedContentTransitionScope.SlideDirection.Left, |
|||
animationSpec = tween(400) |
|||
) + fadeOut(animationSpec = tween(400)) |
|||
} else { |
|||
slideIntoContainer( |
|||
AnimatedContentTransitionScope.SlideDirection.Right, |
|||
animationSpec = tween(400) |
|||
) + fadeIn(animationSpec = tween(400)) togetherWith |
|||
slideOutOfContainer( |
|||
AnimatedContentTransitionScope.SlideDirection.Right, |
|||
animationSpec = tween(400) |
|||
) + fadeOut(animationSpec = tween(400)) |
|||
} |
|||
}, |
|||
label = "WizardSlideAnimation" |
|||
) { step -> |
|||
Column( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
when (step) { |
|||
|
|||
1 -> NameInputWidget( |
|||
name = nameInput, |
|||
onNameChange = { nameInput = it; nameError = null }, |
|||
errorMessage = nameError |
|||
) |
|||
2 -> ContactInputWidget( |
|||
isPhoneSelected = isPhoneSelected, |
|||
onModeChange = { isPhoneSelected = it; contactError = null }, |
|||
countryCode = countryCode, |
|||
onCountryCodeChange = { countryCode = it }, |
|||
contactValue = contactInput, |
|||
onContactChange = { contactInput = it; contactError = null }, |
|||
errorMessage = contactError |
|||
) |
|||
3 -> PasswordInputWidget( |
|||
password = passwordInput, |
|||
onPasswordChange = { passwordInput = it; passwordError = null }, |
|||
confirmPassword = confirmPasswordInput, |
|||
onConfirmPasswordChange = { confirmPasswordInput = it; passwordError = null }, |
|||
errorMessage = passwordError |
|||
) |
|||
|
|||
4 -> DobInputWidget( |
|||
dob = dobInput, |
|||
onDobChange = { dobInput = it; dobError = null }, |
|||
errorMessage = dobError |
|||
) |
|||
5 -> GenderInputWidget( |
|||
selectedGender = genderInput, |
|||
onGenderSelect = { genderInput = it; genderError = null }, |
|||
errorMessage = genderError |
|||
) |
|||
6 -> InterestsInputWidget( |
|||
selectedInterests = selectedInterests, |
|||
errorMessage = interestsError, |
|||
onSelectionChanged = { interestsError = null } |
|||
) |
|||
7 -> VibeCheckWidget( |
|||
selectedVibe = vibeInput, |
|||
onVibeSelected = { |
|||
vibeInput = it |
|||
vibeError = null |
|||
}, |
|||
errorMessage = vibeError |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// BOTTOM ACTION BUTTON |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(20.dp) |
|||
) { |
|||
Button( |
|||
onClick = { validateAndProceed() }, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(54.dp), |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = ButtonDefaults.buttonColors(containerColor = Color.Black) |
|||
) { |
|||
Text( |
|||
text = if (currentStep == totalSteps) "Complete Registration" else "Continue", |
|||
color = Color.White, |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ==================== STEP WIDGET COMPOSABLES ==================== |
|||
|
|||
@Composable |
|||
fun ContactInputWidget( |
|||
isPhoneSelected: Boolean, |
|||
onModeChange: (Boolean) -> Unit, |
|||
countryCode: String, |
|||
onCountryCodeChange: (String) -> Unit, |
|||
contactValue: String, |
|||
onContactChange: (String) -> Unit, |
|||
errorMessage: String? |
|||
) { |
|||
Column( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|||
) { |
|||
Text( |
|||
text = "ENTER YOUR CONTACT INFORMATION", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Gray |
|||
) |
|||
|
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(48.dp) |
|||
.clip(RoundedCornerShape(24.dp)) |
|||
.background(Color(0xFFEFEFEF)) |
|||
.padding(4.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.fillMaxHeight() |
|||
.clip(RoundedCornerShape(20.dp)) |
|||
.background(if (isPhoneSelected) Color.Black else Color.Transparent) |
|||
.clickable { onModeChange(true) }, |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "Phone", |
|||
fontWeight = FontWeight.Bold, |
|||
fontSize = 14.sp, |
|||
color = if (isPhoneSelected) Color.White else Color.DarkGray |
|||
) |
|||
} |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.fillMaxHeight() |
|||
.clip(RoundedCornerShape(20.dp)) |
|||
.background(if (!isPhoneSelected) Color.Black else Color.Transparent) |
|||
.clickable { onModeChange(false) }, |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "Email", |
|||
fontWeight = FontWeight.Bold, |
|||
fontSize = 14.sp, |
|||
color = if (!isPhoneSelected) Color.White else Color.DarkGray |
|||
) |
|||
} |
|||
} |
|||
|
|||
if (isPhoneSelected) { |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
OutlinedTextField( |
|||
value = countryCode, |
|||
onValueChange = { if (it.length <= 4) onCountryCodeChange(it) }, |
|||
placeholder = { Text("+880") }, |
|||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), |
|||
singleLine = true, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.width(90.dp), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
|
|||
OutlinedTextField( |
|||
value = contactValue, |
|||
onValueChange = { if (it.length <= 15) onContactChange(it) }, |
|||
placeholder = { Text("Phone number") }, |
|||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), |
|||
singleLine = true, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.weight(1f), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
} |
|||
} else { |
|||
OutlinedTextField( |
|||
value = contactValue, |
|||
onValueChange = onContactChange, |
|||
placeholder = { Text("Enter email address") }, |
|||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), |
|||
singleLine = true, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (errorMessage != null) { |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
modifier = Modifier.padding(start = 4.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun PasswordInputWidget( |
|||
password: String, |
|||
onPasswordChange: (String) -> Unit, |
|||
confirmPassword: String, |
|||
onConfirmPasswordChange: (String) -> Unit, |
|||
errorMessage: String? |
|||
) { |
|||
Column( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|||
) { |
|||
Text( |
|||
text = "CREATE A PASSWORD", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Gray |
|||
) |
|||
|
|||
OutlinedTextField( |
|||
value = password, |
|||
onValueChange = onPasswordChange, |
|||
placeholder = { Text("Enter password") }, |
|||
visualTransformation = PasswordVisualTransformation(), |
|||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), |
|||
singleLine = true, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
|
|||
OutlinedTextField( |
|||
value = confirmPassword, |
|||
onValueChange = onConfirmPasswordChange, |
|||
placeholder = { Text("Confirm password") }, |
|||
visualTransformation = PasswordVisualTransformation(), |
|||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), |
|||
singleLine = true, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
|
|||
if (errorMessage != null) { |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
modifier = Modifier.padding(start = 4.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun NameInputWidget( |
|||
name: String, |
|||
onNameChange: (String) -> Unit, |
|||
errorMessage: String? |
|||
) { |
|||
Column( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|||
) { |
|||
Text( |
|||
text = "WHAT IS YOUR NAME?", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Gray |
|||
) |
|||
|
|||
OutlinedTextField( |
|||
value = name, |
|||
onValueChange = onNameChange, |
|||
placeholder = { Text("Enter your full name") }, |
|||
singleLine = true, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
|
|||
if (errorMessage != null) { |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
modifier = Modifier.padding(start = 4.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@OptIn(ExperimentalMaterial3Api::class) |
|||
@Composable |
|||
fun DobInputWidget( |
|||
dob: String, |
|||
onDobChange: (String) -> Unit, |
|||
errorMessage: String? |
|||
) { |
|||
var showDatePicker by remember { mutableStateOf(false) } |
|||
val datePickerState = rememberDatePickerState() |
|||
|
|||
if (showDatePicker) { |
|||
DatePickerDialog( |
|||
onDismissRequest = { showDatePicker = false }, |
|||
confirmButton = { |
|||
TextButton(onClick = { |
|||
datePickerState.selectedDateMillis?.let { millis -> |
|||
val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) |
|||
onDobChange(formatter.format(Date(millis))) |
|||
} |
|||
showDatePicker = false |
|||
}) { |
|||
Text("OK") |
|||
} |
|||
}, |
|||
dismissButton = { |
|||
TextButton(onClick = { showDatePicker = false }) { |
|||
Text("Cancel") |
|||
} |
|||
} |
|||
) { |
|||
DatePicker(state = datePickerState) |
|||
} |
|||
} |
|||
|
|||
Column( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|||
) { |
|||
Text( |
|||
text = "YOUR DATE OF BIRTH", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Gray |
|||
) |
|||
|
|||
Box(modifier = Modifier.fillMaxWidth()) { |
|||
OutlinedTextField( |
|||
value = dob, |
|||
onValueChange = {}, |
|||
readOnly = true, |
|||
placeholder = { Text("DD/MM/YYYY") }, |
|||
isError = errorMessage != null, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
) |
|||
) |
|||
Box( |
|||
modifier = Modifier |
|||
.matchParentSize() |
|||
.clickable { showDatePicker = true } |
|||
) |
|||
} |
|||
|
|||
if (errorMessage != null) { |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
modifier = Modifier.padding(start = 4.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun GenderInputWidget( |
|||
selectedGender: String, |
|||
onGenderSelect: (String) -> Unit, |
|||
errorMessage: String? |
|||
) { |
|||
val options = listOf("Male", "Female", "Other") |
|||
|
|||
Column( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|||
) { |
|||
Text( |
|||
text = "SELECT YOUR GENDER", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Gray |
|||
) |
|||
|
|||
options.forEach { gender -> |
|||
val isSelected = selectedGender == gender |
|||
Card( |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = CardDefaults.cardColors( |
|||
containerColor = if (isSelected) Color.Black else Color.White |
|||
), |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clickable { onGenderSelect(gender) } |
|||
) { |
|||
Box( |
|||
modifier = Modifier.padding(16.dp), |
|||
contentAlignment = Alignment.CenterStart |
|||
) { |
|||
Text( |
|||
text = gender, |
|||
color = if (isSelected) Color.White else Color.Black, |
|||
fontWeight = FontWeight.SemiBold, |
|||
fontSize = 15.sp |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (errorMessage != null) { |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
modifier = Modifier.padding(start = 4.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@OptIn(ExperimentalLayoutApi::class) |
|||
@Composable |
|||
fun InterestsInputWidget( |
|||
selectedInterests: SnapshotStateList<String>, |
|||
errorMessage: String? = null, |
|||
onSelectionChanged: () -> Unit = {} |
|||
) { |
|||
val allInterests = listOf("Photography", "Dogs", "Travel", "Music", "Gaming", "Fitness", "Cooking") |
|||
|
|||
Card( |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = CardDefaults.cardColors(containerColor = Color.White), |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
Column(modifier = Modifier.padding(16.dp)) { |
|||
Text( |
|||
text = "INTERESTS (SELECT AT LEAST 2)", |
|||
fontSize = 11.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Gray |
|||
) |
|||
Spacer(modifier = Modifier.height(12.dp)) |
|||
|
|||
FlowRow( |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp), |
|||
verticalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
allInterests.forEach { interest -> |
|||
val isSelected = selectedInterests.contains(interest) |
|||
FilterChip( |
|||
selected = isSelected, |
|||
onClick = { |
|||
if (isSelected) selectedInterests.remove(interest) |
|||
else selectedInterests.add(interest) |
|||
onSelectionChanged() |
|||
}, |
|||
label = { Text(interest) } |
|||
) |
|||
} |
|||
} |
|||
|
|||
if (errorMessage != null) { |
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
@Composable |
|||
fun VibeCheckWidget( |
|||
selectedVibe: String, |
|||
onVibeSelected: (String) -> Unit, |
|||
errorMessage: String? = null |
|||
) { |
|||
val options = listOf( |
|||
VibeOption("Serious Relationship", "Looking for the one", Icons.Outlined.FavoriteBorder, Color(0xFFFFEAEA), Color(0xFFE91E63)), |
|||
VibeOption("Casual Dates", "Drinks & good times", Icons.Outlined.LocalBar, Color(0xFFEDE7F6), Color(0xFF673AB7)), |
|||
VibeOption("New Friends", "Expand my circle", Icons.Outlined.Group, Color(0xFFFFECB3), Color(0xFFFF8F00)), |
|||
VibeOption("Marraige", "Seeing where things go", Icons.Outlined.Explore, Color(0xFFE0F2F1), Color(0xFF00897B)) |
|||
) |
|||
|
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 8.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
// App Title / Brand Header |
|||
Text( |
|||
text = "VibeCheck", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color(0xFFC2185B) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
|
|||
// Three Indicator Bars |
|||
Row( |
|||
horizontalArrangement = Arrangement.spacedBy(6.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Box(modifier = Modifier.width(20.dp).height(4.dp).background(Color(0xFFE0E0E0), CircleShape)) |
|||
Box(modifier = Modifier.width(20.dp).height(4.dp).background(Color(0xFFC2185B), CircleShape)) |
|||
Box(modifier = Modifier.width(20.dp).height(4.dp).background(Color(0xFFE0E0E0), CircleShape)) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(20.dp)) |
|||
|
|||
// Title & Description |
|||
Text( |
|||
text = "What's your vibe?", |
|||
fontSize = 20.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Black |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
|
|||
Text( |
|||
text = "Select your main goal right now to help us find the right connections.", |
|||
fontSize = 13.sp, |
|||
color = Color.Gray, |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.padding(horizontal = 16.dp) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// Vibe Cards List |
|||
Column( |
|||
verticalArrangement = Arrangement.spacedBy(12.dp), |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
options.forEach { option -> |
|||
val isSelected = selectedVibe == option.title |
|||
|
|||
Card( |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = CardDefaults.cardColors(containerColor = Color.White), |
|||
border = BorderStroke( |
|||
width = if (isSelected) 2.dp else 1.dp, |
|||
color = if (isSelected) Color(0xFFC2185B) else Color(0xFFEEEEEE) |
|||
), |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clickable { onVibeSelected(option.title) } |
|||
) { |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(16.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
// Round Icon Container |
|||
Box( |
|||
modifier = Modifier |
|||
.size(42.dp) |
|||
.background(option.iconBgColor, CircleShape), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Icon( |
|||
imageVector = option.icon, |
|||
contentDescription = option.title, |
|||
tint = option.iconTintColor, |
|||
modifier = Modifier.size(22.dp) |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.width(16.dp)) |
|||
|
|||
// Text Labels |
|||
Column { |
|||
Text( |
|||
text = option.title, |
|||
fontSize = 15.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Black |
|||
) |
|||
Text( |
|||
text = option.subtitle, |
|||
fontSize = 12.sp, |
|||
color = Color.Gray |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (errorMessage != null) { |
|||
Spacer(modifier = Modifier.height(12.dp)) |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
Text( |
|||
text = "You can always change this later in your profile.", |
|||
fontSize = 11.sp, |
|||
color = Color.Gray |
|||
) |
|||
} |
|||
} |
|||
|
|||
@Preview( |
|||
name = "Light Mode - Full Wizard", |
|||
showBackground = true, |
|||
showSystemUi = true |
|||
) |
|||
@Composable |
|||
fun FullscreenRegistrationWizardPreview() { |
|||
Surface(modifier = Modifier.fillMaxSize()) { |
|||
FullscreenRegistrationWizard() |
|||
} |
|||
} |
|||
@ -0,0 +1,692 @@ |
|||
package com.example.dattingapp.homes |
|||
|
|||
|
|||
import android.net.Uri |
|||
import androidx.activity.compose.rememberLauncherForActivityResult |
|||
import androidx.activity.result.PickVisualMediaRequest |
|||
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.lazy.LazyRow |
|||
import androidx.compose.foundation.lazy.grid.GridCells |
|||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid |
|||
import androidx.compose.foundation.lazy.itemsIndexed |
|||
import androidx.compose.foundation.rememberScrollState |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.foundation.verticalScroll |
|||
import androidx.compose.material.icons.Icons |
|||
import androidx.compose.material.icons.filled.* |
|||
import androidx.compose.material.icons.outlined.* |
|||
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.graphics.vector.ImageVector |
|||
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.withStyle |
|||
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.homes.networkandapi.VibeFeeling |
|||
import com.example.dattingapp.homes.networkandapi.vibeFeelingsList |
|||
import java.io.File |
|||
import com.example.dattingapp.ui.theme.LightPinkBg |
|||
import com.example.dattingapp.ui.theme.LightPurpleBg |
|||
import com.example.dattingapp.ui.theme.DarkOverlay |
|||
import com.example.dattingapp.ui.theme.LightBlueBg |
|||
|
|||
import androidx.compose.foundation.lazy.grid.items |
|||
|
|||
// Sealed class representing selectable media types (Photo or Video) |
|||
sealed class MediaContentItem(val uri: Uri) { |
|||
class Photo(uri: Uri) : MediaContentItem(uri) |
|||
class Video(uri: Uri) : MediaContentItem(uri) |
|||
} |
|||
|
|||
@OptIn(ExperimentalMaterial3Api::class) |
|||
@Composable |
|||
fun CreateVibePostScreen( |
|||
onCloseClick: () -> Unit = {}, |
|||
onShareClick: () -> Unit = {} |
|||
) { |
|||
val context = LocalContext.current |
|||
var textState by remember { mutableStateOf("") } |
|||
val maxChar = 500 |
|||
|
|||
// Holds selected media items (photos, videos, or both) |
|||
val selectedMediaList = remember { mutableStateListOf<MediaContentItem>() } |
|||
|
|||
var tempCameraImageUri by remember { mutableStateOf<Uri?>(null) } |
|||
var tempCameraVideoUri by remember { mutableStateOf<Uri?>(null) } |
|||
var showImagePickerDialog by remember { mutableStateOf(false) } |
|||
|
|||
var showFeelingSheet by remember { mutableStateOf(false) } |
|||
var selectedFeeling by remember { mutableStateOf<VibeFeeling?>(null) } |
|||
|
|||
// Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously) |
|||
val galleryLauncher = rememberLauncherForActivityResult( |
|||
contract = ActivityResultContracts.PickMultipleVisualMedia() |
|||
) { uris: List<Uri> -> |
|||
uris.forEach { uri -> |
|||
val mimeType = context.contentResolver.getType(uri) |
|||
if (mimeType?.startsWith("video") == true) { |
|||
selectedMediaList.add(MediaContentItem.Video(uri)) |
|||
} else { |
|||
selectedMediaList.add(MediaContentItem.Photo(uri)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Temporary file helper for Camera captures |
|||
fun createTempFileUri(extension: String, prefix: String): Uri { |
|||
val tempFile = File.createTempFile(prefix, extension, context.cacheDir).apply { |
|||
createNewFile() |
|||
deleteOnExit() |
|||
} |
|||
return FileProvider.getUriForFile( |
|||
context, |
|||
"${context.packageName}.provider", |
|||
tempFile |
|||
) |
|||
} |
|||
|
|||
// Camera Launchers for taking photos or recording videos |
|||
val cameraPhotoLauncher = rememberLauncherForActivityResult( |
|||
contract = ActivityResultContracts.TakePicture() |
|||
) { success: Boolean -> |
|||
if (success && tempCameraImageUri != null) { |
|||
selectedMediaList.add(MediaContentItem.Photo(tempCameraImageUri!!)) |
|||
} |
|||
} |
|||
|
|||
val cameraVideoLauncher = rememberLauncherForActivityResult( |
|||
contract = ActivityResultContracts.CaptureVideo() |
|||
) { success: Boolean -> |
|||
if (success && tempCameraVideoUri != null) { |
|||
selectedMediaList.add(MediaContentItem.Video(tempCameraVideoUri!!)) |
|||
} |
|||
} |
|||
|
|||
Scaffold( |
|||
bottomBar = { |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.background(Color.White) |
|||
.padding(horizontal = 16.dp, vertical = 12.dp), |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.SpaceBetween |
|||
) { |
|||
// 1. Scrollable option chips on the left |
|||
LazyRow( |
|||
modifier = Modifier.weight(1f), |
|||
horizontalArrangement = Arrangement.spacedBy(10.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
item { |
|||
VibeOptionChip( |
|||
icon = Icons.Outlined.PhotoLibrary, |
|||
backgroundColor = LightPinkBg, |
|||
contentColor = Color(0xFFE91E63), |
|||
onClick = { showImagePickerDialog = true } |
|||
) |
|||
} |
|||
item { |
|||
VibeOptionChip( |
|||
icon = Icons.Outlined.MyLocation, |
|||
backgroundColor = LightBlueBg, |
|||
contentColor = Color(0xFF1E88E5) |
|||
) |
|||
} |
|||
item { |
|||
VibeOptionChip( |
|||
icon = Icons.Outlined.Mood, |
|||
backgroundColor = LightPinkBg, |
|||
contentColor = Color(0xFFE91E63), |
|||
onClick = { showFeelingSheet = true } // Opens bottom sheet |
|||
) |
|||
} |
|||
item { |
|||
VibeOptionChip( |
|||
icon = Icons.Outlined.MusicNote, |
|||
backgroundColor = LightPurpleBg, |
|||
contentColor = Color(0xFF7B1FA2) |
|||
) |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
|
|||
// 2. Share button on the right |
|||
Button( |
|||
onClick = onShareClick, |
|||
modifier = Modifier |
|||
.wrapContentWidth() |
|||
.height(40.dp), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
|||
contentPadding = PaddingValues(0.dp) |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.background( |
|||
brush = Brush.horizontalGradient( |
|||
colors = listOf(Color(0xFFC2185B), Color(0xFF7B1FA2)) |
|||
), |
|||
shape = RoundedCornerShape(12.dp) |
|||
) |
|||
.padding(horizontal = 12.dp, vertical = 8.dp), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Row(verticalAlignment = Alignment.CenterVertically) { |
|||
Text( |
|||
text = "Share", |
|||
color = Color.White, |
|||
fontWeight = FontWeight.Bold, |
|||
fontSize = 14.sp |
|||
) |
|||
Spacer(modifier = Modifier.width(6.dp)) |
|||
Icon( |
|||
imageVector = Icons.Default.Send, |
|||
contentDescription = null, |
|||
tint = Color.White, |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
containerColor = Color.White |
|||
) { innerPadding -> |
|||
|
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(innerPadding) |
|||
.padding(horizontal = 16.dp) |
|||
.verticalScroll(rememberScrollState()) |
|||
|
|||
) { |
|||
// User Profile Header |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
Box(modifier = Modifier.size(48.dp)) { |
|||
Box( |
|||
modifier = Modifier |
|||
.size(48.dp) |
|||
.clip(CircleShape) |
|||
.background(Color.Gray) |
|||
) |
|||
Box( |
|||
modifier = Modifier |
|||
.size(12.dp) |
|||
.clip(CircleShape) |
|||
.background(Color(0xFFE91E63)) |
|||
.align(Alignment.BottomEnd) |
|||
.border(2.dp, Color.White, CircleShape) |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.width(12.dp)) |
|||
|
|||
Column(modifier = Modifier.weight(1f)) { |
|||
Row(verticalAlignment = Alignment.CenterVertically) { |
|||
Text( |
|||
text = buildAnnotatedString { |
|||
// "Alex Rivera " - Bold |
|||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|||
append("Alex Rivera ") |
|||
} |
|||
|
|||
// "- feeling " - Normal weight (also fixed "felling" to "feeling") |
|||
selectedFeeling?.let { feeling -> |
|||
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) { |
|||
append(" — feeling ") |
|||
} |
|||
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|||
// Change feeling.name to feeling.label (or feeling.title depending on your data class) |
|||
append("${feeling.name} ${feeling.emoji}") |
|||
} |
|||
} |
|||
}, |
|||
fontSize = 16.sp |
|||
) |
|||
Spacer(modifier = Modifier.width(4.dp)) |
|||
|
|||
//this is for vibe |
|||
|
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(4.dp)) |
|||
|
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.SpaceBetween |
|||
) { |
|||
Row(verticalAlignment = Alignment.CenterVertically) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.LocationOn, |
|||
contentDescription = null, |
|||
tint = Color(0xFFE91E63), |
|||
modifier = Modifier.size(20.dp) |
|||
) |
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
Text( |
|||
text = "Blue Bottle Coffee, Soho NYC", |
|||
fontWeight = FontWeight.Medium, |
|||
fontSize = 13.sp |
|||
) |
|||
} |
|||
Icon( |
|||
imageVector = Icons.Default.Close, |
|||
contentDescription = "Remove location", |
|||
tint = Color.Gray, |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// Text Area |
|||
TextField( |
|||
value = textState, |
|||
onValueChange = { if (it.length <= maxChar) textState = it }, |
|||
placeholder = { |
|||
Text( |
|||
"What's your vibe today? Share a moment, date idea, or story...", |
|||
color = Color.Gray, |
|||
fontSize = 15.sp |
|||
) |
|||
}, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
colors = TextFieldDefaults.colors( |
|||
focusedContainerColor = Color.Transparent, |
|||
unfocusedContainerColor = Color.Transparent, |
|||
focusedIndicatorColor = Color.Transparent, |
|||
unfocusedIndicatorColor = Color.Transparent |
|||
) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
|
|||
// Formatting & Character Counter Bar |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { |
|||
Text("B", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.Gray) |
|||
Text("I", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.Gray) |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// Horizontal Scrollable Media Row (Contains Photos and Videos) |
|||
if (selectedMediaList.isNotEmpty()) { |
|||
LazyRow( |
|||
horizontalArrangement = Arrangement.spacedBy(12.dp), |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
itemsIndexed(selectedMediaList) { index, item -> |
|||
Card( |
|||
shape = RoundedCornerShape(20.dp), |
|||
modifier = Modifier |
|||
.width(280.dp) |
|||
.height(220.dp) |
|||
) { |
|||
Box(modifier = Modifier.fillMaxSize()) { |
|||
when (item) { |
|||
is MediaContentItem.Photo -> { |
|||
Image( |
|||
painter = rememberAsyncImagePainter(item.uri), |
|||
contentDescription = "Photo Preview", |
|||
contentScale = ContentScale.Crop, |
|||
modifier = Modifier.fillMaxSize() |
|||
) |
|||
} |
|||
is MediaContentItem.Video -> { |
|||
VideoPlayerItem(videoUri = item.uri) |
|||
} |
|||
} |
|||
|
|||
// Delete Card Button |
|||
Box( |
|||
modifier = Modifier |
|||
.align(Alignment.TopEnd) |
|||
.padding(10.dp) |
|||
) { |
|||
OverlayIconButton( |
|||
icon = Icons.Default.Close, |
|||
onClick = { selectedMediaList.removeAt(index) } |
|||
) |
|||
} |
|||
|
|||
// Media Type Badge Overlay (Top Left) |
|||
Surface( |
|||
color = DarkOverlay, |
|||
shape = RoundedCornerShape(8.dp), |
|||
modifier = Modifier |
|||
.align(Alignment.TopStart) |
|||
.padding(10.dp) |
|||
) { |
|||
Row( |
|||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Icon( |
|||
imageVector = if (item is MediaContentItem.Video) Icons.Default.Videocam else Icons.Default.Image, |
|||
contentDescription = null, |
|||
tint = Color.White, |
|||
modifier = Modifier.size(14.dp) |
|||
) |
|||
Spacer(modifier = Modifier.width(4.dp)) |
|||
Text( |
|||
text = if (item is MediaContentItem.Video) "Video" else "Photo ${index + 1}", |
|||
color = Color.White, |
|||
fontSize = 11.sp |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
} |
|||
|
|||
// Location Tag Bar |
|||
|
|||
|
|||
Spacer(modifier = Modifier.height(20.dp)) |
|||
|
|||
|
|||
Spacer(modifier = Modifier.height(10.dp)) |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
} |
|||
|
|||
// Camera / Gallery Choice Bottom Sheet Dialog |
|||
if (showImagePickerDialog) { |
|||
ModalBottomSheet( |
|||
onDismissRequest = { showImagePickerDialog = false }, |
|||
containerColor = Color.White |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(24.dp) |
|||
) { |
|||
Text( |
|||
text = "Add Photos or Videos", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
modifier = Modifier.padding(bottom = 16.dp) |
|||
) |
|||
|
|||
// Pick from Gallery (Photo + Video combined option) |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clickable { |
|||
showImagePickerDialog = false |
|||
galleryLauncher.launch( |
|||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) |
|||
) |
|||
} |
|||
.padding(vertical = 12.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.PhotoLibrary, |
|||
contentDescription = "Gallery", |
|||
tint = Color(0xFF7B1FA2), |
|||
modifier = Modifier.size(24.dp) |
|||
) |
|||
Spacer(modifier = Modifier.width(16.dp)) |
|||
Text(text = "Choose Photos & Videos from Gallery", fontSize = 16.sp) |
|||
} |
|||
|
|||
// Take Photo |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clickable { |
|||
showImagePickerDialog = false |
|||
val uri = createTempFileUri(".jpg", "camera_photo_") |
|||
tempCameraImageUri = uri |
|||
cameraPhotoLauncher.launch(uri) |
|||
} |
|||
.padding(vertical = 12.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.CameraAlt, |
|||
contentDescription = "Take Photo", |
|||
tint = Color(0xFFE91E63), |
|||
modifier = Modifier.size(24.dp) |
|||
) |
|||
Spacer(modifier = Modifier.width(16.dp)) |
|||
Text(text = "Take Photo", fontSize = 16.sp) |
|||
} |
|||
|
|||
// Record Video |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clickable { |
|||
showImagePickerDialog = false |
|||
val uri = createTempFileUri(".mp4", "camera_video_") |
|||
tempCameraVideoUri = uri |
|||
cameraVideoLauncher.launch(uri) |
|||
} |
|||
.padding(vertical = 12.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Videocam, |
|||
contentDescription = "Record Video", |
|||
tint = Color(0xFF1E88E5), |
|||
modifier = Modifier.size(24.dp) |
|||
) |
|||
Spacer(modifier = Modifier.width(16.dp)) |
|||
Text(text = "Record Video", fontSize = 16.sp) |
|||
} |
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (showFeelingSheet) { |
|||
FeelingPickerBottomSheet( |
|||
onDismiss = { showFeelingSheet = false }, |
|||
onFeelingSelected = { feeling -> |
|||
selectedFeeling = feeling |
|||
} |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun VideoPlayerItem(videoUri: Uri) { |
|||
val context = LocalContext.current |
|||
val exoPlayer = remember(videoUri) { |
|||
ExoPlayer.Builder(context).build().apply { |
|||
setMediaItem(MediaItem.fromUri(videoUri)) |
|||
prepare() |
|||
playWhenReady = false |
|||
} |
|||
} |
|||
|
|||
DisposableEffect(Unit) { |
|||
onDispose { |
|||
exoPlayer.release() |
|||
} |
|||
} |
|||
|
|||
AndroidView( |
|||
factory = { ctx -> |
|||
PlayerView(ctx).apply { |
|||
player = exoPlayer |
|||
useController = true |
|||
} |
|||
}, |
|||
modifier = Modifier.fillMaxSize() |
|||
) |
|||
} |
|||
|
|||
@Composable |
|||
fun OverlayIconButton(icon: ImageVector, onClick: () -> Unit = {}) { |
|||
Box( |
|||
modifier = Modifier |
|||
.size(32.dp) |
|||
.background(DarkOverlay, CircleShape) |
|||
.clickable { onClick() }, |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Icon( |
|||
imageVector = icon, |
|||
contentDescription = null, |
|||
tint = Color.White, |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun VibeOptionChip( |
|||
icon: ImageVector, |
|||
backgroundColor: Color, |
|||
contentColor: Color, |
|||
onClick: () -> Unit = {} |
|||
) { |
|||
Surface( |
|||
shape = RoundedCornerShape(12.dp), |
|||
color = backgroundColor, |
|||
modifier = Modifier |
|||
.height(40.dp) |
|||
.clickable { onClick() } |
|||
) { |
|||
Row( |
|||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Icon( |
|||
imageVector = icon, |
|||
contentDescription = null, |
|||
tint = contentColor, |
|||
modifier = Modifier.size(18.dp) |
|||
) |
|||
Spacer(modifier = Modifier.width(6.dp)) |
|||
|
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
@OptIn(ExperimentalMaterial3Api::class) |
|||
@Composable |
|||
fun FeelingPickerBottomSheet( |
|||
onDismiss: () -> Unit, |
|||
onFeelingSelected: (VibeFeeling) -> Unit |
|||
) { |
|||
ModalBottomSheet( |
|||
onDismissRequest = onDismiss, |
|||
containerColor = Color.White |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 16.dp, vertical = 8.dp) |
|||
) { |
|||
Text( |
|||
text = "How are you feeling?", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.Black, |
|||
modifier = Modifier.padding(bottom = 16.dp) |
|||
) |
|||
|
|||
LazyVerticalGrid( |
|||
columns = GridCells.Fixed(2), |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp), |
|||
verticalArrangement = Arrangement.spacedBy(8.dp), |
|||
modifier = Modifier.padding(bottom = 24.dp) |
|||
) { |
|||
items(vibeFeelingsList) { feeling -> |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clip(RoundedCornerShape(12.dp)) |
|||
.background(Color(0xFFF8F9FA)) |
|||
.clickable { |
|||
onFeelingSelected(feeling) |
|||
onDismiss() |
|||
} |
|||
.padding(horizontal = 12.dp, vertical = 10.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Text( |
|||
text = feeling.emoji, |
|||
fontSize = 20.sp |
|||
) |
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
Text( |
|||
text = feeling.name, |
|||
fontSize = 13.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF212121), |
|||
maxLines = 1 |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Preview( |
|||
name = "Create Vibe Post Screen", |
|||
showBackground = true, |
|||
showSystemUi = true |
|||
) |
|||
@Composable |
|||
fun CreateVibePostScreenPreview() { |
|||
MaterialTheme { |
|||
CreateVibePostScreen( |
|||
onCloseClick = {}, |
|||
onShareClick = {} |
|||
) |
|||
} |
|||
} |
|||
Loading…
Reference in new issue