10 changed files with 1418 additions and 96 deletions
@ -0,0 +1,441 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.media.MediaMetadataRetriever |
||||
|
import android.net.Uri |
||||
|
import android.os.Bundle |
||||
|
import android.widget.Toast |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.BackHandler |
||||
|
import androidx.activity.compose.rememberLauncherForActivityResult |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
import androidx.activity.result.PickVisualMediaRequest |
||||
|
import androidx.activity.result.contract.ActivityResultContracts |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.border |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.layout.statusBarsPadding |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||
|
import androidx.compose.foundation.text.BasicTextField |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.filled.Close |
||||
|
import androidx.compose.material.icons.filled.PhotoLibrary |
||||
|
import androidx.compose.material.icons.filled.Send |
||||
|
import androidx.compose.material.icons.filled.SwapHoriz |
||||
|
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.graphics.SolidColor |
||||
|
import androidx.compose.ui.platform.LocalContext |
||||
|
import androidx.compose.ui.text.TextStyle |
||||
|
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.core.content.FileProvider |
||||
|
import androidx.lifecycle.viewmodel.compose.viewModel |
||||
|
import com.example.dattingapp.homes.VideoPlayerItem |
||||
|
import com.example.dattingapp.homes.networkandapi.CreatePostRequest |
||||
|
import com.example.dattingapp.homes.networkandapi.CreatePostUiState |
||||
|
import com.example.dattingapp.homes.networkandapi.CreatePostViewModel |
||||
|
import com.example.dattingapp.homes.networkandapi.PostMediaViewModel |
||||
|
import com.example.dattingapp.ui.theme.DattingAPPTheme |
||||
|
import kotlinx.coroutines.async |
||||
|
import java.io.File |
||||
|
|
||||
|
private const val MAX_REEL_SECONDS = 60 |
||||
|
private const val MAX_CAPTION = 300 |
||||
|
|
||||
|
class CreateReelActivity : ComponentActivity() { |
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
enableEdgeToEdge() |
||||
|
setContent { |
||||
|
DattingAPPTheme { |
||||
|
CreateReelScreen(onClose = { finish() }) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun CreateReelScreen(onClose: () -> Unit) { |
||||
|
val context = LocalContext.current |
||||
|
val createVm: CreatePostViewModel = viewModel() |
||||
|
val mediaVm: PostMediaViewModel = viewModel() |
||||
|
val uiState by createVm.cretePpostUiState.collectAsState() |
||||
|
|
||||
|
var videoUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var caption by remember { mutableStateOf("") } |
||||
|
var cameraUri by remember { mutableStateOf<Uri?>(null) } |
||||
|
var askDiscard by remember { mutableStateOf(false) } |
||||
|
|
||||
|
val isPosting = uiState is CreatePostUiState.Loading || uiState is CreatePostUiState.Success |
||||
|
|
||||
|
// Accept a video only if it is within the length limit |
||||
|
fun accept(uri: Uri) { |
||||
|
if (reelDurationSeconds(context, uri) > MAX_REEL_SECONDS) { |
||||
|
Toast.makeText(context, "Reels can be up to $MAX_REEL_SECONDS seconds", Toast.LENGTH_LONG).show() |
||||
|
} else { |
||||
|
videoUri = uri |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val pickLauncher = rememberLauncherForActivityResult( |
||||
|
ActivityResultContracts.PickVisualMedia() |
||||
|
) { uri -> if (uri != null) accept(uri) } |
||||
|
|
||||
|
val recordLauncher = rememberLauncherForActivityResult( |
||||
|
ActivityResultContracts.CaptureVideo() |
||||
|
) { ok -> if (ok) cameraUri?.let { accept(it) } } |
||||
|
|
||||
|
fun startRecording() { |
||||
|
val file = File.createTempFile("reel_", ".mp4", context.cacheDir) |
||||
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file) |
||||
|
cameraUri = uri |
||||
|
recordLauncher.launch(uri) |
||||
|
} |
||||
|
|
||||
|
// Create the post, then upload the video (same flow as CreateVibePostScreen) |
||||
|
LaunchedEffect(uiState) { |
||||
|
when (val s = uiState) { |
||||
|
is CreatePostUiState.Success -> { |
||||
|
try { |
||||
|
val uri = videoUri |
||||
|
if (uri != null) { |
||||
|
val err = mediaVm.uploadMedia(context, s.postData.id, 0, uri) |
||||
|
if (err != null) Toast.makeText(context, err, Toast.LENGTH_LONG).show() |
||||
|
} |
||||
|
onClose() |
||||
|
} finally { |
||||
|
createVm.reset() |
||||
|
} |
||||
|
} |
||||
|
is CreatePostUiState.Error -> { |
||||
|
Toast.makeText(context, s.message, Toast.LENGTH_LONG).show() |
||||
|
createVm.reset() |
||||
|
} |
||||
|
else -> {} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
BackHandler(enabled = videoUri != null && !isPosting) { askDiscard = true } |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color.Black) |
||||
|
) { |
||||
|
val uri = videoUri |
||||
|
if (uri == null) { |
||||
|
// ---------------- STEP 1: CAPTURE ---------------- |
||||
|
CaptureStep( |
||||
|
onClose = onClose, |
||||
|
onRecord = { startRecording() }, |
||||
|
onUpload = { |
||||
|
pickLauncher.launch( |
||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly) |
||||
|
) |
||||
|
} |
||||
|
) |
||||
|
} else { |
||||
|
// ---------------- STEP 2: PREVIEW + DETAILS ---------------- |
||||
|
VideoPlayerItem(videoUri = uri) // fills the screen |
||||
|
|
||||
|
// Bottom scrim |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.BottomCenter) |
||||
|
.fillMaxWidth() |
||||
|
.height(300.dp) |
||||
|
.background( |
||||
|
Brush.verticalGradient(listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f))) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
// Top bar |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.TopCenter) |
||||
|
.fillMaxWidth() |
||||
|
.statusBarsPadding() |
||||
|
.padding(12.dp), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
RoundIconButton(Icons.Default.Close, "Discard", enabled = !isPosting) { askDiscard = true } |
||||
|
Surface( |
||||
|
onClick = { |
||||
|
if (!isPosting) pickLauncher.launch( |
||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.VideoOnly) |
||||
|
) |
||||
|
}, |
||||
|
shape = RoundedCornerShape(20.dp), |
||||
|
color = Color.Black.copy(alpha = 0.45f) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Icon(Icons.Default.SwapHoriz, null, tint = Color.White, modifier = Modifier.size(16.dp)) |
||||
|
Spacer(Modifier.width(6.dp)) |
||||
|
Text("Change video", color = Color.White, fontSize = 12.sp) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Caption + Share |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.BottomCenter) |
||||
|
.fillMaxWidth() |
||||
|
.navigationBarsPadding() |
||||
|
.imePadding() |
||||
|
.padding(16.dp) |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clip(RoundedCornerShape(16.dp)) |
||||
|
.background(Color.White.copy(alpha = 0.14f)) |
||||
|
.border(1.dp, Color.White.copy(alpha = 0.25f), RoundedCornerShape(16.dp)) |
||||
|
.padding(14.dp) |
||||
|
) { |
||||
|
if (caption.isEmpty()) { |
||||
|
Text("Write a caption...", color = Color.White.copy(alpha = 0.6f), fontSize = 15.sp) |
||||
|
} |
||||
|
BasicTextField( |
||||
|
value = caption, |
||||
|
onValueChange = { if (it.length <= MAX_CAPTION) caption = it }, |
||||
|
enabled = !isPosting, |
||||
|
textStyle = TextStyle(color = Color.White, fontSize = 15.sp), |
||||
|
cursorBrush = SolidColor(Color.White), |
||||
|
maxLines = 4, |
||||
|
modifier = Modifier.fillMaxWidth() |
||||
|
) |
||||
|
} |
||||
|
Text( |
||||
|
text = "${caption.length}/$MAX_CAPTION", |
||||
|
color = Color.White.copy(alpha = 0.6f), |
||||
|
fontSize = 11.sp, |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.End) |
||||
|
.padding(top = 4.dp, bottom = 10.dp) |
||||
|
) |
||||
|
|
||||
|
Button( |
||||
|
enabled = !isPosting, |
||||
|
onClick = { |
||||
|
createVm.createPost( |
||||
|
context, |
||||
|
CreatePostRequest( |
||||
|
content = caption.trim().ifBlank { " " }, |
||||
|
post_type = "video", // change to "reel" if your backend has it |
||||
|
content_format = "plain", |
||||
|
feeling = null, |
||||
|
location = null |
||||
|
) |
||||
|
) |
||||
|
}, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(50.dp), |
||||
|
shape = RoundedCornerShape(25.dp), |
||||
|
colors = ButtonDefaults.buttonColors( |
||||
|
containerColor = Color(0xFFE91E63), |
||||
|
disabledContainerColor = Color(0xFFE91E63).copy(alpha = 0.5f) |
||||
|
) |
||||
|
) { |
||||
|
Text( |
||||
|
if (isPosting) "Posting..." else "Share Reel", |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
fontSize = 16.sp, |
||||
|
color = Color.White |
||||
|
) |
||||
|
Spacer(Modifier.width(8.dp)) |
||||
|
Icon(Icons.Default.Send, null, tint = Color.White, modifier = Modifier.size(18.dp)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ---------------- STEP 3: POSTING PROGRESS ---------------- |
||||
|
if (isPosting) { |
||||
|
LinearProgressIndicator( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.TopCenter) |
||||
|
.fillMaxWidth() |
||||
|
.statusBarsPadding() |
||||
|
.height(3.dp), |
||||
|
color = Color(0xFFE91E63), |
||||
|
trackColor = Color.White.copy(alpha = 0.2f) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (askDiscard) { |
||||
|
AlertDialog( |
||||
|
onDismissRequest = { askDiscard = false }, |
||||
|
title = { Text("Discard reel?") }, |
||||
|
text = { Text("Your video and caption will be lost.") }, |
||||
|
confirmButton = { |
||||
|
TextButton(onClick = { |
||||
|
askDiscard = false |
||||
|
videoUri = null |
||||
|
caption = "" |
||||
|
}) { Text("Discard", color = Color.Red) } |
||||
|
}, |
||||
|
dismissButton = { TextButton(onClick = { askDiscard = false }) { Text("Keep editing") } } |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun CaptureStep( |
||||
|
onClose: () -> Unit, |
||||
|
onRecord: () -> Unit, |
||||
|
onUpload: () -> Unit |
||||
|
) { |
||||
|
Box(modifier = Modifier.fillMaxSize()) { |
||||
|
RoundIconButton( |
||||
|
icon = Icons.Default.Close, |
||||
|
description = "Close", |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.TopStart) |
||||
|
.statusBarsPadding() |
||||
|
.padding(12.dp), |
||||
|
onClick = onClose |
||||
|
) |
||||
|
|
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.align(Alignment.Center) |
||||
|
.padding(horizontal = 32.dp), |
||||
|
horizontalAlignment = Alignment.CenterHorizontally |
||||
|
) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(88.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background( |
||||
|
Brush.linearGradient(listOf(Color(0xFFC2185B), Color(0xFF7B1FA2))) |
||||
|
), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon(Icons.Default.Videocam, null, tint = Color.White, modifier = Modifier.size(40.dp)) |
||||
|
} |
||||
|
Spacer(Modifier.height(20.dp)) |
||||
|
Text("Create a reel", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold) |
||||
|
Spacer(Modifier.height(6.dp)) |
||||
|
Text( |
||||
|
"Share a short video up to $MAX_REEL_SECONDS seconds", |
||||
|
color = Color.White.copy(alpha = 0.7f), |
||||
|
fontSize = 14.sp |
||||
|
) |
||||
|
Spacer(Modifier.height(32.dp)) |
||||
|
|
||||
|
Button( |
||||
|
onClick = onRecord, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp), |
||||
|
shape = RoundedCornerShape(26.dp), |
||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE91E63)) |
||||
|
) { |
||||
|
Icon(Icons.Default.Videocam, null, tint = Color.White) |
||||
|
Spacer(Modifier.width(8.dp)) |
||||
|
Text("Record", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.White) |
||||
|
} |
||||
|
Spacer(Modifier.height(12.dp)) |
||||
|
OutlinedButton( |
||||
|
onClick = onUpload, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(52.dp), |
||||
|
shape = RoundedCornerShape(26.dp), |
||||
|
border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.5f)) |
||||
|
) { |
||||
|
Icon(Icons.Default.PhotoLibrary, null, tint = Color.White) |
||||
|
Spacer(Modifier.width(8.dp)) |
||||
|
Text("Upload from gallery", fontSize = 16.sp, color = Color.White) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun RoundIconButton( |
||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector, |
||||
|
description: String, |
||||
|
modifier: Modifier = Modifier, |
||||
|
enabled: Boolean = true, |
||||
|
onClick: () -> Unit |
||||
|
) { |
||||
|
IconButton( |
||||
|
onClick = onClick, |
||||
|
enabled = enabled, |
||||
|
modifier = modifier |
||||
|
.size(40.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(Color.Black.copy(alpha = 0.45f)) |
||||
|
) { |
||||
|
Icon(icon, description, tint = Color.White, modifier = Modifier.size(20.dp)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun reelDurationSeconds(context: Context, uri: Uri): Long { |
||||
|
return try { |
||||
|
val r = MediaMetadataRetriever() |
||||
|
r.setDataSource(context, uri) |
||||
|
val ms = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L |
||||
|
r.release() |
||||
|
ms / 1000 |
||||
|
} catch (e: Exception) { |
||||
|
0L |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// 1. Preview for the initial Capture step (No video selected yet) |
||||
|
@Preview(showBackground = true, backgroundColor = 0xFF000000) |
||||
|
@Composable |
||||
|
fun CaptureStepPreview() { |
||||
|
DattingAPPTheme { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color.Black) |
||||
|
) { |
||||
|
CaptureStep( |
||||
|
onClose = {}, |
||||
|
onRecord = {}, |
||||
|
onUpload = {} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. Preview for the Capture step using a Dark/Light device frame |
||||
|
@Preview( |
||||
|
name = "Phone Preview", |
||||
|
showBackground = true, |
||||
|
backgroundColor = 0xFF000000 |
||||
|
) |
||||
|
@Composable |
||||
|
fun CreateReelScreenCapturePreview() { |
||||
|
DattingAPPTheme { |
||||
|
// Since the real screen uses ViewModels that require a live backend/context, |
||||
|
// you can preview the CaptureStep layout directly. |
||||
|
CaptureStep( |
||||
|
onClose = {}, |
||||
|
onRecord = {}, |
||||
|
onUpload = {} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,805 @@ |
|||||
|
package com.example.dattingapp |
||||
|
|
||||
|
import android.os.Bundle |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.activity.enableEdgeToEdge |
||||
|
import androidx.compose.foundation.layout.fillMaxSize |
||||
|
import androidx.compose.foundation.layout.padding |
||||
|
import androidx.compose.material3.Scaffold |
||||
|
import androidx.compose.material3.Text |
||||
|
import androidx.compose.runtime.Composable |
||||
|
import com.example.dattingapp.ui.theme.DattingAPPTheme |
||||
|
import android.content.Intent |
||||
|
import androidx.compose.foundation.BorderStroke |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.border |
||||
|
import androidx.compose.foundation.clickable |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
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.Add |
||||
|
import androidx.compose.material.icons.filled.ArrowBack |
||||
|
import androidx.compose.material.icons.filled.Check |
||||
|
import androidx.compose.material.icons.filled.Close |
||||
|
import androidx.compose.material.icons.filled.Favorite |
||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown |
||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp |
||||
|
import androidx.compose.material.icons.filled.Videocam |
||||
|
import androidx.compose.material3.* |
||||
|
import androidx.compose.runtime.* |
||||
|
import androidx.compose.runtime.saveable.rememberSaveable |
||||
|
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.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 coil.compose.AsyncImage |
||||
|
import com.example.dattingapp.R |
||||
|
import com.example.dattingapp.ui.theme.* |
||||
|
import android.widget.Toast |
||||
|
import androidx.lifecycle.viewmodel.compose.viewModel |
||||
|
import com.example.dattingapp.homes.networkandapi.EditProfileUiState |
||||
|
import com.example.dattingapp.homes.networkandapi.EditProfileViewModel |
||||
|
import com.example.dattingapp.homes.networkandapi.ProfileUiState |
||||
|
import com.example.dattingapp.homes.networkandapi.ProfileViewModel |
||||
|
import com.example.dattingapp.homes.profile.ProfileData |
||||
|
import com.example.dattingapp.homes.profile.UpdateProfileRequest |
||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi |
||||
|
class EditMyProfileActivty : ComponentActivity() { |
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
enableEdgeToEdge() |
||||
|
setContent { |
||||
|
DattingAPPTheme { |
||||
|
val context = LocalContext.current |
||||
|
val profileVm: ProfileViewModel = viewModel() |
||||
|
val editVm: EditProfileViewModel = viewModel() |
||||
|
val profileState by profileVm.profileUiState.collectAsState() |
||||
|
val saveState by editVm.state.collectAsState() |
||||
|
|
||||
|
LaunchedEffect(Unit) { profileVm.profileResponse(context) } |
||||
|
|
||||
|
LaunchedEffect(saveState) { |
||||
|
when (val s = saveState) { |
||||
|
is EditProfileUiState.Saved -> { |
||||
|
Toast.makeText(context, "Profile updated", Toast.LENGTH_SHORT).show() |
||||
|
editVm.reset() |
||||
|
finish() |
||||
|
} |
||||
|
is EditProfileUiState.Error -> { |
||||
|
Toast.makeText(context, s.message, Toast.LENGTH_LONG).show() |
||||
|
editVm.reset() |
||||
|
} |
||||
|
else -> {} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
when (val s = profileState) { |
||||
|
is ProfileUiState.Success -> EditProfileScreen( |
||||
|
profile = s.profileData.profile, |
||||
|
isSaving = saveState is EditProfileUiState.Saving, |
||||
|
onBackClick = { finish() }, |
||||
|
onSaveClick = { request -> editVm.save(context, request) } |
||||
|
) |
||||
|
is ProfileUiState.Error -> Box(Modifier.fillMaxSize(), Alignment.Center) { |
||||
|
Text(s.message, color = Color.Red) |
||||
|
} |
||||
|
else -> Box(Modifier.fillMaxSize(), Alignment.Center) { |
||||
|
CircularProgressIndicator() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@OptIn(ExperimentalMaterial3Api::class) |
||||
|
@Composable |
||||
|
fun EditProfileScreen( |
||||
|
profile: ProfileData? = null, |
||||
|
isSaving: Boolean = false, |
||||
|
onBackClick: () -> Unit = {}, |
||||
|
onSaveClick: (UpdateProfileRequest) -> Unit = {} |
||||
|
) { |
||||
|
val context = LocalContext.current |
||||
|
val scrollState = rememberScrollState() |
||||
|
|
||||
|
// Prefilled from the server response |
||||
|
var fullName by remember(profile) { mutableStateOf(profile?.full_name.orEmpty()) } |
||||
|
var birthDate by remember(profile) { mutableStateOf(profile?.birth_date.orEmpty()) } |
||||
|
var gender by remember(profile) { mutableStateOf(profile?.gender.orEmpty()) } |
||||
|
var city by remember(profile) { mutableStateOf(profile?.city.orEmpty()) } |
||||
|
var country by remember(profile) { mutableStateOf(profile?.country.orEmpty()) } |
||||
|
var datingIntent by remember(profile) { mutableStateOf(profile?.relationship_goal.orEmpty()) } |
||||
|
var bioText by remember(profile) { mutableStateOf(profile?.bio.orEmpty()) } |
||||
|
val interests = remember(profile) { |
||||
|
mutableStateListOf<String>().apply { addAll(profile?.interests.orEmpty()) } |
||||
|
} |
||||
|
var newInterest by remember { mutableStateOf("") } |
||||
|
|
||||
|
var isBeliefsExpanded by rememberSaveable { mutableStateOf(true) } |
||||
|
var selectedBelief by rememberSaveable { mutableStateOf("Spiritual / Agnostic") } |
||||
|
|
||||
|
val submit: () -> Unit = { |
||||
|
val date = birthDate.trim() |
||||
|
val dateOk = date.isEmpty() || try { |
||||
|
java.time.LocalDate.parse(date); true |
||||
|
} catch (e: Exception) { false } |
||||
|
|
||||
|
if (!dateOk) { |
||||
|
Toast.makeText(context, "Birthday must look like 1998-08-12", Toast.LENGTH_LONG).show() |
||||
|
} else { |
||||
|
onSaveClick( |
||||
|
UpdateProfileRequest( |
||||
|
full_name = fullName.trim().ifBlank { null }, |
||||
|
gender = gender.trim().ifBlank { null }, |
||||
|
birth_date = date.ifBlank { null }, |
||||
|
city = city.trim().ifBlank { null }, |
||||
|
country = country.trim().ifBlank { null }, |
||||
|
relationship_goal = datingIntent.ifBlank { null }, |
||||
|
bio = bioText.trim().ifBlank { null }, |
||||
|
interests = interests.toList() |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Scaffold( |
||||
|
topBar = { |
||||
|
TopAppBar( |
||||
|
title = { Text("Edit Profile", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = textDark) }, |
||||
|
navigationIcon = { |
||||
|
IconButton(onClick = onBackClick) { |
||||
|
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back", tint = textDark) |
||||
|
} |
||||
|
}, |
||||
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.White) |
||||
|
) |
||||
|
}, |
||||
|
bottomBar = { |
||||
|
Surface( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
color = Color.White, |
||||
|
shadowElevation = 8.dp |
||||
|
) { |
||||
|
Box(modifier = Modifier.padding(16.dp)) { |
||||
|
Button( |
||||
|
onClick = submit, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(50.dp), |
||||
|
shape = RoundedCornerShape(25.dp), |
||||
|
colors = ButtonDefaults.buttonColors(containerColor = BrandMagenta) |
||||
|
) { |
||||
|
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = Color.White) |
||||
|
Spacer(modifier = Modifier.width(8.dp)) |
||||
|
Text(text = "Save Changes", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
containerColor = Color.White |
||||
|
) { paddingValues -> |
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.padding(paddingValues) |
||||
|
.verticalScroll(scrollState) |
||||
|
.padding(16.dp), |
||||
|
verticalArrangement = Arrangement.spacedBy(16.dp) |
||||
|
) { |
||||
|
// ==================================================== |
||||
|
// 1. PROFILE COMPLETION CARD |
||||
|
// ==================================================== |
||||
|
Card( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFF9FA)), |
||||
|
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp)// color = BrandMagenta.copy(alpha = 0.3f)) |
||||
|
) { |
||||
|
Column(modifier = Modifier.padding(16.dp)) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically,){//} horizontalAlignment = Arrangement.spacedBy(6.dp)) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(10.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(BrandMagenta) |
||||
|
) |
||||
|
Text(text = "Profile 92% Complete", fontWeight = FontWeight.Bold, fontSize = 14.sp, color = textDark) |
||||
|
} |
||||
|
Surface( |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
color = BrandMagenta.copy(alpha = 0.1f) |
||||
|
) { |
||||
|
Text( |
||||
|
text = "+5% to 100%", |
||||
|
fontSize = 10.sp, |
||||
|
fontWeight = FontWeight.Bold, |
||||
|
color = BrandMagenta, |
||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
Text( |
||||
|
text = "Add a 30s voice intro to unlock the Super Listener verified badge!", |
||||
|
fontSize = 12.sp, |
||||
|
color = TextMuted |
||||
|
) |
||||
|
Spacer(modifier = Modifier.height(10.dp)) |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically){// horizontalAlignment = Arrangement.spacedBy(4.dp)) { |
||||
|
Text(text = "🎙️", fontSize = 12.sp) |
||||
|
Text(text = "Voice prompt pending", fontSize = 12.sp, fontWeight = FontWeight.Medium, color = textDark) |
||||
|
} |
||||
|
TextButton(onClick = {}) { |
||||
|
Text(text = "Record now ›", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = BrandMagenta) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 2. PROFILE PHOTO SECTION |
||||
|
// ==================================================== |
||||
|
Column(modifier = Modifier.fillMaxWidth()) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Text(text = "Profile Photos", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = "4 max", fontSize = 12.sp, color = BrandMagenta, fontWeight = FontWeight.Bold) |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
Text(text = "Verified is matches-up your main avatar", fontSize = 11.sp, color = TextMuted) |
||||
|
Spacer(modifier = Modifier.height(10.dp)) |
||||
|
|
||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(90.dp, 120.dp) |
||||
|
.clip(RoundedCornerShape(12.dp)) |
||||
|
) { |
||||
|
AsyncImage( |
||||
|
model = "https://picsum.photos/300/400", |
||||
|
contentDescription = "Main Photo", |
||||
|
contentScale = ContentScale.Crop, |
||||
|
modifier = Modifier.fillMaxSize() |
||||
|
) |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(20.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(BrandMagenta) |
||||
|
.align(Alignment.BottomEnd) |
||||
|
.padding(4.dp), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(12.dp)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Column( |
||||
|
modifier = Modifier |
||||
|
.weight(1f) |
||||
|
.height(120.dp), |
||||
|
verticalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
OutlinedButton( |
||||
|
onClick = {}, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.weight(1f), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = BrandMagenta) |
||||
|
) { |
||||
|
Icon(imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp)) |
||||
|
Spacer(modifier = Modifier.width(6.dp)) |
||||
|
Text(text = "Upload New Photo", fontSize = 12.sp, fontWeight = FontWeight.Bold) |
||||
|
} |
||||
|
OutlinedButton( |
||||
|
onClick = {}, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.weight(1f), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = textDark) |
||||
|
) { |
||||
|
Text(text = "AI Camera", fontSize = 12.sp, fontWeight = FontWeight.Bold) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
Text(text = "⚠️ Cropping only protocol to keep rules & connections.", fontSize = 10.sp, color = Color(0xFFD32F2F)) |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 3. VIDEO INTRO CARD |
||||
|
// ==================================================== |
||||
|
Card( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9F7FB)), |
||||
|
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(50.dp, 60.dp) |
||||
|
.clip(RoundedCornerShape(8.dp)) |
||||
|
.background(Color.Black) |
||||
|
) { |
||||
|
AsyncImage( |
||||
|
model = "https://picsum.photos/200/300", |
||||
|
contentDescription = null, |
||||
|
contentScale = ContentScale.Crop, |
||||
|
modifier = Modifier.fillMaxSize() |
||||
|
) |
||||
|
} |
||||
|
Column { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { |
||||
|
Text(text = "Video Intro", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = "💖", fontSize = 11.sp) |
||||
|
} |
||||
|
Text(text = "0:45sec • 456 total views", fontSize = 11.sp, color = TextMuted) |
||||
|
} |
||||
|
} |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
||||
|
Surface( |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
color = Color(0xFFEFE8F7) |
||||
|
) { |
||||
|
Text(text = "Replace", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = BrandMagenta, modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)) |
||||
|
} |
||||
|
IconButton(onClick = {}, modifier = Modifier.size(28.dp)) { |
||||
|
Icon(imageVector = Icons.Default.Close, contentDescription = "Delete", tint = TextMuted, modifier = Modifier.size(16.dp)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 4. BASIC INFORMATION FIELDS |
||||
|
// ==================================================== |
||||
|
Column( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
verticalArrangement = Arrangement.spacedBy(10.dp) |
||||
|
) { |
||||
|
OutlinedTextField( |
||||
|
value = fullName, |
||||
|
onValueChange = { if (it.length <= 30) fullName = it }, |
||||
|
label = { Text("Display Name") }, |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
focusedBorderColor = BrandMagenta, unfocusedBorderColor = Color(0xFFE0E0E0) |
||||
|
), |
||||
|
trailingIcon = { Text("${fullName.length} / 30", fontSize = 10.sp, color = TextMuted) } |
||||
|
) |
||||
|
OutlinedTextField( |
||||
|
value = birthDate, |
||||
|
onValueChange = { birthDate = it }, |
||||
|
label = { Text("Birthday (YYYY-MM-DD)") }, |
||||
|
singleLine = true, |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
focusedBorderColor = BrandMagenta, unfocusedBorderColor = Color(0xFFE0E0E0) |
||||
|
) |
||||
|
) |
||||
|
OutlinedTextField( |
||||
|
value = gender, |
||||
|
onValueChange = { gender = it }, |
||||
|
label = { Text("Gender") }, |
||||
|
singleLine = true, |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
focusedBorderColor = BrandMagenta, unfocusedBorderColor = Color(0xFFE0E0E0) |
||||
|
) |
||||
|
) |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
||||
|
OutlinedTextField( |
||||
|
value = city, onValueChange = { city = it }, |
||||
|
label = { Text("City") }, singleLine = true, |
||||
|
modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
focusedBorderColor = BrandMagenta, unfocusedBorderColor = Color(0xFFE0E0E0) |
||||
|
) |
||||
|
) |
||||
|
OutlinedTextField( |
||||
|
value = country, onValueChange = { country = it }, |
||||
|
label = { Text("Country") }, singleLine = true, |
||||
|
modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
focusedBorderColor = BrandMagenta, unfocusedBorderColor = Color(0xFFE0E0E0) |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 5. DATING INTENT |
||||
|
// ==================================================== |
||||
|
Column( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
verticalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Text(text = "Dating Intent", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = "Visible on profile", fontSize = 11.sp, color = TextMuted) |
||||
|
} |
||||
|
|
||||
|
DatingIntentOption( |
||||
|
title = "Long-term relationship", |
||||
|
subtitle = "Looking for a true connection or serious dating", |
||||
|
isSelected = true, |
||||
|
onClick = { datingIntent = "Long-term relationship" } |
||||
|
) |
||||
|
DatingIntentOption( |
||||
|
title = "Casual dates", |
||||
|
subtitle = "Fun dinners, socializing, and great talks", |
||||
|
isSelected = false, |
||||
|
onClick = { datingIntent = "Casual dates" } |
||||
|
) |
||||
|
DatingIntentOption( |
||||
|
title = "New Friends & Vibes", |
||||
|
subtitle = "Expanding my creative circle in the city", |
||||
|
isSelected = false, |
||||
|
onClick = { datingIntent = "New Friends & Vibes" } |
||||
|
) |
||||
|
DatingIntentOption( |
||||
|
title = "Marriage / Life Partner", |
||||
|
subtitle = "Looking for true degree and building a life together", |
||||
|
isSelected = false, |
||||
|
onClick = { datingIntent = "Marriage / Life Partner" } |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 6. ABOUT BIO SECTION |
||||
|
// ==================================================== |
||||
|
Column(modifier = Modifier.fillMaxWidth()) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Text(text = "About Alex (Bio)", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = "124 / 500", fontSize = 10.sp, color = TextMuted) |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
OutlinedTextField( |
||||
|
value = bioText, |
||||
|
onValueChange = { bioText = it }, |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.height(110.dp), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = OutlinedTextFieldDefaults.colors( |
||||
|
focusedBorderColor = BrandMagenta, |
||||
|
unfocusedBorderColor = Color(0xFFE0E0E0) |
||||
|
) |
||||
|
) |
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { |
||||
|
Text(text = "💡", fontSize = 12.sp) |
||||
|
Text(text = "Tip: Add a question to get more replies!", fontSize = 11.sp, color = TextMuted) |
||||
|
} |
||||
|
TextButton(onClick = {}) { |
||||
|
Text(text = "Suggest AI • Polish", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = BrandMagenta) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 7. PASSIONS & INTERESTS |
||||
|
// ==================================================== |
||||
|
Column(modifier = Modifier.fillMaxWidth()) { |
||||
|
Row( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Text(text = "Passions & Interests", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = "8 selected", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = BrandMagenta) |
||||
|
} |
||||
|
Spacer(modifier = Modifier.height(2.dp)) |
||||
|
Text(text = "Tap to remove or add more tailored tags", fontSize = 11.sp, color = TextMuted) |
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
|
||||
|
val interestList = listOf( |
||||
|
"📷 Photography", "🐶 Dogs", "☕ Coffee Culture", "✈️ Travel", |
||||
|
"🎨 Art & Design", "🎵 Live Concerts", "🍷 Natural Wine", "🍳 Cooking" |
||||
|
) |
||||
|
|
||||
|
OptFlowRowEdit(interests = interestList) |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
OutlinedButton( |
||||
|
onClick = {}, |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = BrandMagenta), |
||||
|
modifier = Modifier.height(36.dp) |
||||
|
) { |
||||
|
Icon(imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp)) |
||||
|
Spacer(modifier = Modifier.width(4.dp)) |
||||
|
Text(text = "Add More Interests", fontSize = 11.sp, fontWeight = FontWeight.Bold) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// 8. KEY STATS & LIFESTYLE |
||||
|
// ==================================================== |
||||
|
Column( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
verticalArrangement = Arrangement.spacedBy(10.dp) |
||||
|
) { |
||||
|
Text(text = "Key Stats & Lifestyle", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
|
||||
|
StatEditRow(icon = "💼", title = "Work / Occupation", value = "Creative Director at Studio Drift") |
||||
|
StatEditRow(icon = "🎓", title = "Education", value = "NYU Gallatin School") |
||||
|
StatEditRow(icon = "📏", title = "Height", value = "5'9\" (175 cm)") |
||||
|
|
||||
|
// Beliefs Expandable Section |
||||
|
Card( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFFAFAFA)), |
||||
|
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp) |
||||
|
) { |
||||
|
Column(modifier = Modifier.padding(12.dp)) { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clickable { isBeliefsExpanded = !isBeliefsExpanded }, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween, |
||||
|
verticalAlignment = Alignment.CenterVertically |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { |
||||
|
Text(text = "🏛️", fontSize = 16.sp) |
||||
|
Column { |
||||
|
Text(text = "Beliefs / Values", fontSize = 10.sp, color = TextMuted) |
||||
|
Text(text = selectedBelief, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
} |
||||
|
} |
||||
|
Icon( |
||||
|
imageVector = if (isBeliefsExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, |
||||
|
contentDescription = "Toggle", |
||||
|
tint = TextMuted |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (isBeliefsExpanded) { |
||||
|
Spacer(modifier = Modifier.height(10.dp)) |
||||
|
Divider(color = Color(0xFFEEEEEE), thickness = 0.5.dp) |
||||
|
Spacer(modifier = Modifier.height(8.dp)) |
||||
|
Text(text = "SELECT BELIEFS", fontSize = 9.sp, fontWeight = FontWeight.Bold, color = TextMuted) |
||||
|
Spacer(modifier = Modifier.height(6.dp)) |
||||
|
|
||||
|
BeliefOptionItem(title = "Muslim", desc =" Islam", isSelected = selectedBelief == "Muslim", onClick = { selectedBelief = "Muslim" }) |
||||
|
BeliefOptionItem(title = "Christian", desc = "Christian", isSelected = selectedBelief == "Christian", onClick = { selectedBelief = "Christian" }) |
||||
|
BeliefOptionItem(title = "Hindu", desc = "Hinduism", isSelected = selectedBelief == "Hindu", onClick = { selectedBelief = "Hindu" }) |
||||
|
BeliefOptionItem(title = "Buddhist", desc = "Buddhism", isSelected = selectedBelief == "Buddhist", onClick = { selectedBelief = "Buddhist" }) |
||||
|
BeliefOptionItem(title = "Spiritual / Agnostic", desc = "Open to universal healing & values", isSelected = selectedBelief == "Spiritual / Agnostic", onClick = { selectedBelief = "Spiritual / Agnostic" }) |
||||
|
BeliefOptionItem(title = "Other", desc = "Specify or leave open", isSelected = selectedBelief == "Other", onClick = { selectedBelief = "Other" }) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Lifestyle Grid Edit Tiles |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { |
||||
|
LifestyleEditTile(icon = "🚭", label = "Smoking", detail = "Non-smoker", modifier = Modifier.weight(1f)) |
||||
|
LifestyleEditTile(icon = "🍷", label = "Drinking", detail = "Socially", modifier = Modifier.weight(1f)) |
||||
|
} |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { |
||||
|
LifestyleEditTile(icon = "🐾", label = "Pets", detail = "Golden Retriever", modifier = Modifier.weight(1f)) |
||||
|
LifestyleEditTile(icon = "🏃", label = "Exercise", detail = "Active 4x a week", modifier = Modifier.weight(1f)) |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(4.dp)) |
||||
|
OutlinedButton( |
||||
|
onClick = {}, |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = BrandMagenta), |
||||
|
modifier = Modifier.height(36.dp) |
||||
|
) { |
||||
|
Icon(imageVector = Icons.Default.Add, contentDescription = null, modifier = Modifier.size(14.dp)) |
||||
|
Spacer(modifier = Modifier.width(4.dp)) |
||||
|
Text(text = "Add More Lifestyle Details (Hobby, Diet, Sleep)", fontSize = 11.sp, fontWeight = FontWeight.Bold) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Spacer(modifier = Modifier.height(40.dp)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ==================================================== |
||||
|
// SUB-COMPONENTS FOR EDIT PROFILE SCREEN |
||||
|
// ==================================================== |
||||
|
|
||||
|
@Composable |
||||
|
private fun DatingIntentOption(title: String, subtitle: String, isSelected: Boolean, onClick: () -> Unit) { |
||||
|
Card( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clickable { onClick() }, |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = if (isSelected) Color(0xFFFFF9FA) else Color(0xFFFAFAFA)), |
||||
|
border = CardDefaults.outlinedCardBorder().copy(width = if (isSelected) 1.dp else 0.5.dp)// color = if (isSelected) BrandMagenta else Color(0xFFE0E0E0)) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { |
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.size(30.dp) |
||||
|
.clip(CircleShape) |
||||
|
.background(if (isSelected) BrandMagenta.copy(alpha = 0.15f) else Color(0xFFEFEFEF)), |
||||
|
contentAlignment = Alignment.Center |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = Icons.Default.Favorite, |
||||
|
contentDescription = null, |
||||
|
tint = if (isSelected) BrandMagenta else TextMuted, |
||||
|
modifier = Modifier.size(14.dp) |
||||
|
) |
||||
|
} |
||||
|
Column { |
||||
|
Text(text = title, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = subtitle, fontSize = 11.sp, color = TextMuted) |
||||
|
} |
||||
|
} |
||||
|
RadioButton(selected = isSelected, onClick = onClick, colors = RadioButtonDefaults.colors(selectedColor = BrandMagenta)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun StatEditRow(icon: String, title: String, value: String) { |
||||
|
Card( |
||||
|
modifier = Modifier.fillMaxWidth(), |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFFAFAFA)), |
||||
|
border = CardDefaults.outlinedCardBorder().copy(width = 0.5.dp) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.padding(12.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { |
||||
|
Text(text = icon, fontSize = 16.sp) |
||||
|
Column { |
||||
|
Text(text = title, fontSize = 10.sp, color = TextMuted) |
||||
|
Text(text = value, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
} |
||||
|
} |
||||
|
Text(text = "›", fontSize = 16.sp, color = TextMuted) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun BeliefOptionItem(title: String, desc: String, isSelected: Boolean, onClick: () -> Unit) { |
||||
|
Row( |
||||
|
modifier = Modifier |
||||
|
.fillMaxWidth() |
||||
|
.clickable { onClick() } |
||||
|
.padding(vertical = 6.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
||||
|
RadioButton(selected = isSelected, onClick = onClick, colors = RadioButtonDefaults.colors(selectedColor = BrandMagenta)) |
||||
|
Column { |
||||
|
Text(text = title, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
Text(text = desc, fontSize = 10.sp, color = TextMuted) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun LifestyleEditTile(icon: String, label: String, detail: String, modifier: Modifier = Modifier) { |
||||
|
Surface( |
||||
|
modifier = modifier, |
||||
|
shape = RoundedCornerShape(12.dp), |
||||
|
color = Color(0xFFFAFAFA), |
||||
|
border = BorderStroke(0.5.dp, Color(0xFFE0E0E0)) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(10.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.SpaceBetween |
||||
|
) { |
||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
||||
|
Text(text = icon, fontSize = 16.sp) |
||||
|
Column { |
||||
|
Text(text = label, fontSize = 9.sp, color = TextMuted) |
||||
|
Text(text = detail, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = textDark) |
||||
|
} |
||||
|
} |
||||
|
Text(text = "✎", fontSize = 11.sp, color = TextMuted) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
private fun OptFlowRowEdit(interests: List<String>) { |
||||
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { |
||||
|
val chunked = interests.chunked(3) |
||||
|
chunked.forEach { rowItems -> |
||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { |
||||
|
rowItems.forEach { item -> |
||||
|
Surface( |
||||
|
shape = RoundedCornerShape(16.dp), |
||||
|
color = Color(0xFFF4F4F4), |
||||
|
border = BorderStroke(0.5.dp, Color(0xFFDDDDDD)) |
||||
|
) { |
||||
|
Row( |
||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp) |
||||
|
) { |
||||
|
Text(text = item, fontSize = 11.sp, fontWeight = FontWeight.Medium, color = textDark) |
||||
|
Icon(imageVector = Icons.Default.Close, contentDescription = "Remove", tint = TextMuted, modifier = Modifier.size(12.dp)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Preview(showBackground = true, showSystemUi = true) |
||||
|
@Composable |
||||
|
fun EditProfileScreenPreview() { |
||||
|
DattingAPPTheme { |
||||
|
EditProfileScreen() |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue