Browse Source

create post update

master
osman160128 1 week ago
parent
commit
91e1138bb9
  1. 564
      app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt
  2. 4
      app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt
  3. 8
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
  4. 82
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
  5. 5
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeNetwokModule.kt
  6. 141
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt

564
app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt

@ -2,7 +2,13 @@ package com.example.dattingapp.homes
import com.example.dattingapp.homes.networkandapi.Location
import com.example.dattingapp.homes.networkandapi.CreatePostRequest
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@ -14,6 +20,7 @@ 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.grid.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@ -33,6 +40,7 @@ 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.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
@ -40,35 +48,23 @@ 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.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.viewmodel.compose.viewModel
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.CreatePostUiState
import com.example.dattingapp.homes.networkandapi.CreatePostViewModel
import com.example.dattingapp.homes.networkandapi.PostMediaViewModel
import com.example.dattingapp.homes.networkandapi.VibeFeeling
import com.example.dattingapp.homes.networkandapi.vibeFeelingsList
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 android.Manifest
import android.content.pm.PackageManager
import android.location.LocationManager
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.compose.foundation.lazy.grid.items
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.dattingapp.homes.networkandapi.CreatePostViewModel
import com.example.dattingapp.homes.networkandapi.CreatePostUiState
import android.util.Log
import android.content.Context
import androidx.compose.ui.text.TextStyle
import com.example.dattingapp.homes.networkandapi.PostMediaViewModel
import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.LightBlueBg
import com.example.dattingapp.ui.theme.LightPinkBg
import com.example.dattingapp.ui.theme.LightPurpleBg
import java.io.File
// Sealed class representing selectable media types (Photo or Video)
@ -77,21 +73,17 @@ sealed class MediaContentItem(val uri: Uri) {
class Video(uri: Uri) : MediaContentItem(uri)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateVibePostScreen(
onCloseClick: () -> Unit = {},
onShareClick: () -> Unit = {}
onShareClick: () -> Unit = {} // called ONLY after the post is created
) {
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>() }
// Determine the post type dynamically based on attached media
val post_type = remember(selectedMediaList) {
when {
selectedMediaList.any { it is MediaContentItem.Video } -> "video"
@ -107,10 +99,60 @@ fun CreateVibePostScreen(
var showFeelingSheet by remember { mutableStateOf(false) }
var selectedFeeling by remember { mutableStateOf<VibeFeeling?>(null) }
// ---------------- ViewModels (moved to the top of the screen) ----------------
val createPostViewModel: CreatePostViewModel = viewModel()
val postMediaViewModel: PostMediaViewModel = viewModel()
val uiState by createPostViewModel.cretePpostUiState.collectAsState()
// True from the Share tap until the whole flow is done
val isPosting = uiState is CreatePostUiState.Loading || uiState is CreatePostUiState.Success
// Location states (declared before the effect below, which clears them)
var latitude by remember { mutableStateOf<Double?>(null) }
var longitude by remember { mutableStateOf<Double?>(null) }
var showLocationTag by remember { mutableStateOf(false) }
// React to the create-post result
LaunchedEffect(uiState) {
when (val state = uiState) {
is CreatePostUiState.Success -> {
try {
val postId = state.postData.id
Log.d("CreatePostDebug", "Post created with ID: $postId")
val errors = mutableListOf<String>()
selectedMediaList.toList().forEachIndexed { index, item ->
val err = postMediaViewModel.uploadMedia(context, postId, index, item.uri)
if (err != null) errors.add("File ${index + 1}: $err")
}
if (errors.isNotEmpty()) {
Toast.makeText(context, errors.first(), Toast.LENGTH_LONG).show()
}
textState = ""
selectedMediaList.clear()
selectedFeeling = null
latitude = null
longitude = null
showLocationTag = false
onShareClick()
} finally {
// always runs, even if this effect is cancelled, so the button never stays stuck
createPostViewModel.reset()
}
}
is CreatePostUiState.Error -> {
Log.e("CreatePostDebug", "Error: ${state.message}")
Toast.makeText(context, state.message, Toast.LENGTH_LONG).show()
createPostViewModel.reset()
}
// Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously)
else -> {}
}
}
// ---------------- Pickers ----------------
val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia()
) { uris: List<Uri> ->
@ -124,20 +166,14 @@ fun CreateVibePostScreen(
}
}
// 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
)
return FileProvider.getUriForFile(context, "${context.packageName}.provider", tempFile)
}
// Camera Launchers for taking photos or recording videos
val cameraPhotoLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture()
) { success: Boolean ->
@ -154,25 +190,17 @@ fun CreateVibePostScreen(
}
}
// Location States
var latitude by remember { mutableStateOf<Double?>(null) }
var longitude by remember { mutableStateOf<Double?>(null) }
var showLocationTag by remember { mutableStateOf(false) }
// Native Location Manager (Works in China without Google Play Services)
// ---------------- Location ----------------
val locationManager = remember {
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
// Helper function to get location using native APIs
fun fetchDeviceLocation() {
try {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
var location: android.location.Location? = null
if (isGpsEnabled) {
location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
}
@ -192,31 +220,23 @@ fun CreateVibePostScreen(
}
}
// Permission Launcher for the system dialog
val locationPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
fetchDeviceLocation()
} else {
Toast.makeText(context, "Location permission denied", Toast.LENGTH_SHORT).show()
}
if (isGranted) fetchDeviceLocation()
else Toast.makeText(context, "Location permission denied", Toast.LENGTH_SHORT).show()
}
// Click handler for the location chip
val onLocationChipClick: () -> Unit = {
val hasPermission = ContextCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
if (hasPermission) {
fetchDeviceLocation()
} else {
// Triggers the system "Allow / Deny" dialog
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
if (hasPermission) fetchDeviceLocation()
else locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
// ---------------- UI ----------------
Scaffold(
bottomBar = {
Row(
@ -227,7 +247,6 @@ fun CreateVibePostScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
// 1. Scrollable option chips on the left
LazyRow(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(10.dp),
@ -238,7 +257,7 @@ fun CreateVibePostScreen(
icon = Icons.Outlined.PhotoLibrary,
backgroundColor = LightPinkBg,
contentColor = Color(0xFFE91E63),
onClick = { showImagePickerDialog = true }
onClick = { if (!isPosting) showImagePickerDialog = true }
)
}
item {
@ -246,7 +265,7 @@ fun CreateVibePostScreen(
icon = Icons.Outlined.MyLocation,
backgroundColor = LightBlueBg,
contentColor = Color(0xFF1E88E5),
onClick = { onLocationChipClick() }
onClick = { if (!isPosting) onLocationChipClick() }
)
}
item {
@ -254,7 +273,7 @@ fun CreateVibePostScreen(
icon = Icons.Outlined.Mood,
backgroundColor = LightPinkBg,
contentColor = Color(0xFFE91E63),
onClick = { showFeelingSheet = true } // Opens bottom sheet
onClick = { if (!isPosting) showFeelingSheet = true }
)
}
item {
@ -268,97 +287,52 @@ fun CreateVibePostScreen(
Spacer(modifier = Modifier.width(8.dp))
// 2. React to UI state changes (Handles post success and triggers media uploads)
// 2. Share button on the right
val context = LocalContext.current
val createPostViewModel: CreatePostViewModel = viewModel()
val postMediaViewModel: PostMediaViewModel = viewModel()
val uiState by createPostViewModel.cretePpostUiState.collectAsState()
LaunchedEffect(uiState) {
when (val state = uiState) {
is CreatePostUiState.Success -> {
val postId = state.postData.id
Log.d("CreatePostDebug", "Post created successfully with ID: $postId. Starting media uploads...")
if (selectedMediaList.isEmpty()) {
Log.d("CreatePostDebug", "No media attached. Flow complete.")
onShareClick() // Navigate away or finish screen
} else {
// Loop through and upload media sequentially
for ((index, item) in selectedMediaList.withIndex()) {
val uri = when (item) {
is MediaContentItem.Photo -> item.uri
is MediaContentItem.Video -> item.uri
}
val position = index + 1 // Position starts at 1
if (uri != null && uri.toString().isNotBlank()) {
Log.d("CreatePostDebug", "Uploading media at position $position...")
postMediaViewModel.poostMedia(context,postId,index,uri)
// Call your media upload function here
// val isUploaded = createPostViewModel.uploadPostMediaItem(context, postId, position, uri)
}
}
Log.d("CreatePostDebug", "All media processed.")
onShareClick() // Navigate away or finish screen after media upload
}
}
is CreatePostUiState.Error -> {
Log.e("CreatePostDebug", "Error: ${state.message}")
}
else -> {}
}
}
Button(
enabled = !isPosting, // blocks double taps
onClick = {
// 1. Build the Location object if the tag is active and coordinates exist
if (textState.isBlank() && selectedMediaList.isEmpty()) {
Toast.makeText(context, "Add text or at least one photo/video", Toast.LENGTH_SHORT).show()
return@Button
}
// ... build request and call createPost()
val postLocation = if (showLocationTag && latitude != null && longitude != null) {
Location(
name = "Current Location", // Or your geocoded address string
name = "Current Location",
latitude = latitude,
longitude = longitude
)
} else {
null
}
} else null
// 2. Construct the request payload matching your data class
val createPostRequest = CreatePostRequest(
content = textState,
post_type = post_type, // "text", "image", or "video" (dynamically derived)
content_format = "markdown", // Adjust based on your backend requirement
feeling = selectedFeeling?.let { "${it.name} ${it.emoji}" }, // Passing the selected feeling name/label
content = textState.trim().ifBlank { " " }, // ← the fix: " " instead of ""
post_type = post_type,
content_format = "plain", // keep markdown, it works
feeling = selectedFeeling?.let { "${it.name} ${it.emoji}" },
location = postLocation
)
// Simply trigger the ViewModel function inside onClick!
// Only start the request. The screen is closed by the
// LaunchedEffect above once the post is created.
createPostViewModel.createPost(context, createPostRequest)
// 3. Pass the payload to your ViewModel or API repository callback
// onShareClick(requestPayload, selectedMediaList)
onShareClick()
},
modifier = Modifier
.wrapContentWidth()
.height(40.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
disabledContainerColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.background(
brush = Brush.horizontalGradient(
colors = listOf(Color(0xFFC2185B), Color(0xFF7B1FA2))
colors = if (isPosting)
listOf(Color(0xFFC2185B).copy(alpha = 0.5f), Color(0xFF7B1FA2).copy(alpha = 0.5f))
else
listOf(Color(0xFFC2185B), Color(0xFF7B1FA2))
),
shape = RoundedCornerShape(12.dp)
)
@ -367,7 +341,7 @@ fun CreateVibePostScreen(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Share",
text = if (isPosting) "Posting..." else "Share",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
@ -387,71 +361,67 @@ fun CreateVibePostScreen(
containerColor = Color.White
) { innerPadding ->
Column(
Box(
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
.fillMaxSize()
.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)
)
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = buildAnnotatedString {
// "Alex Rivera " - Bold
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("Alex Rivera ")
}
Spacer(modifier = Modifier.width(12.dp))
// "- feeling " - Normal weight (also fixed "felling" to "feeling")
selectedFeeling?.let { feeling ->
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) {
append(" — feeling ")
}
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = buildAnnotatedString {
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}")
append("Alex Rivera ")
}
}
},
fontSize = 16.sp
)
Spacer(modifier = Modifier.width(4.dp))
//this is for vibe
}
selectedFeeling?.let { feeling ->
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) {
append(" — feeling ")
}
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("${feeling.name} ${feeling.emoji}")
}
}
},
fontSize = 16.sp
)
Spacer(modifier = Modifier.width(4.dp))
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth(),
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
@ -475,134 +445,134 @@ fun CreateVibePostScreen(
modifier = Modifier.size(16.dp)
)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Text Area
TextField(
value = textState,
onValueChange = { if (it.length <= maxChar) textState = it },
textStyle = TextStyle(
fontSize = 20.sp,
lineHeight = 28.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF1E1E1E),
letterSpacing = 0.2.sp
),
placeholder = {
Text(
"What's your vibe today? Share a moment, date idea, or story...",
color = Color(0xFFA0A0A8),
Spacer(modifier = Modifier.height(16.dp))
// Text Area
TextField(
value = textState,
onValueChange = { if (it.length <= maxChar) textState = it },
enabled = !isPosting,
textStyle = TextStyle(
fontSize = 20.sp,
lineHeight = 28.sp
lineHeight = 28.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF1E1E1E),
letterSpacing = 0.2.sp
),
placeholder = {
Text(
"What's your vibe today? Share a moment, date idea, or story...",
color = Color(0xFFA0A0A8),
fontSize = 20.sp,
lineHeight = 28.sp
)
},
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFE91E63)
)
},
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFE91E63)
)
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(16.dp))
// Horizontal Scrollable Media Row
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)
}
}
// 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()
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(10.dp)
) {
OverlayIconButton(
icon = Icons.Default.Close,
onClick = { if (!isPosting) selectedMediaList.removeAt(index) }
)
}
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
Surface(
color = DarkOverlay,
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.align(Alignment.TopStart)
.padding(10.dp)
) {
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
)
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))
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(24.dp))
}
// Location Tag Bar
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = Modifier.height(24.dp))
// LOADING BAR: runs left to right along the top while posting
if (isPosting) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.align(Alignment.TopCenter),
color = Color(0xFFC2185B),
trackColor = Color(0xFFF3D5E3)
)
}
}
// Camera / Gallery Choice Bottom Sheet Dialog
// Camera / Gallery Choice Bottom Sheet
if (showImagePickerDialog) {
ModalBottomSheet(
onDismissRequest = { showImagePickerDialog = false },
@ -620,7 +590,6 @@ fun CreateVibePostScreen(
modifier = Modifier.padding(bottom = 16.dp)
)
// Pick from Gallery (Photo + Video combined option)
Row(
modifier = Modifier
.fillMaxWidth()
@ -643,7 +612,6 @@ fun CreateVibePostScreen(
Text(text = "Choose Photos & Videos from Gallery", fontSize = 16.sp)
}
// Take Photo
Row(
modifier = Modifier
.fillMaxWidth()
@ -666,7 +634,6 @@ fun CreateVibePostScreen(
Text(text = "Take Photo", fontSize = 16.sp)
}
// Record Video
Row(
modifier = Modifier
.fillMaxWidth()
@ -696,9 +663,7 @@ fun CreateVibePostScreen(
if (showFeelingSheet) {
FeelingPickerBottomSheet(
onDismiss = { showFeelingSheet = false },
onFeelingSelected = { feeling ->
selectedFeeling = feeling
}
onFeelingSelected = { feeling -> selectedFeeling = feeling }
)
}
}
@ -716,9 +681,7 @@ fun VideoPlayerItem(videoUri: Uri) {
}
DisposableEffect(Unit) {
onDispose {
exoPlayer.release()
}
onDispose { exoPlayer.release() }
}
AndroidView(
@ -775,13 +738,10 @@ fun VibeOptionChip(
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(6.dp))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeelingPickerBottomSheet(
@ -824,10 +784,7 @@ fun FeelingPickerBottomSheet(
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = feeling.emoji,
fontSize = 20.sp
)
Text(text = feeling.emoji, fontSize = 20.sp)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = feeling.name,
@ -842,10 +799,11 @@ fun FeelingPickerBottomSheet(
}
}
}
@Preview(showBackground = true, showSystemUi = true, backgroundColor = 0xFFFFFFFF)
@Composable
fun CreateVibePostScreenPreview() {
DattingAPPTheme {
CreateVibePostScreen()
}
}
}

4
app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt

@ -350,7 +350,9 @@ fun HomeScreen() {
// CREATE POST
// =========================================
2 -> {
CreateVibePostScreen()
CreateVibePostScreen(
onShareClick = { selectedItem = 1 } // 1 = Explore tab
)
}
// =========================================
// CHAT

8
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt

@ -50,12 +50,14 @@ interface HomeApiService {
@Path("id") postId: Int
): Response<PostResponse>
@POST("/api/v1/explore/posts")
@POST("api/v1/explore/posts")
suspend fun createPost(
@Body request: CreatePostRequest
@Body body: CreatePostRequest
): Response<CreatePostResponse>
// POST Upload Post Media
// https://melove.youthome.com.cn/api/v1/explore/posts/{post_id}/media?position=0
// form-data field name: "media" (as in your second screenshot)
@Multipart
@POST("api/v1/explore/posts/{post_id}/media")
suspend fun uploadPostMedia(

82
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt

@ -179,58 +179,64 @@ data class PostAuthor(
)
//this is for create post
// ---------------- Create Post ----------------
data class Location(
val name: String?,
val latitude: Double?,
val longitude: Double?
@SerializedName("name") val name: String?,
@SerializedName("latitude") val latitude: Double?,
@SerializedName("longitude") val longitude: Double?
)
data class CreatePostAuthor(
val id: Int,
val full_name: String,
val city: String,
val country: String,
val compatibility_percentage: Int
// Request body — matches your first screenshot exactly
data class CreatePostRequest(
@SerializedName("content") val content: String,
@SerializedName("post_type") val post_type: String,
@SerializedName("content_format") val content_format: String,
@SerializedName("feeling") val feeling: String?,
@SerializedName("location") val location: Location?
)
// Represents the request payload structure (Top Image)
data class CreatePostRequest(
val content: String,
val post_type: String,
val content_format: String,
val feeling: String?,
val location: Location?
data class CreatePostAuthor(
@SerializedName("id") val id: Int,
@SerializedName("full_name") val full_name: String?,
@SerializedName("profile_image_url") val profile_image_url: String?,
@SerializedName("city") val city: String?,
@SerializedName("country") val country: String?, // server returns null here!
@SerializedName("compatibility_percentage") val compatibility_percentage: Int?
)
// Represents the full post response (Bottom Image)
// Response — matches your first screenshot's response body
data class CreatePostResponse(
val id: Int,
val content: String,
val post_type: String,
val content_format: String,
val feeling: String?,
val location: Location?,
val author: CreatePostAuthor,
val like_count: Int,
val comment_count: Int,
val liked_by_me: Boolean,
val media: List<Any>, // Change to specific model if media items have structure
val created_at: String
@SerializedName("id") val id: Int,
@SerializedName("content") val content: String?,
@SerializedName("post_type") val post_type: String?,
@SerializedName("content_format") val content_format: String?,
@SerializedName("feeling") val feeling: String?,
@SerializedName("location") val location: Location?,
@SerializedName("author") val author: CreatePostAuthor?,
@SerializedName("like_count") val like_count: Int?,
@SerializedName("comment_count") val comment_count: Int?,
@SerializedName("liked_by_me") val liked_by_me: Boolean?,
@SerializedName("liked_by_users") val liked_by_users: List<Any>?,
@SerializedName("media") val media: List<Any>?,
@SerializedName("created_at") val created_at: String?
)
// ---------------- Upload Post Media ----------------
// Response — matches your second screenshot's response body
data class PostMediaResponse(
@SerializedName("id") val id: Int,
@SerializedName("media_type") val media_type: String?,
@SerializedName("mime_type") val mime_type: String?,
@SerializedName("size_bytes") val size_bytes: Long?,
@SerializedName("position") val position: Int?,
@SerializedName("url") val url: String?
)
data class DeletePostResponse(
val message: String
)
data class PostMediaResponse(
val id: Int,
val media_type: String,
val mime_type: String,
val size_bytes: Int,
val position: Int,
val url: String
)
data class PostInteraction(
val likedByMe: Boolean,

5
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeNetwokModule.kt

@ -8,6 +8,11 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object AppLog {
const val TAG = "PostFlow" // ← the ONLY tag you filter in Logcat
}
object HomeNetworkModule {
private const val BASE_URL = "https://melove.youthome.com.cn/"

141
app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt

@ -312,13 +312,11 @@ class PostViewModel : ViewModel() {
}
//this is for createpost viwmodel
sealed interface CreatePostUiState {
object Idle : CreatePostUiState
object Loading : CreatePostUiState
data class Success(val postData: CreatePostResponse) : CreatePostUiState // Changed to List<PostResponse>
data class Success(val postData: CreatePostResponse) : CreatePostUiState
data class Error(val message: String) : CreatePostUiState
}
@ -336,50 +334,91 @@ class CreatePostViewModel : ViewModel() {
_cretePostUiState.value = CreatePostUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_cretePostUiState.value = CreatePostUiState.Error("No access token found. Please log in again.")
Log.e(AppLog.TAG, "No access token found")
_cretePostUiState.value =
CreatePostUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
// Returns CreatePostResponse directly
val createdPost = HomeNetworkModule
val safeRequest = postInfo.copy(
content = postInfo.content.trim().ifBlank { " " },
content_format = "plain",
location = postInfo.location?.let {
if (it.name.isNullOrBlank()) it.copy(name = "Current Location") else it
} ?: Location(name = "Current Location", latitude = null, longitude = null)
)
Log.d(AppLog.TAG,
"Sending JSON: ${com.google.gson.GsonBuilder().serializeNulls().create().toJson(safeRequest)}")
val response = HomeNetworkModule
.getApiService(appContext)
.createPost(postInfo)
.createPost(safeRequest)
Log.d("CreatePostDebug", "Response Code: ${createdPost.code()}")
if (createdPost.isSuccessful && createdPost.body() != null) {
_cretePostUiState.value = CreatePostUiState.Success(createdPost.body()!!) // Use .body() here!
Log.d(AppLog.TAG, "Create post response code: ${response.code()}")
val body = response.body()
if (response.isSuccessful && body != null) {
Log.d(AppLog.TAG, "Post created, id = ${body.id}")
_cretePostUiState.value = CreatePostUiState.Success(body)
} else {
_cretePostUiState.value = CreatePostUiState.Error("No post data received from the server.")
val err = response.errorBody()?.string()
Log.e(AppLog.TAG, "Create post FAILED HTTP ${response.code()}: $err")
val message = when (response.code()) {
401 -> "Authentication failed. Please log in again."
403 -> "The server refused access."
422 -> "Invalid post: ${parse422Message(err)}"
else -> "HTTP ${response.code()}: ${err ?: "no details"}"
}
_cretePostUiState.value = CreatePostUiState.Error(message)
}
} catch (e: HttpException) {
// Handle HTTP errors via HttpException automatically thrown by Retrofit
val message = when (e.code()) {
401 -> "Authentication failed. Please log in again."
403 -> "The server refused access to this profile."
404 -> "User profile was not found."
else -> "Request failed: HTTP ${e.code()}."
}
_cretePostUiState.value = CreatePostUiState.Error(message)
Log.e(AppLog.TAG, "Create post HttpException: ${e.code()}", e)
_cretePostUiState.value =
CreatePostUiState.Error("Request failed: HTTP ${e.code()}.")
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_cretePostUiState.value = CreatePostUiState.Error("Request failed: ${e.javaClass.simpleName}")
Log.e(AppLog.TAG, "Create post unexpected failure", e)
_cretePostUiState.value =
CreatePostUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
private fun parse422Message(raw: String?): String {
if (raw.isNullOrBlank()) return "validation failed"
return try {
val json = org.json.JSONObject(raw)
when {
json.has("detail") -> {
val detail = json.get("detail")
if (detail is org.json.JSONArray && detail.length() > 0) {
val first = detail.getJSONObject(0)
val field = first.optJSONArray("loc")
?.let { it.getString(it.length() - 1) } ?: "field"
"$field: ${first.optString("msg")}"
} else detail.toString()
}
json.has("message") -> json.getString("message")
else -> raw
}
} catch (e: Exception) {
raw
}
}
fun reset() {
_cretePostUiState.value = CreatePostUiState.Idle
}
}
sealed interface PostMediaUiState {
object Idle : PostMediaUiState
object Loading : PostMediaUiState
data class Success(val postData: PostMediaResponse) : PostMediaUiState // Changed to List<PostResponse>
data class Success(val postData: PostMediaResponse) : PostMediaUiState
data class Error(val message: String) : PostMediaUiState
}
class PostMediaViewModel : ViewModel() {
private val _postMediaUiState = MutableStateFlow<PostMediaUiState>(PostMediaUiState.Idle)
@ -394,13 +433,13 @@ class PostMediaViewModel : ViewModel() {
_postMediaUiState.value = PostMediaUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_postMediaUiState.value = PostMediaUiState.Error("No access token found. Please log in again.")
Log.e(AppLog.TAG, "No access token in user_tokens preferences")
_postMediaUiState.value =
PostMediaUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
// Fix: Use return@launch instead of return false inside viewModelScope.launch
val multipartPart = uriToMultipartPart(appContext, uri)
if (multipartPart == null) {
_postMediaUiState.value = PostMediaUiState.Error("Failed to process media file.")
@ -411,14 +450,18 @@ class PostMediaViewModel : ViewModel() {
.getApiService(appContext)
.uploadPostMedia(postId = postId, position = position, mediaFile = multipartPart)
Log.d("CreatePostDebug", "Response Code: ${response.code()}")
Log.d(AppLog.TAG, "Upload media response code: ${response.code()}")
if (response.isSuccessful && response.body() != null) {
_postMediaUiState.value = PostMediaUiState.Success(response.body()!!)
} else {
_postMediaUiState.value = PostMediaUiState.Error("No post data received from the server.")
val err = response.errorBody()?.string()
Log.e(AppLog.TAG, "Upload media FAILED HTTP ${response.code()}: $err")
_postMediaUiState.value =
PostMediaUiState.Error("Upload failed (${response.code()}): ${err ?: "no details"}")
}
} catch (e: HttpException) {
Log.e(AppLog.TAG, "Upload media HttpException: ${e.code()}", e)
val message = when (e.code()) {
401 -> "Authentication failed. Please log in again."
403 -> "The server refused access to this profile."
@ -427,14 +470,43 @@ class PostMediaViewModel : ViewModel() {
}
_postMediaUiState.value = PostMediaUiState.Error(message)
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_postMediaUiState.value = PostMediaUiState.Error("Request failed: ${e.javaClass.simpleName}")
Log.e(AppLog.TAG, "Upload media unexpected failure", e)
_postMediaUiState.value =
PostMediaUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
suspend fun uploadMedia(context: Context, postId: Int, position: Int, uri: Uri): String? {
val appContext = context.applicationContext
return try {
val part = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
uriToMultipartPart(appContext, uri)
} ?: run {
Log.e(AppLog.TAG, "Upload $position: could not read the file ($uri)")
return "Could not read the file"
}
val response = HomeNetworkModule
.getApiService(appContext)
.uploadPostMedia(postId = postId, position = position, mediaFile = part)
if (response.isSuccessful) {
Log.d(AppLog.TAG, "Upload $position OK (${response.code()})")
null
} else {
val raw = try { response.errorBody()?.string() } catch (e: Exception) { null }
Log.e(AppLog.TAG, "Upload $position FAILED code=${response.code()} body=$raw url=${response.raw().request.url}")
"Upload failed (${response.code()}): ${raw ?: "no details"}"
}
} catch (e: kotlin.coroutines.cancellation.CancellationException) {
throw e
} catch (e: Exception) {
Log.e(AppLog.TAG, "Upload $position exception", e)
"${e.javaClass.simpleName}: ${e.message ?: "no details"}"
}
}
// Helper to convert Uri to MultipartBody.Part
fun uriToMultipartPart(
context: Context,
uri: Uri,
@ -444,7 +516,6 @@ class PostMediaViewModel : ViewModel() {
val contentResolver = context.contentResolver
val mimeType = contentResolver.getType(uri) ?: "image/*"
// Create a temp file in cache directory
val inputStream = contentResolver.openInputStream(uri) ?: return null
val tempFile =
File.createTempFile("upload_", getExtensionFromMimeType(mimeType), context.cacheDir)
@ -457,7 +528,7 @@ class PostMediaViewModel : ViewModel() {
val requestBody = tempFile.asRequestBody(mimeType.toMediaTypeOrNull())
return MultipartBody.Part.createFormData(paramName, tempFile.name, requestBody)
} catch (e: kotlin.Exception) {
e.printStackTrace()
Log.e(AppLog.TAG, "Failed to convert Uri to multipart: $uri", e)
return null
}
}

Loading…
Cancel
Save