Browse Source

create post update

master
osman160128 1 week ago
parent
commit
91e1138bb9
  1. 306
      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. 139
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt

306
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.Location
import com.example.dattingapp.homes.networkandapi.CreatePostRequest 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.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts 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.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape 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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import coil.compose.rememberAsyncImagePainter 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.VibeFeeling
import com.example.dattingapp.homes.networkandapi.vibeFeelingsList 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.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.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 import java.io.File
// Sealed class representing selectable media types (Photo or Video) // 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) class Video(uri: Uri) : MediaContentItem(uri)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CreateVibePostScreen( fun CreateVibePostScreen(
onCloseClick: () -> Unit = {}, onCloseClick: () -> Unit = {},
onShareClick: () -> Unit = {} onShareClick: () -> Unit = {} // called ONLY after the post is created
) { ) {
val context = LocalContext.current val context = LocalContext.current
var textState by remember { mutableStateOf("") } var textState by remember { mutableStateOf("") }
val maxChar = 500 val maxChar = 500
// Holds selected media items (photos, videos, or both)
val selectedMediaList = remember { mutableStateListOf<MediaContentItem>() } val selectedMediaList = remember { mutableStateListOf<MediaContentItem>() }
// Determine the post type dynamically based on attached media
val post_type = remember(selectedMediaList) { val post_type = remember(selectedMediaList) {
when { when {
selectedMediaList.any { it is MediaContentItem.Video } -> "video" selectedMediaList.any { it is MediaContentItem.Video } -> "video"
@ -107,10 +99,60 @@ fun CreateVibePostScreen(
var showFeelingSheet by remember { mutableStateOf(false) } var showFeelingSheet by remember { mutableStateOf(false) }
var selectedFeeling by remember { mutableStateOf<VibeFeeling?>(null) } 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( val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia() contract = ActivityResultContracts.PickMultipleVisualMedia()
) { uris: List<Uri> -> ) { uris: List<Uri> ->
@ -124,20 +166,14 @@ fun CreateVibePostScreen(
} }
} }
// Temporary file helper for Camera captures
fun createTempFileUri(extension: String, prefix: String): Uri { fun createTempFileUri(extension: String, prefix: String): Uri {
val tempFile = File.createTempFile(prefix, extension, context.cacheDir).apply { val tempFile = File.createTempFile(prefix, extension, context.cacheDir).apply {
createNewFile() createNewFile()
deleteOnExit() deleteOnExit()
} }
return FileProvider.getUriForFile( return FileProvider.getUriForFile(context, "${context.packageName}.provider", tempFile)
context,
"${context.packageName}.provider",
tempFile
)
} }
// Camera Launchers for taking photos or recording videos
val cameraPhotoLauncher = rememberLauncherForActivityResult( val cameraPhotoLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture() contract = ActivityResultContracts.TakePicture()
) { success: Boolean -> ) { success: Boolean ->
@ -154,25 +190,17 @@ fun CreateVibePostScreen(
} }
} }
// ---------------- Location ----------------
// 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)
val locationManager = remember { val locationManager = remember {
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
} }
// Helper function to get location using native APIs
fun fetchDeviceLocation() { fun fetchDeviceLocation() {
try { try {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
var location: android.location.Location? = null var location: android.location.Location? = null
if (isGpsEnabled) { if (isGpsEnabled) {
location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
} }
@ -192,31 +220,23 @@ fun CreateVibePostScreen(
} }
} }
// Permission Launcher for the system dialog
val locationPermissionLauncher = rememberLauncherForActivityResult( val locationPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission() contract = ActivityResultContracts.RequestPermission()
) { isGranted -> ) { isGranted ->
if (isGranted) { if (isGranted) fetchDeviceLocation()
fetchDeviceLocation() else Toast.makeText(context, "Location permission denied", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Location permission denied", Toast.LENGTH_SHORT).show()
}
} }
// Click handler for the location chip
val onLocationChipClick: () -> Unit = { val onLocationChipClick: () -> Unit = {
val hasPermission = ContextCompat.checkSelfPermission( val hasPermission = ContextCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_FINE_LOCATION context, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
if (hasPermission) { if (hasPermission) fetchDeviceLocation()
fetchDeviceLocation() else locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
// Triggers the system "Allow / Deny" dialog
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
} }
// ---------------- UI ----------------
Scaffold( Scaffold(
bottomBar = { bottomBar = {
Row( Row(
@ -227,7 +247,6 @@ fun CreateVibePostScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
// 1. Scrollable option chips on the left
LazyRow( LazyRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
@ -238,7 +257,7 @@ fun CreateVibePostScreen(
icon = Icons.Outlined.PhotoLibrary, icon = Icons.Outlined.PhotoLibrary,
backgroundColor = LightPinkBg, backgroundColor = LightPinkBg,
contentColor = Color(0xFFE91E63), contentColor = Color(0xFFE91E63),
onClick = { showImagePickerDialog = true } onClick = { if (!isPosting) showImagePickerDialog = true }
) )
} }
item { item {
@ -246,7 +265,7 @@ fun CreateVibePostScreen(
icon = Icons.Outlined.MyLocation, icon = Icons.Outlined.MyLocation,
backgroundColor = LightBlueBg, backgroundColor = LightBlueBg,
contentColor = Color(0xFF1E88E5), contentColor = Color(0xFF1E88E5),
onClick = { onLocationChipClick() } onClick = { if (!isPosting) onLocationChipClick() }
) )
} }
item { item {
@ -254,7 +273,7 @@ fun CreateVibePostScreen(
icon = Icons.Outlined.Mood, icon = Icons.Outlined.Mood,
backgroundColor = LightPinkBg, backgroundColor = LightPinkBg,
contentColor = Color(0xFFE91E63), contentColor = Color(0xFFE91E63),
onClick = { showFeelingSheet = true } // Opens bottom sheet onClick = { if (!isPosting) showFeelingSheet = true }
) )
} }
item { item {
@ -268,97 +287,52 @@ fun CreateVibePostScreen(
Spacer(modifier = Modifier.width(8.dp)) 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( Button(
enabled = !isPosting, // blocks double taps
onClick = { 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) { val postLocation = if (showLocationTag && latitude != null && longitude != null) {
Location( Location(
name = "Current Location", // Or your geocoded address string name = "Current Location",
latitude = latitude, latitude = latitude,
longitude = longitude longitude = longitude
) )
} else { } else null
null
}
// 2. Construct the request payload matching your data class
val createPostRequest = CreatePostRequest( val createPostRequest = CreatePostRequest(
content = textState, content = textState.trim().ifBlank { " " }, // ← the fix: " " instead of ""
post_type = post_type, // "text", "image", or "video" (dynamically derived) post_type = post_type,
content_format = "markdown", // Adjust based on your backend requirement content_format = "plain", // keep markdown, it works
feeling = selectedFeeling?.let { "${it.name} ${it.emoji}" }, // Passing the selected feeling name/label feeling = selectedFeeling?.let { "${it.name} ${it.emoji}" },
location = postLocation location = postLocation
) )
// Only start the request. The screen is closed by the
// LaunchedEffect above once the post is created.
// Simply trigger the ViewModel function inside onClick!
createPostViewModel.createPost(context, createPostRequest) createPostViewModel.createPost(context, createPostRequest)
// 3. Pass the payload to your ViewModel or API repository callback
// onShareClick(requestPayload, selectedMediaList)
onShareClick()
}, },
modifier = Modifier modifier = Modifier
.wrapContentWidth() .wrapContentWidth()
.height(40.dp), .height(40.dp),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
disabledContainerColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp) contentPadding = PaddingValues(0.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.background( .background(
brush = Brush.horizontalGradient( 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) shape = RoundedCornerShape(12.dp)
) )
@ -367,7 +341,7 @@ fun CreateVibePostScreen(
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "Share", text = if (isPosting) "Posting..." else "Share",
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 14.sp fontSize = 14.sp
@ -387,13 +361,17 @@ fun CreateVibePostScreen(
containerColor = Color.White containerColor = Color.White
) { innerPadding -> ) { innerPadding ->
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
// User Profile Header // User Profile Header
Row( Row(
@ -423,18 +401,14 @@ fun CreateVibePostScreen(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
// "Alex Rivera " - Bold
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("Alex Rivera ") append("Alex Rivera ")
} }
// "- feeling " - Normal weight (also fixed "felling" to "feeling")
selectedFeeling?.let { feeling -> selectedFeeling?.let { feeling ->
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) { withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) {
append(" — feeling ") append(" — feeling ")
} }
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 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("${feeling.name} ${feeling.emoji}")
} }
} }
@ -442,16 +416,12 @@ fun CreateVibePostScreen(
fontSize = 16.sp fontSize = 16.sp
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
//this is for vibe
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -475,10 +445,6 @@ fun CreateVibePostScreen(
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
} }
} }
} }
@ -488,6 +454,7 @@ fun CreateVibePostScreen(
TextField( TextField(
value = textState, value = textState,
onValueChange = { if (it.length <= maxChar) textState = it }, onValueChange = { if (it.length <= maxChar) textState = it },
enabled = !isPosting,
textStyle = TextStyle( textStyle = TextStyle(
fontSize = 20.sp, fontSize = 20.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
@ -509,15 +476,15 @@ fun CreateVibePostScreen(
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFE91E63) 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 (Contains Photos and Videos) // Horizontal Scrollable Media Row
if (selectedMediaList.isNotEmpty()) { if (selectedMediaList.isNotEmpty()) {
LazyRow( LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
@ -545,7 +512,6 @@ fun CreateVibePostScreen(
} }
} }
// Delete Card Button
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
@ -553,11 +519,10 @@ fun CreateVibePostScreen(
) { ) {
OverlayIconButton( OverlayIconButton(
icon = Icons.Default.Close, icon = Icons.Default.Close,
onClick = { selectedMediaList.removeAt(index) } onClick = { if (!isPosting) selectedMediaList.removeAt(index) }
) )
} }
// Media Type Badge Overlay (Top Left)
Surface( Surface(
color = DarkOverlay, color = DarkOverlay,
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
@ -591,18 +556,23 @@ fun CreateVibePostScreen(
Spacer(modifier = Modifier.height(16.dp)) 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)) Spacer(modifier = Modifier.height(24.dp))
} }
// Camera / Gallery Choice Bottom Sheet Dialog // 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
if (showImagePickerDialog) { if (showImagePickerDialog) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showImagePickerDialog = false }, onDismissRequest = { showImagePickerDialog = false },
@ -620,7 +590,6 @@ fun CreateVibePostScreen(
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
// Pick from Gallery (Photo + Video combined option)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -643,7 +612,6 @@ fun CreateVibePostScreen(
Text(text = "Choose Photos & Videos from Gallery", fontSize = 16.sp) Text(text = "Choose Photos & Videos from Gallery", fontSize = 16.sp)
} }
// Take Photo
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -666,7 +634,6 @@ fun CreateVibePostScreen(
Text(text = "Take Photo", fontSize = 16.sp) Text(text = "Take Photo", fontSize = 16.sp)
} }
// Record Video
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -696,9 +663,7 @@ fun CreateVibePostScreen(
if (showFeelingSheet) { if (showFeelingSheet) {
FeelingPickerBottomSheet( FeelingPickerBottomSheet(
onDismiss = { showFeelingSheet = false }, onDismiss = { showFeelingSheet = false },
onFeelingSelected = { feeling -> onFeelingSelected = { feeling -> selectedFeeling = feeling }
selectedFeeling = feeling
}
) )
} }
} }
@ -716,9 +681,7 @@ fun VideoPlayerItem(videoUri: Uri) {
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose { exoPlayer.release() }
exoPlayer.release()
}
} }
AndroidView( AndroidView(
@ -775,13 +738,10 @@ fun VibeOptionChip(
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun FeelingPickerBottomSheet( fun FeelingPickerBottomSheet(
@ -824,10 +784,7 @@ fun FeelingPickerBottomSheet(
.padding(horizontal = 12.dp, vertical = 10.dp), .padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(text = feeling.emoji, fontSize = 20.sp)
text = feeling.emoji,
fontSize = 20.sp
)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = feeling.name, text = feeling.name,
@ -842,6 +799,7 @@ fun FeelingPickerBottomSheet(
} }
} }
} }
@Preview(showBackground = true, showSystemUi = true, backgroundColor = 0xFFFFFFFF) @Preview(showBackground = true, showSystemUi = true, backgroundColor = 0xFFFFFFFF)
@Composable @Composable
fun CreateVibePostScreenPreview() { fun CreateVibePostScreenPreview() {

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

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

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

@ -50,12 +50,14 @@ interface HomeApiService {
@Path("id") postId: Int @Path("id") postId: Int
): Response<PostResponse> ): Response<PostResponse>
@POST("/api/v1/explore/posts") @POST("api/v1/explore/posts")
suspend fun createPost( suspend fun createPost(
@Body request: CreatePostRequest @Body body: CreatePostRequest
): Response<CreatePostResponse> ): 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 @Multipart
@POST("api/v1/explore/posts/{post_id}/media") @POST("api/v1/explore/posts/{post_id}/media")
suspend fun uploadPostMedia( 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( data class Location(
val name: String?, @SerializedName("name") val name: String?,
val latitude: Double?, @SerializedName("latitude") val latitude: Double?,
val longitude: Double? @SerializedName("longitude") val longitude: Double?
) )
data class CreatePostAuthor( // Request body — matches your first screenshot exactly
val id: Int, data class CreatePostRequest(
val full_name: String, @SerializedName("content") val content: String,
val city: String, @SerializedName("post_type") val post_type: String,
val country: String, @SerializedName("content_format") val content_format: String,
val compatibility_percentage: Int @SerializedName("feeling") val feeling: String?,
@SerializedName("location") val location: Location?
) )
// Represents the request payload structure (Top Image) data class CreatePostAuthor(
data class CreatePostRequest( @SerializedName("id") val id: Int,
val content: String, @SerializedName("full_name") val full_name: String?,
val post_type: String, @SerializedName("profile_image_url") val profile_image_url: String?,
val content_format: String, @SerializedName("city") val city: String?,
val feeling: String?, @SerializedName("country") val country: String?, // server returns null here!
val location: Location? @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( data class CreatePostResponse(
val id: Int, @SerializedName("id") val id: Int,
val content: String, @SerializedName("content") val content: String?,
val post_type: String, @SerializedName("post_type") val post_type: String?,
val content_format: String, @SerializedName("content_format") val content_format: String?,
val feeling: String?, @SerializedName("feeling") val feeling: String?,
val location: Location?, @SerializedName("location") val location: Location?,
val author: CreatePostAuthor, @SerializedName("author") val author: CreatePostAuthor?,
val like_count: Int, @SerializedName("like_count") val like_count: Int?,
val comment_count: Int, @SerializedName("comment_count") val comment_count: Int?,
val liked_by_me: Boolean, @SerializedName("liked_by_me") val liked_by_me: Boolean?,
val media: List<Any>, // Change to specific model if media items have structure @SerializedName("liked_by_users") val liked_by_users: List<Any>?,
val created_at: String @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( data class DeletePostResponse(
val message: String 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( data class PostInteraction(
val likedByMe: Boolean, 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 retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object AppLog {
const val TAG = "PostFlow" // ← the ONLY tag you filter in Logcat
}
object HomeNetworkModule { object HomeNetworkModule {
private const val BASE_URL = "https://melove.youthome.com.cn/" private const val BASE_URL = "https://melove.youthome.com.cn/"

139
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 { sealed interface CreatePostUiState {
object Idle : CreatePostUiState object Idle : CreatePostUiState
object Loading : 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 data class Error(val message: String) : CreatePostUiState
} }
@ -336,50 +334,91 @@ class CreatePostViewModel : ViewModel() {
_cretePostUiState.value = CreatePostUiState.Loading _cretePostUiState.value = CreatePostUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences") Log.e(AppLog.TAG, "No access token found")
_cretePostUiState.value = CreatePostUiState.Error("No access token found. Please log in again.") _cretePostUiState.value =
CreatePostUiState.Error("No access token found. Please log in again.")
return@launch return@launch
} }
try { try {
// Returns CreatePostResponse directly val safeRequest = postInfo.copy(
val createdPost = HomeNetworkModule 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) .getApiService(appContext)
.createPost(postInfo) .createPost(safeRequest)
Log.d("CreatePostDebug", "Response Code: ${createdPost.code()}") Log.d(AppLog.TAG, "Create post response code: ${response.code()}")
if (createdPost.isSuccessful && createdPost.body() != null) {
_cretePostUiState.value = CreatePostUiState.Success(createdPost.body()!!) // Use .body() here!
val body = response.body()
if (response.isSuccessful && body != null) {
Log.d(AppLog.TAG, "Post created, id = ${body.id}")
_cretePostUiState.value = CreatePostUiState.Success(body)
} else { } 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()) {
} catch (e: HttpException) {
// Handle HTTP errors via HttpException automatically thrown by Retrofit
val message = when (e.code()) {
401 -> "Authentication failed. Please log in again." 401 -> "Authentication failed. Please log in again."
403 -> "The server refused access to this profile." 403 -> "The server refused access."
404 -> "User profile was not found." 422 -> "Invalid post: ${parse422Message(err)}"
else -> "Request failed: HTTP ${e.code()}." else -> "HTTP ${response.code()}: ${err ?: "no details"}"
} }
_cretePostUiState.value = CreatePostUiState.Error(message) _cretePostUiState.value = CreatePostUiState.Error(message)
}
} catch (e: HttpException) {
Log.e(AppLog.TAG, "Create post HttpException: ${e.code()}", e)
_cretePostUiState.value =
CreatePostUiState.Error("Request failed: HTTP ${e.code()}.")
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e) Log.e(AppLog.TAG, "Create post unexpected failure", e)
_cretePostUiState.value = CreatePostUiState.Error("Request failed: ${e.javaClass.simpleName}") _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 { sealed interface PostMediaUiState {
object Idle : PostMediaUiState object Idle : PostMediaUiState
object Loading : 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 data class Error(val message: String) : PostMediaUiState
} }
class PostMediaViewModel : ViewModel() { class PostMediaViewModel : ViewModel() {
private val _postMediaUiState = MutableStateFlow<PostMediaUiState>(PostMediaUiState.Idle) private val _postMediaUiState = MutableStateFlow<PostMediaUiState>(PostMediaUiState.Idle)
@ -394,13 +433,13 @@ class PostMediaViewModel : ViewModel() {
_postMediaUiState.value = PostMediaUiState.Loading _postMediaUiState.value = PostMediaUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences") Log.e(AppLog.TAG, "No access token in user_tokens preferences")
_postMediaUiState.value = PostMediaUiState.Error("No access token found. Please log in again.") _postMediaUiState.value =
PostMediaUiState.Error("No access token found. Please log in again.")
return@launch return@launch
} }
try { try {
// Fix: Use return@launch instead of return false inside viewModelScope.launch
val multipartPart = uriToMultipartPart(appContext, uri) val multipartPart = uriToMultipartPart(appContext, uri)
if (multipartPart == null) { if (multipartPart == null) {
_postMediaUiState.value = PostMediaUiState.Error("Failed to process media file.") _postMediaUiState.value = PostMediaUiState.Error("Failed to process media file.")
@ -411,14 +450,18 @@ class PostMediaViewModel : ViewModel() {
.getApiService(appContext) .getApiService(appContext)
.uploadPostMedia(postId = postId, position = position, mediaFile = multipartPart) .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) { if (response.isSuccessful && response.body() != null) {
_postMediaUiState.value = PostMediaUiState.Success(response.body()!!) _postMediaUiState.value = PostMediaUiState.Success(response.body()!!)
} else { } 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) { } catch (e: HttpException) {
Log.e(AppLog.TAG, "Upload media HttpException: ${e.code()}", e)
val message = when (e.code()) { val message = when (e.code()) {
401 -> "Authentication failed. Please log in again." 401 -> "Authentication failed. Please log in again."
403 -> "The server refused access to this profile." 403 -> "The server refused access to this profile."
@ -427,14 +470,43 @@ class PostMediaViewModel : ViewModel() {
} }
_postMediaUiState.value = PostMediaUiState.Error(message) _postMediaUiState.value = PostMediaUiState.Error(message)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e) Log.e(AppLog.TAG, "Upload media unexpected failure", e)
_postMediaUiState.value = PostMediaUiState.Error("Request failed: ${e.javaClass.simpleName}") _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( fun uriToMultipartPart(
context: Context, context: Context,
uri: Uri, uri: Uri,
@ -444,7 +516,6 @@ class PostMediaViewModel : ViewModel() {
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
val mimeType = contentResolver.getType(uri) ?: "image/*" val mimeType = contentResolver.getType(uri) ?: "image/*"
// Create a temp file in cache directory
val inputStream = contentResolver.openInputStream(uri) ?: return null val inputStream = contentResolver.openInputStream(uri) ?: return null
val tempFile = val tempFile =
File.createTempFile("upload_", getExtensionFromMimeType(mimeType), context.cacheDir) File.createTempFile("upload_", getExtensionFromMimeType(mimeType), context.cacheDir)
@ -457,7 +528,7 @@ class PostMediaViewModel : ViewModel() {
val requestBody = tempFile.asRequestBody(mimeType.toMediaTypeOrNull()) val requestBody = tempFile.asRequestBody(mimeType.toMediaTypeOrNull())
return MultipartBody.Part.createFormData(paramName, tempFile.name, requestBody) return MultipartBody.Part.createFormData(paramName, tempFile.name, requestBody)
} catch (e: kotlin.Exception) { } catch (e: kotlin.Exception) {
e.printStackTrace() Log.e(AppLog.TAG, "Failed to convert Uri to multipart: $uri", e)
return null return null
} }
} }

Loading…
Cancel
Save