Browse Source

comment section

master
osman160128 2 weeks ago
parent
commit
f02cbea3c8
  1. 1
      app/build.gradle.kts
  2. 29
      app/src/main/AndroidManifest.xml
  3. 702
      app/src/main/java/com/example/dattingapp/FriendProfile.kt
  4. 301
      app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt
  5. 252
      app/src/main/java/com/example/dattingapp/LikeAndMatchScreen.kt
  6. 447
      app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt
  7. 61
      app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt
  8. 181
      app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt
  9. 41
      app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt
  10. 105
      app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
  11. 61
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
  12. 125
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
  13. 536
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
  14. 72
      app/src/main/java/com/example/dattingapp/homes/profile/FrienProfileResponse.kt
  15. 107
      app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt
  16. 168
      app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt
  17. 2
      app/src/main/res/values/strings.xml

1
app/build.gradle.kts

@ -64,6 +64,7 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.6.0") implementation("io.coil-kt:coil-compose:2.6.0")
implementation("androidx.navigation:navigation-compose:2.9.3") implementation("androidx.navigation:navigation-compose:2.9.3")
implementation("androidx.media3:media3-exoplayer:1.4.1") implementation("androidx.media3:media3-exoplayer:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
// Media3 UI components (PlayerView) // Media3 UI components (PlayerView)
implementation("androidx.media3:media3-ui:1.4.1") implementation("androidx.media3:media3-ui:1.4.1")

29
app/src/main/AndroidManifest.xml

@ -12,6 +12,8 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@ -23,7 +25,6 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.DattingAPP"> android:theme="@style/Theme.DattingAPP">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -32,74 +33,64 @@
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".registration.RegistrationScreen" android:name=".registration.RegistrationScreen"
android:exported="false" android:exported="false"
android:label="@string/title_activity_registration_screen" android:label="@string/title_activity_registration_screen"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".Login.LoginActivity" android:name=".Login.LoginActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_upload_profile_image" android:label="@string/title_activity_upload_profile_image"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".Login.IdVerification" android:name=".Login.IdVerification"
android:exported="false" android:exported="false"
android:label="@string/title_activity_id_verification" android:label="@string/title_activity_id_verification"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".homes.MyProfile" android:name=".homes.MyProfile"
android:exported="false" android:exported="false"
android:label="@string/title_activity_my_profile" android:label="@string/title_activity_my_profile"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".CreatePost" android:name=".CreatePost"
android:exported="false" android:exported="false"
android:label="@string/title_activity_create_post" android:label="@string/title_activity_create_post"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".homes.MeetScreen" android:name=".homes.MeetScreen"
android:exported="false" android:exported="false"
android:label="@string/title_activity_meet_screen" android:label="@string/title_activity_meet_screen"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".ChatScreen" android:name=".ChatScreen"
android:exported="false" android:exported="false"
android:label="@string/title_activity_chat_screen" android:label="@string/title_activity_chat_screen"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".homes.HomeActivity" android:name=".homes.HomeActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_chat_screen" android:label="@string/title_activity_chat_screen"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".FriendProfile" android:name=".FriendProfile"
android:exported="false" android:exported="false"
android:label="@string/title_activity_friend_profile" android:label="@string/title_activity_friend_profile"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".homes.MeetActivity" android:name=".homes.MeetActivity"
android:exported="false" android:exported="false"
@ -124,7 +115,6 @@
android:label="@string/title_activity_upload_image" android:label="@string/title_activity_upload_image"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".registration.UploadIntroVideoActivity" android:name=".registration.UploadIntroVideoActivity"
android:exported="false" android:exported="false"
@ -137,7 +127,18 @@
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.DattingAPP" android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity
android:name=".LikeANdMatchActivity"
android:exported="false"
android:label="@string/title_activity_like_and_match"
android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ShowPostingActivity"
android:exported="false"
android:label="@string/title_activity_show_posting"
android:theme="@style/Theme.DattingAPP"
android:windowSoftInputMode="adjustResize" />
</application> </application>
</manifest> </manifest>

702
app/src/main/java/com/example/dattingapp/FriendProfile.kt

@ -1,125 +1,107 @@
package com.example.dattingapp package com.example.dattingapp
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.TextSnippet
import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.material.icons.filled.Mood
import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.TextSnippet
import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.Button import androidx.compose.material3.*
import androidx.compose.material3.ButtonDefaults import androidx.compose.runtime.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview 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.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.dattingapp.homes.networkandapi.FriendProfileMedia import com.example.dattingapp.homes.networkandapi.*
import com.example.dattingapp.homes.networkandapi.FriendProfileResponse import com.example.dattingapp.homes.profile.FrienPostMedia
import com.example.dattingapp.homes.networkandapi.FriendProfileUiState import com.example.dattingapp.homes.profile.FriendAuthor
import com.example.dattingapp.homes.networkandapi.FriendProfileViewModel import com.example.dattingapp.homes.profile.FriendPost
import com.example.dattingapp.homes.networkandapi.MeetUserProfile import com.example.dattingapp.homes.profile.FriendProfileResponse
import com.example.dattingapp.homes.networkandapi.SwipeRequest import com.example.dattingapp.ui.theme.*
import com.example.dattingapp.homes.networkandapi.SwipeViewModel
import com.example.dattingapp.ui.theme.AccentPurple
import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.textDark
import com.example.dattingapp.ui.theme.BadgePink
import com.example.dattingapp.ui.theme.gradientBrush
import com.example.dattingapp.ui.theme.purpleButtonColor
import java.nio.file.WatchEvent
import java.time.LocalDate import java.time.LocalDate
import java.time.Period import java.time.Period
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.jvm.java
import androidx.compose.foundation.layout.* import kotlinx.serialization.json.Json
import androidx.compose.foundation.lazy.grid.GridCells import kotlinx.serialization.encodeToString
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.ui.unit.dp
class FriendProfile : ComponentActivity() { class FriendProfile : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
// Read extras passed from the Intent
val userId = intent.getIntExtra("USER_ID", -1) val userId = intent.getIntExtra("USER_ID", -1)
setContent { setContent {
DattingAPPTheme { DattingAPPTheme {
val context = LocalContext.current val context = LocalContext.current
val viewModel: FriendProfileViewModel = viewModel() val friendViewModel: FriendProfileViewModel = viewModel()
val postViewModel: PostViewModel = viewModel()
// 2. Collect state from ViewModel val profileUiState by friendViewModel.friendProfileUiState.collectAsState()
val uiState by viewModel.friendProfileUiState.collectAsState() val postUiState by postViewModel.postUiState.collectAsState()
// 3. Trigger network call once when screen loads LaunchedEffect(userId) {
LaunchedEffect(Unit) { if (userId != -1) {
viewModel.fetchUserProfile(context,userId) friendViewModel.fetchUserProfile(context, userId)
postViewModel.fetchUserPost(context, userId)
} }
when (val state = uiState) { }
when (val state = profileUiState) {
is FriendProfileUiState.Loading -> { is FriendProfileUiState.Loading -> {
// Show loading indicator Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} }
is FriendProfileUiState.Success -> { is FriendProfileUiState.Success -> {
val friendProfileData = state.friendProfile FriendProfileScreen(
FriendProfileScreen(friendProfileData,context) friendProfileData = state.friendProfile,
context = context,
postUiState = postUiState
)
} }
is FriendProfileUiState.Error -> { is FriendProfileUiState.Error -> {
// Show error message Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = state.message, color = MaterialTheme.colorScheme.error)
} }
FriendProfileUiState.Idle -> {
// Idle state
} }
FriendProfileUiState.Idle -> {}
} }
} }
} }
} }
@ -129,29 +111,26 @@ class FriendProfile : ComponentActivity() {
fun FriendProfileScreen( fun FriendProfileScreen(
friendProfileData: FriendProfileResponse, friendProfileData: FriendProfileResponse,
context: Context, context: Context,
postUiState: PostUiState = PostUiState.Idle,
swipeViewModel: SwipeViewModel = viewModel(), swipeViewModel: SwipeViewModel = viewModel(),
onBackClick: () -> Unit = {}, onBackClick: () -> Unit = {},
onMoreClick: () -> Unit = {}, onMoreClick: () -> Unit = {},
onMessageClick: () -> Unit = {}, onMessageClick: () -> Unit = {},
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
onStarClick: () -> Unit = {}, onStarClick: () -> Unit = {},
onCancelClick:()->Unit={}, onCancelClick: () -> Unit = {}
) { ) {
val age = remember(friendProfileData.birth_date) {
calculateAge(friendProfileData.birth_date)
val age = remember(friendProfileData.birthDate) {
calculateAge(friendProfileData.birthDate)
} }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.White) .background(Color.White)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
// Header Cover & Profile Avatar
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -160,44 +139,45 @@ fun FriendProfileScreen(
Image( Image(
painter = painterResource(id = R.drawable.gallarypermissionbg), painter = painterResource(id = R.drawable.gallarypermissionbg),
contentDescription = "cover photo", contentDescription = "cover photo",
modifier = Modifier.fillMaxWidth().height(200.dp) modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(bottomStart = 10.dp, bottomEnd = 20.dp)), .clip(RoundedCornerShape(bottomStart = 10.dp, bottomEnd = 20.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 25.dp), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 25.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton( IconButton(
onClick = onBackClick, onClick = onBackClick,
modifier = Modifier.size(20.dp).clip(CircleShape) modifier = Modifier.size(20.dp).clip(CircleShape)
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back button", contentDescription = "back button"
) )
} }
IconButton( IconButton(
onClick=onBackClick, onClick = onMoreClick,
modifier = Modifier.size(20.dp).clip(CircleShape) modifier = Modifier.size(20.dp).clip(CircleShape)
) { ) {
Icon( Icon(
imageVector = Icons.Default.MoreHoriz, imageVector = Icons.Default.MoreHoriz,
contentDescription = "back button", contentDescription = "more options"
) )
} }
} }
// Assuming 'profileImageUrl' is passed into your composable from FriendProfileResponse
AsyncImage( AsyncImage(
model = friendProfileData?.profileImageUrl, // Pass the String URL from FriendProfileResponse model = friendProfileData.profile_image_url,
contentDescription = "Profile Avatar", contentDescription = "Profile Avatar",
placeholder = painterResource(id = R.drawable.locationpermissionbg), // Placeholder while loading placeholder = painterResource(id = R.drawable.locationpermissionbg),
error = painterResource(id = R.drawable.locationpermissionbg), // Fallback if URL fails or is null error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier modifier = Modifier
.padding(start = 20.dp) .padding(start = 20.dp)
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
@ -210,18 +190,17 @@ fun FriendProfileScreen(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Column(modifier = Modifier // Profile Main Details
Column(
modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 20.dp) .padding(horizontal = 20.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
val displayName = friendProfileData.full_name ?: "User"
val displayName = friendProfileData.fullName ?: "User"
val nameAndAge = if (age != null) "$displayName, $age" else displayName val nameAndAge = if (age != null) "$displayName, $age" else displayName
Text( Text(
text = nameAndAge, text = nameAndAge,
@ -229,12 +208,6 @@ fun FriendProfileScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = textDark color = textDark
) )
Text(
text = "24",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = textDark
)
Surface( Surface(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = BadgePink color = BadgePink
@ -251,19 +224,19 @@ fun FriendProfileScreen(
modifier = Modifier.size(10.dp) modifier = Modifier.size(10.dp)
) )
Text( Text(
text = "98% Match", text = "${friendProfileData.county?: 0}% Match",
color = Color.White, color = Color.White,
fontSize = 10.sp, fontSize = 10.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
} }
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(vertical = 4.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.LocationOn, imageVector = Icons.Outlined.LocationOn,
@ -272,41 +245,22 @@ fun FriendProfileScreen(
modifier = Modifier.size(14.dp) modifier = Modifier.size(14.dp)
) )
Text( Text(
text = "2 miles away", text = "${friendProfileData.city ?: "Unknown"}, ${friendProfileData.country ?: ""}",
fontSize = 12.sp,
color = Color.Gray
)
Text(
text = "•",
fontSize = 12.sp,
color = Color.Gray
)
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(AccentPurple)
)
Text(
text = "Active 1 hour ago",
fontSize = 12.sp, fontSize = 12.sp,
color = Color.Gray color = Color.Gray
) )
} }
Surface( Surface(
onClick = { modifier = Modifier
// Handle bio click action here (e.g., expand bio or navigate) .fillMaxWidth()
}, .padding(vertical = 8.dp),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
color = Color(0xFFF9F9F9), color = Color(0xFFF9F9F9),
shadowElevation = 1.dp shadowElevation = 1.dp
) { ) {
val displayBio= friendProfileData.bio ?: ""
Text( Text(
text = displayBio, text = friendProfileData.bio ?: "",
fontSize = 13.sp, fontSize = 13.sp,
color = Color(0xFF4A4A4A), color = Color(0xFF4A4A4A),
modifier = Modifier.padding(12.dp), modifier = Modifier.padding(12.dp),
@ -315,6 +269,7 @@ fun FriendProfileScreen(
) )
} }
// Action Buttons Row
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -322,7 +277,6 @@ fun FriendProfileScreen(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Message Button (Gradient Background)
Button( Button(
onClick = onMessageClick, onClick = onMessageClick,
modifier = Modifier modifier = Modifier
@ -351,106 +305,288 @@ fun FriendProfileScreen(
) )
} }
// Heart Button
IconButton( IconButton(
onClick = { onClick = {
// Option 1: Invoke callback directly if handleSwipeAction is inside the parent screen
onLikeClick() onLikeClick()
handleLikeAndDislikeAction(context, "right", friendProfileData, swipeViewModel)
// Option 2: Call handleSwipeAction directly here
handleLikeAndDislikeAction(
context = context,
direction = "right", // Or "like", matching your API endpoint expected value
profile = friendProfileData, // Your MeetUserProfile instance
swipeViewModel = swipeViewModel
)
}, },
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background( .background(color = purpleButtonColor, shape = RoundedCornerShape(10.dp))
color = purpleButtonColor,
shape = RoundedCornerShape(10.dp)
)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Favorite, imageVector = Icons.Default.Favorite,
contentDescription = "Like", contentDescription = "Like",
tint = Color(0xFFE2C4FF), // Soft light purple icon color tint = Color(0xFFE2C4FF),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }
// Star Button
IconButton( IconButton(
onClick = { onClick = {
onStarClick() onStarClick()
handleLikeAndDislikeAction( handleLikeAndDislikeAction(context, "right", friendProfileData, swipeViewModel)
context = context,
direction = "right", // Or "like", matching your API endpoint expected value
profile = friendProfileData, // Your MeetUserProfile instance
swipeViewModel = swipeViewModel
)
}, },
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background( .background(color = purpleButtonColor, shape = RoundedCornerShape(10.dp))
color = purpleButtonColor,
shape = RoundedCornerShape(10.dp)
)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Star, imageVector = Icons.Default.Star,
contentDescription = "Favorite", contentDescription = "Favorite",
tint = Color(0xFFE2C4FF), // Soft light purple icon color tint = Color(0xFFE2C4FF),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }
IconButton( IconButton(
onClick = { onClick = {
onStarClick() onCancelClick()
handleLikeAndDislikeAction( handleLikeAndDislikeAction(context, "left", friendProfileData, swipeViewModel)
context = context,
direction = "right", // Or "like", matching your API endpoint expected value
profile = friendProfileData, // Your MeetUserProfile instance
swipeViewModel = swipeViewModel
)
}, },
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.background( .background(color = purpleButtonColor, shape = RoundedCornerShape(10.dp))
color = purpleButtonColor,
shape = RoundedCornerShape(10.dp)
)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Favorite", contentDescription = "Dislike",
tint = Color(0xFFE2C4FF), // Soft light purple icon color tint = Color(0xFFE2C4FF),
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }
}
Spacer(modifier = Modifier.height(16.dp))
// User Posts Grid Section below Action Row
Text(
text = "Posts",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = textDark,
modifier = Modifier.padding(bottom = 8.dp)
)
Spacer(modifier = Modifier.height(16.dp))
StaggeredFriendPostsGrid(posts = friendProfileData.posts)
}
} }
}
@Composable
fun StaggeredFriendPostsGrid(posts: List<FriendPost>) {
// Distribute posts into two columns
val columns = remember(posts) {
val col1 = mutableListOf<FriendPost>()
val col2 = mutableListOf<FriendPost>()
posts.forEachIndexed { index, post ->
if (index % 2 == 0) {
col1.add(post)
} else {
col2.add(post)
} }
// }
// LazyVerticalGrid( Pair(col1, col2)
// columns = GridCells.Fixed(3),
// contentPadding = PaddingValues(12.dp),
// horizontalArrangement = Arrangement.spacedBy(8.dp),
// verticalArrangement = Arrangement.spacedBy(8.dp),
// modifier = Modifier.fillMaxSize()
// ) {
// items(
// items = uiState.items,
// key = { post -> post.id }
// ) { item ->
// PostGridItem(item = item)
// }
// }
} }
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(3.dp)
) {
// Column 1
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
columns.first.forEachIndexed { idx, post ->
// Apply a pattern of aspect ratios to create the staggered effect
// E.g., Alternating tall and short items
val aspectRatio = if (idx % 2 == 0) 0.8f else 1.2f
MasonryPostItem(post = post, aspectRatio = aspectRatio)
}
}
// Column 2
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
columns.second.forEachIndexed { idx, post ->
// Apply a different pattern (or offset) to the second column
// E.g., Offsetting the alternating tall and short items
val aspectRatio = if (idx % 2 == 0) 1.2f else 0.8f
MasonryPostItem(post = post, aspectRatio = aspectRatio)
}
}
} }
}
@Composable
fun MasonryPostItem(post: FriendPost, aspectRatio: Float) {
val context = LocalContext.current // Get context to launch the activity
val primaryMedia = post.media.firstOrNull()?.url
val isImage = post.post_type.equals("image", ignoreCase = true) || !primaryMedia.isNullOrEmpty()
Card(
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(aspectRatio)
.clickable {
val intent = Intent(context, ShowPostingActivity::class.java).apply {
val postID =post.id
Log.d("Post_ID","$postID")
putExtra("Post_ID", postID)
}
context.startActivity(intent)
}
) {
Box(
modifier = Modifier
.fillMaxSize()
// .background(
// if (isImage) Color.DarkGray else getMasonryGradient(post.post_type)
// )
) {
if (isImage && !primaryMedia.isNullOrEmpty()) {
AsyncImage(
model = primaryMedia,
contentDescription = "Post Image",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
// Dark Gradient Scrim for text readability over images
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.65f)),
startY = 100f
)
)
)
// Image metadata overlay (Feeling or Status snippet)
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
if (!post.feeling.isNullOrBlank()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = post.feeling,
color = Color.White.copy(alpha = 0.9f),
fontSize = 8.sp,
fontWeight = FontWeight.Bold
)
}
}
if (!post.content.isNullOrBlank()) {
Text(
text = post.content,
color = Color.White,
fontSize = 10.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
} else {
// Text / Status / Vibe Card Design
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(3.dp)
) {
Icon(
imageVector = when (post.post_type.lowercase()) {
"music" -> Icons.Default.MusicNote
else -> Icons.Default.TextSnippet
},
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(10.dp)
)
Text(
text = post.post_type.uppercase(),
color = Color.White.copy(alpha = 0.9f),
fontSize = 7.sp,
fontWeight = FontWeight.ExtraBold
)
}
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Feeling tag if available
if (!post.feeling.isNullOrBlank()) {
Surface(
shape = RoundedCornerShape(6.dp),
color = Color.White.copy(alpha = 0.2f)
) {
Text(
text = "feeling ${post.feeling}",
color = Color.White,
fontSize = 7.sp,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
// Main Content / Status Line
Text(
text = post.content ?: "",
color = Color.White,
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
lineHeight = 14.sp
)
}
// Footer Likes
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(9.dp)
)
Text(
text = "${post.like_count}",
color = Color.White.copy(alpha = 0.8f),
fontSize = 8.sp
)
}
}
}
}
}
}
private fun handleLikeAndDislikeAction( private fun handleLikeAndDislikeAction(
context: Context, context: Context,
@ -463,8 +599,6 @@ private fun handleLikeAndDislikeAction(
swipeViewModel.sendSwipeResponse(context, swipeRequest) swipeViewModel.sendSwipeResponse(context, swipeRequest)
} }
//calculte age
private fun calculateAge(birthDateStr: String?): Int? { private fun calculateAge(birthDateStr: String?): Int? {
if (birthDateStr.isNullOrBlank()) return null if (birthDateStr.isNullOrBlank()) return null
return try { return try {
@ -483,28 +617,224 @@ fun FriendProfileScreenPreview() {
DattingAPPTheme { DattingAPPTheme {
val mockProfileResponse = FriendProfileResponse( val mockProfileResponse = FriendProfileResponse(
id = 101, id = 101,
fullName = "Elena Rostova", full_name = "Elena Rostova",
gender = "Female", gender = "Female",
birthDate = "1999-05-14", birth_date = "1999-05-14",
city = "San Francisco", city = "San Francisco",
county = "San Francisco County",
country = "USA", country = "USA",
relationshipGoal = "Long-term relationship", relationship_goal = "Long-term relationship",
bio = "Always looking for the best flat white in the city. Amateur photographer, professional dog petter.", bio = "Always looking for the best flat white in the city. Amateur photographer, professional dog petter.",
profileImageUrl = "https://via.placeholder.com/150", profile_image_url = "https://via.placeholder.com/150",
videoUrl = null, video_url = null,
compatibilityPercentage = 98, distance_km = 2.5,
media = null compatibility_percentage = 98,
media = emptyList(),
interests = listOf("Travel", "Photography", "Coffee"),
posts = emptyList() // Posts are handled by postUiState in your screen
)
val mockPosts = listOf(
PostResponse(
id = 1,
content = "Cupidatat fugiat voluptate",
postType = "image",
author = PostAuthor(
id = 101,
fullName = "Elena Rostova",
city = "San Francisco",
country = "USA",
compatibilityPercentage = 98
),
likeCount = 12,
commentCount = 2,
likedByMe = false,
media = listOf("https://via.placeholder.com/300"),
createdAt = "2026-10-09T03:19:49"
),
PostResponse(
id = 2,
content = "Museum hopping & iced matcha 🍵🎨",
postType = "vibe",
author = PostAuthor(
id = 101,
fullName = "Elena Rostova",
city = "San Francisco",
country = "USA",
compatibilityPercentage = 98
),
likeCount = 34,
commentCount = 5,
likedByMe = true,
media = emptyList(),
createdAt = "2026-10-10T02:00:00"
),
PostResponse(
id = 3,
content = "Friday night dancing 🔮✨",
postType = "music",
author = PostAuthor(
id = 101,
fullName = "Elena Rostova",
city = "San Francisco",
country = "USA",
compatibilityPercentage = 98
),
likeCount = 45,
commentCount = 8,
likedByMe = false,
media = emptyList(),
createdAt = "2026-10-11T05:00:00"
)
) )
FriendProfileScreen( FriendProfileScreen(
friendProfileData = mockProfileResponse, friendProfileData = mockProfileResponse,
context = LocalContext.current, context = LocalContext.current,
postUiState = PostUiState.Success(mockPosts),
onBackClick = {}, onBackClick = {},
onMoreClick = {}, onMoreClick = {},
onMessageClick = {}, onMessageClick = {},
onLikeClick = {}, onLikeClick = {},
onStarClick = {} onStarClick = {},
onCancelClick = {}
) )
} }
} }
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun StaggeredFriendPostsGridPreview() {
DattingAPPTheme {
val mockAuthor = FriendAuthor(
id = 1,
full_name = "Mock User",
city = "Mock City",
country = "Mock Country",
compatibility_percentage = 90
)
val mockPosts = listOf(
// Image Post - Aspect Ratio 0.75f (Tall)
FriendPost(
id = 1,
content = "Beautiful sunset!",
post_type = "image",
content_format = "plain",
feeling = "peaceful",
location = null,
author = mockAuthor,
like_count = 120,
comment_count = 15,
liked_by_me = false,
media = listOf(
FrienPostMedia(
id = 1,
media_type = "image",
mime_type = "image/jpeg",
size_bytes = 1024,
position = 0,
url = "https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyMDkyMnwwfDF8c2VhcmNofDF8fHN1bnNldHxlbnwwfHx8fDE2NzQwMzUyMzA&ixlib=rb-1.2.1&q=80&w=400"
)
),
created_at = "2023-10-27T10:00:00Z"
),
// Text Post - Aspect Ratio 1.0f (Square)
FriendPost(
id = 2,
content = "Thinking about Kotlin Coroutines...",
post_type = "text",
content_format = "plain",
feeling = "thoughtful",
location = null,
author = mockAuthor,
like_count = 45,
comment_count = 5,
liked_by_me = true,
media = emptyList(),
created_at = "2023-10-27T09:30:00Z"
),
// Music Post - Aspect Ratio 1.1f (Slightly Wide)
FriendPost(
id = 3,
content = "Now Playing: 'Bohemian Rhapsody' by Queen",
post_type = "music",
content_format = "plain",
feeling = "energetic",
location = null,
author = mockAuthor,
like_count = 89,
comment_count = 10,
liked_by_me = false,
media = emptyList(),
created_at = "2023-10-27T09:00:00Z"
),
// Image Post - Aspect Ratio 1.25f (Wide)
FriendPost(
id = 4,
content = "Coffee time!",
post_type = "image",
content_format = "plain",
feeling = "happy",
location = null,
author = mockAuthor,
like_count = 210,
comment_count = 25,
liked_by_me = false,
media = listOf(
FrienPostMedia(
id = 2,
media_type = "image",
mime_type = "image/png",
size_bytes = 2048,
position = 0,
url = "https://images.unsplash.com/photo-1509042239860-f550ce710b93?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyMDkyMnwwfDF8c2VhcmNofDF8fGNvZmZlZXxlbnwwfHx8fDE2NzQwMzUzMzA&ixlib=rb-1.2.1&q=80&w=400"
)
),
created_at = "2023-10-27T08:30:00Z"
),
// Vibe Post - Aspect Ratio 0.8f (Slightly Tall)
FriendPost(
id = 5,
content = "Just enjoying the quiet evening.",
post_type = "vibe",
content_format = "plain",
feeling = "calm",
location = null,
author = mockAuthor,
like_count = 60,
comment_count = 8,
liked_by_me = true,
media = emptyList(),
created_at = "2023-10-27T08:00:00Z"
),
// Image Post - Aspect Ratio 0.7f (Very Tall)
FriendPost(
id = 6,
content = "City lights",
post_type = "image",
content_format = "plain",
feeling = "amazed",
location = null,
author = mockAuthor,
like_count = 150,
comment_count = 18,
liked_by_me = false,
media = listOf(
FrienPostMedia(
id = 3,
media_type = "image",
mime_type = "image/jpeg",
size_bytes = 3072,
position = 0,
url = "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyMDkyMnwwfDF8c2VhcmNofDF8fGNpdHklMjBsaWdodHN8ZW58MHx8fHwxNjc0MDM1NDMw&ixlib=rb-1.2.1&q=80&w=400"
)
),
created_at = "2023-10-27T07:30:00Z"
)
)
StaggeredFriendPostsGrid(posts = mockPosts)
}
}

301
app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt

@ -0,0 +1,301 @@
package com.example.dattingapp
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.example.dattingapp.homes.networkandapi.LikedUiState
import com.example.dattingapp.homes.networkandapi.LikedViewModel
import com.example.dattingapp.homes.profile.LikedUserResponse
import com.example.dattingapp.ui.theme.DattingAPPTheme
import java.time.LocalDate
import java.time.Period
import java.time.format.DateTimeFormatter
import kotlin.jvm.java
class LikeANdMatchActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
DattingAPPTheme {
val likedViewModel: LikedViewModel = viewModel()
val context = LocalContext.current
// Collect state from ViewModel
val uiState by likedViewModel.likeUiState.collectAsState()
// Trigger network call once when screen loads
LaunchedEffect(Unit) {
likedViewModel.likedResponse(context)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
when (val state = uiState) {
is LikedUiState.Idle, is LikedUiState.Loading -> {
CircularProgressIndicator()
}
is LikedUiState.Success -> {
ProfileGrid(
context =context,
likedResponse = state.LikeData,
isLoading = false,
errorMessage = null
)
}
is LikedUiState.Error -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp)
) {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Medium
)
Button(onClick = { likedViewModel.likedResponse(context) }) {
Text("Retry")
}
}
}
}
}
}
}
}
}
}
// 1. Define ProfileCard FIRST so ProfileGrid can reference it without issue
@Composable
fun ProfileCard(context: Context, likedProfile: LikedUserResponse, modifier: Modifier = Modifier,
onNameClick: () -> Unit={},
) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
modifier = modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.clip(RoundedCornerShape(12.dp))
) {
// Use profile_image_url or fallback to the first media item
AsyncImage(
model = likedProfile.profile_image_url?.takeIf { it.isNotBlank() }
?: likedProfile.media.firstOrNull(),
contentDescription = likedProfile.full_name,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
// Gradient Overlay
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f)),
startY = 100f
)
)
)
// Match Badge
Surface(
shape = RoundedCornerShape(20.dp),
color = Color.White.copy(alpha = 0.9f),
modifier = Modifier
.padding(8.dp)
.align(Alignment.TopStart)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = Color(0xFF6C5CE7),
modifier = Modifier.size(12.dp)
)
Text(
text = "${likedProfile.compatibility_percentage}%",
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2D3436)
)
}
}
// Name, Age & Distance
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.clickable {
// Create an intent to launch your target activity (e.g., FriendProfileActivity)
val intent = Intent(context, FriendProfile::class.java).apply {
putExtra("USER_ID", likedProfile.id) // Pass the user ID here
}
context.startActivity(intent)
onNameClick() } // Makes just this text section clickable
.padding(8.dp)
){
val displayName = likedProfile.full_name.ifBlank { "User" }
val age = remember(likedProfile.birth_date) {
calculateAge(likedProfile.birth_date)
}
val nameAndAge = if (age != null) "$displayName, $age" else displayName
Text(
text = nameAndAge,
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = null,
tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(10.dp)
)
Text(
text = "${likedProfile.distance_km ?: 0.0} km",
color = Color.White.copy(alpha = 0.8f),
fontSize = 10.sp
)
}
}
}
}
}
}
// 2. Define ProfileGrid AFTER ProfileCard
@Composable
fun ProfileGrid(
context: Context,
likedResponse: List<LikedUserResponse>,
isLoading: Boolean,
errorMessage: String?,
onRetry: () -> Unit = {},
modifier: Modifier = Modifier
) {
if (likedResponse.isEmpty() && !isLoading) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No liked profiles found.", color = Color.Gray)
}
return
}
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier.fillMaxSize()
) {
items(likedResponse) { profile ->
ProfileCard(context,likedProfile = profile)
}
}
}
// Helper to calculate age from birth date string (e.g., "1999-05-15")
private fun calculateAge(birthDateStr: String?): Int? {
if (birthDateStr.isNullOrBlank()) return null
return try {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val birthDate = LocalDate.parse(birthDateStr.take(10), formatter)
Period.between(birthDate, LocalDate.now()).years
} catch (e: Exception) {
null
}
}
@Preview(showBackground = true)
@Composable
fun ProfileGridPreview() {
val sampleLikedUsers = listOf(
LikedUserResponse(
id = 55,
full_name = "Sarah Jenkins",
gender = "female",
birth_date = "1995-08-20",
city = "Shanghai",
county = "District A",
country = "China",
relationship_goal = "dating",
bio = "Coffee lover and traveler.",
profile_image_url = "",
video_url = null,
distance_km = 4.1,
compatibility_percentage = 92,
media = emptyList()
)
)
val context = LocalContext.current
DattingAPPTheme {
ProfileGrid(
context=context,
likedResponse = sampleLikedUsers,
isLoading = false,
errorMessage = null
)
}
}

252
app/src/main/java/com/example/dattingapp/LikeAndMatchScreen.kt

@ -1,252 +0,0 @@
package com.example.dattingapp
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.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
class LikeAndMatchScreen {
}
data class Profile(
val name: String,
val age: Int,
val distance: String,
val matchPercentage: Int,
val tags: List<String>,
val imageUrl: String,
val hasUnread: Boolean = false
)
@Composable
fun ProfileGrid(profiles: List<Profile>, modifier: Modifier = Modifier) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier.fillMaxSize()
) {
items(profiles) { profile ->
ProfileCard(profile = profile)
}
}
}
@Composable
fun ProfileCard(profile: Profile, modifier: Modifier = Modifier) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
modifier = modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Image with Overlays
Box(
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.clip(RoundedCornerShape(12.dp))
) {
AsyncImage(
model = profile.imageUrl,
contentDescription = profile.name,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
// Gradient Overlay for Text Readability
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f)),
startY = 100f
)
)
)
// Match Badge
Surface(
shape = RoundedCornerShape(20.dp),
color = Color.White.copy(alpha = 0.9f),
modifier = Modifier
.padding(8.dp)
.align(Alignment.TopStart)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = null,
tint = Color(0xFF6C5CE7),
modifier = Modifier.size(12.dp)
)
Text(
text = "${profile.matchPercentage}%",
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2D3436)
)
}
}
// Notification Dot
if (profile.hasUnread) {
Box(
modifier = Modifier
.padding(8.dp)
.size(8.dp)
.clip(CircleShape)
.background(Color(0xFF6C5CE7))
.align(Alignment.TopEnd)
)
}
// Name, Age & Distance
Column(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(8.dp)
) {
Text(
text = "${profile.name}, ${profile.age}",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = null,
tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(10.dp)
)
Text(
text = profile.distance,
color = Color.White.copy(alpha = 0.8f),
fontSize = 10.sp
)
}
}
}
// Tags Row
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth()
) {
profile.tags.take(2).forEach { tag ->
Surface(
shape = RoundedCornerShape(6.dp),
color = Color(0xFFF1F2F6),
modifier = Modifier.weight(1f)
) {
Text(
text = tag,
fontSize = 10.sp,
color = Color(0xFF636E72),
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp),
maxLines = 1
)
}
}
}
// Action Buttons
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(10.dp)
) {
IconButton(
onClick = { },
modifier = Modifier
.size(22.dp)
.background(Color(0xFFF8F9FA), CircleShape)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Pass",
tint = Color(0xFF636E72),
modifier = Modifier.size(16.dp)
)
}
IconButton(
onClick = { },
modifier = Modifier
.size(22.dp)
.background(Color(0xFFF1EFFE), CircleShape)
) {
Icon(
imageVector = Icons.Outlined.ChatBubbleOutline,
contentDescription = "Message",
tint = Color(0xFF6C5CE7),
modifier = Modifier.size(16.dp)
)
}
IconButton(
onClick = { },
modifier = Modifier
.size(22.dp)
.background(Color(0xFFD63031), CircleShape)
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Like",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ProfileGridPreview() {
val sampleProfiles = listOf(
Profile("Elena", 24, "2 miles away", 98, listOf("Photography", "Coffee"), "", true),
Profile("Sarah", 26, "4 miles away", 94, listOf("Travel", "Music"), ""),
Profile("Julian", 27, "3 miles away", 91, listOf("Music Prod", "Vinyl"), ""),
Profile("Maya", 25, "5 miles away", 89, listOf("Surfing", "Yoga"), "")
)
ProfileGrid(profiles = sampleProfiles)
}

447
app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt

@ -0,0 +1,447 @@
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.example.dattingapp.ui.theme.DattingAPPTheme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.unit.dp
import coil.compose.AsyncImage
import android.util.Log
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.dattingapp.dataclass.Author
import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.networkandapi.ShowPostUiState
import com.example.dattingapp.homes.networkandapi.ShowPostViewModel
import androidx.compose.material.icons.filled.Send
import androidx.compose.ui.tooling.preview.Preview
import com.example.dattingapp.dataclass.CommentResponse
import com.example.dattingapp.dataclass.Location
// Color definitions matching the design palette
val PrimaryDarkText = Color(0xFF1E1E1E)
val SecondaryGrayText = Color(0xFF757575)
val AccentPink = Color(0xFFD81B60)
val CommentBubbleBg = Color(0xFFF6F6F6)
class ShowPostingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val postId = intent.getIntExtra("Post_ID", 0)
Log.d("Post_ID", "$postId")
setContent {
DattingAPPTheme {
val context = LocalContext.current
val showPostViewModel: ShowPostViewModel = viewModel()
val uiState by showPostViewModel.showPostUiState.collectAsState()
val comments by showPostViewModel.commentsUiState.collectAsState()
// Trigger API calls when the screen launches
LaunchedEffect(postId) {
showPostViewModel.fetchPostById(context, postId)
showPostViewModel.fetchComments(context, postId)
}
Scaffold(
modifier = Modifier.fillMaxSize()
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
when (val state = uiState) {
is ShowPostUiState.Idle, is ShowPostUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is ShowPostUiState.Success -> {
// Full screen scrollable container
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(bottom = 24.dp)
) {
PostCard(
post = state.post,
comments = comments,
onAddComment = { commentText ->
val currentUserName = state.post.author?.full_name
showPostViewModel.addCommentLocallyAndServer(
context = context,
postId = postId,
content = commentText,
currentUserName = currentUserName
)
}
)
}
}
is ShowPostUiState.Error -> {
Text(
text = state.message,
color = Color.Red,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
}
}
}
}
@Composable
fun PostCard(
post: ShowPostResponse,
comments: List<CommentResponse>,
onAddComment: (String) -> Unit = {},
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
// --- 1. Author Header ---
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(text = post.author?.full_name?.take(1) ?: "U")
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
val authorName = post.author?.full_name ?: "Anonymous"
val feelingText = post.feeling ?: ""
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("$authorName ")
}
if (feelingText.isNotEmpty()) {
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) {
append(" — feeling ")
}
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(feelingText)
}
}
},
fontSize = 16.sp
)
}
if (!post.location?.name.isNullOrEmpty()) {
Text(
text = post.location?.name ?: "",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
}
// --- 2. Post Text Content ---
if (!post.content.isNullOrEmpty()) {
Text(
text = post.content,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
// --- 3. Media Carousel (Images) ---
if (post.media.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(350.dp)
.padding(vertical = 4.dp)
) {
AsyncImage(
model = post.media.first().url,
contentDescription = "Post Media Image",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
}
// --- 4. Interaction Buttons ---
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { /* Handle like action */ }) {
Icon(
imageVector = if (post.likedByMe) Icons.Filled.Favorite else Icons.Default.FavoriteBorder,
contentDescription = "Like",
tint = if (post.likedByMe) Color.Red else Color.Black
)
}
Text(text = "${post.likeCount}")
Spacer(modifier = Modifier.width(16.dp))
IconButton(onClick = { /* Handle comments */ }) {
Icon(
imageVector = Icons.Outlined.ChatBubbleOutline,
contentDescription = "Comments"
)
}
Text(text = "${post.commentCount}")
}
// --- 5. Comment Section UI ---
CommentSection(
comments = comments,
onAddCommentClick = onAddComment
)
}
}
@Composable
fun CommentSection(
comments: List<CommentResponse>,
onAddCommentClick: (String) -> Unit
) {
var newCommentText by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Text(
text = "Comments",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
// List of fetched comments
comments.reversed().forEach { comment ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(
text = comment.author?.full_name?.take(1) ?: "U",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier
.weight(1f)
.background(CommentBubbleBg, shape = RoundedCornerShape(12.dp))
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Text(
text = comment.author?.full_name ?: "Anonymous",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = comment.content,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
// Bottom Comment Input Field Bar
Row(
modifier = Modifier
.fillMaxWidth()
.background(CommentBubbleBg, shape = RoundedCornerShape(24.dp))
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
value = newCommentText,
onValueChange = { newCommentText = it },
modifier = Modifier
.weight(1f)
.padding(vertical = 8.dp),
decorationBox = { innerTextField ->
if (newCommentText.isEmpty()) {
Text(
text = "Add a comment...",
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium
)
}
innerTextField()
}
)
IconButton(
onClick = {
if (newCommentText.isNotBlank()) {
onAddCommentClick(newCommentText)
newCommentText = ""
}
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Send,
contentDescription = "Send Comment",
tint = AccentPink,
modifier = Modifier.size(16.dp)
)
}
}
}
}
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
@Composable
fun ShowPostScreenPreview() {
DattingAPPTheme {
// Mock sample data mirroring your API schema
val mockPost = ShowPostResponse(
id = 21,
content = "Exploring the city and enjoying some coffee! ☕🏙️ #Android #JetpackCompose",
postType = "image",
contentFormat = "markdown",
feeling = "happy 😊",
location = Location(
name = "Chiattagong",
latitude = 27.9671,
longitude = -175.0599
),
author = Author(
id = 49,
full_name = "Alex Rivera",
city = "Shanghai",
country = "China",
compatibilityPercentage = 95
),
likeCount = 12,
commentCount = 2,
likedByMe = true,
media = listOf(
com.example.dattingapp.dataclass.MediaItem(
id = 5,
mediaType = "image",
mimeType = "image/png",
sizeBytes = 199619,
position = 0,
url = "https://images.unsplash.com/photo-1541167760496-1628856ab772"
)
),
createdAt = "2026-10-13T06:06:15"
)
// Mock comments list
val mockComments = listOf(
CommentResponse(
id = 1,
postId = 21,
content = "This spot looks amazing! Enjoy your day! ☕",
author = Author(
id = 54,
full_name = "Sarah",
city = "Shanghai",
country = "China",
compatibilityPercentage = 100
),
createdAt = "2026-10-13T07:45:15"
),
CommentResponse(
id = 2,
postId = 21,
content = "What a wonderful view, I love it!",
author = Author(
id = 55,
full_name = "David",
city = "Beijing",
country = "China",
compatibilityPercentage = 90
),
createdAt = "2026-10-13T08:04:48"
)
)
// Full screen container matching activity layout
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
PostCard(
post = mockPost,
comments = mockComments,
onAddComment = { newComment ->
// Preview action handler
}
)
}
}
}

61
app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt

@ -0,0 +1,61 @@
package com.example.dattingapp.dataclass
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ShowPostResponse(
val id: Int,
val content: String?,
@SerialName("post_type") val postType: String?,
@SerialName("content_format") val contentFormat: String?,
val feeling: String?,
@SerialName("location") val location: Location?,
@SerialName("author") val author: Author? = null,
@SerialName("like_count") val likeCount: Int = 0,
@SerialName("comment_count") val commentCount: Int = 0,
@SerialName("liked_by_me") val likedByMe: Boolean = false,
val media: List<MediaItem> = emptyList(),
@SerialName("created_at") val createdAt: String?
)
@Serializable
data class Location(
val name: String?,
val latitude: Double?,
val longitude: Double?
)
@Serializable
data class Author(
val id: Int,
val full_name: String?,
val city: String?,
val country: String?,
@SerialName("compatibility_percentage") val compatibilityPercentage: Int? = 0
)
@Serializable
data class MediaItem(
val id: Long,
@SerialName("media_type") val mediaType: String, // "image" or "video"
@SerialName("mime_type") val mimeType: String?,
@SerialName("size_bytes") val sizeBytes: Long?,
val position: Int?,
val url: String
)
@Serializable
data class CreateCommentRequest(
val content: String
)
@Serializable
data class CommentResponse(
val id: Int,
@SerialName("post_id") val postId: Int,
val content: String,
val author: Author?,
@SerialName("created_at") val createdAt: String?
)

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

@ -1,6 +1,7 @@
package com.example.dattingapp.homes package com.example.dattingapp.homes
import com.example.dattingapp.homes.networkandapi.Location
import com.example.dattingapp.homes.networkandapi.CreatePostRequest
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
@ -46,13 +47,27 @@ import androidx.media3.ui.PlayerView
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
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 java.io.File
import com.example.dattingapp.ui.theme.LightPinkBg import com.example.dattingapp.ui.theme.LightPinkBg
import com.example.dattingapp.ui.theme.LightPurpleBg 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 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.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 com.example.dattingapp.homes.networkandapi.PostMediaViewModel
import java.io.File
// Sealed class representing selectable media types (Photo or Video) // Sealed class representing selectable media types (Photo or Video)
sealed class MediaContentItem(val uri: Uri) { sealed class MediaContentItem(val uri: Uri) {
@ -72,6 +87,14 @@ fun CreateVibePostScreen(
// Holds selected media items (photos, videos, or both) // 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) {
when {
selectedMediaList.any { it is MediaContentItem.Video } -> "video"
selectedMediaList.any { it is MediaContentItem.Photo } -> "image"
else -> "text"
}
}
var tempCameraImageUri by remember { mutableStateOf<Uri?>(null) } var tempCameraImageUri by remember { mutableStateOf<Uri?>(null) }
var tempCameraVideoUri by remember { mutableStateOf<Uri?>(null) } var tempCameraVideoUri by remember { mutableStateOf<Uri?>(null) }
@ -80,6 +103,9 @@ 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) }
// Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously) // Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously)
val galleryLauncher = rememberLauncherForActivityResult( val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia() contract = ActivityResultContracts.PickMultipleVisualMedia()
@ -124,6 +150,69 @@ 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)
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)
}
if (location == null && isNetworkEnabled) {
location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
}
if (location != null) {
latitude = location.latitude
longitude = location.longitude
showLocationTag = true
} else {
Toast.makeText(context, "Unable to get location. Ensure GPS is turned on.", Toast.LENGTH_SHORT).show()
}
} catch (e: SecurityException) {
Toast.makeText(context, "Location permission not granted", Toast.LENGTH_SHORT).show()
}
}
// 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()
}
}
// 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)
}
}
Scaffold( Scaffold(
bottomBar = { bottomBar = {
Row( Row(
@ -152,7 +241,8 @@ fun CreateVibePostScreen(
VibeOptionChip( VibeOptionChip(
icon = Icons.Outlined.MyLocation, icon = Icons.Outlined.MyLocation,
backgroundColor = LightBlueBg, backgroundColor = LightBlueBg,
contentColor = Color(0xFF1E88E5) contentColor = Color(0xFF1E88E5),
onClick = { onLocationChipClick() }
) )
} }
item { item {
@ -174,9 +264,85 @@ 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 // 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(
onClick = onShareClick, onClick = {
// 1. Build the Location object if the tag is active and coordinates exist
val postLocation = if (showLocationTag && latitude != null && longitude != null) {
Location(
name = "Current Location", // Or your geocoded address string
latitude = latitude,
longitude = longitude
)
} 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
location = postLocation
)
// Simply trigger the ViewModel function inside onClick!
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),
@ -293,7 +459,7 @@ fun CreateVibePostScreen(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "Blue Bottle Coffee, Soho NYC", text = "Lat: $latitude, Lng: $longitude",
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 13.sp fontSize = 13.sp
) )
@ -676,6 +842,9 @@ fun FeelingPickerBottomSheet(
} }
} }
@Preview( @Preview(
name = "Create Vibe Post Screen", name = "Create Vibe Post Screen",
showBackground = true, showBackground = true,

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

@ -33,21 +33,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.dattingapp.homes.networkandapi.HomeNetworkModule
import com.example.dattingapp.homes.networkandapi.MeetUiState import com.example.dattingapp.homes.networkandapi.MeetUiState
import com.example.dattingapp.homes.networkandapi.MeetUserProfile
import com.example.dattingapp.homes.networkandapi.MeetViewModel import com.example.dattingapp.homes.networkandapi.MeetViewModel
import com.example.dattingapp.homes.networkandapi.MyProfileUiState import com.example.dattingapp.homes.networkandapi.ProfileViewModel
import com.example.dattingapp.homes.networkandapi.MyProfileViewModel import com.example.dattingapp.homes.networkandapi.ProfileUiState
import com.example.dattingapp.permission.PermissionHelper import com.example.dattingapp.permission.PermissionHelper
import com.example.dattingapp.ui.theme.DattingAPPTheme import com.example.dattingapp.ui.theme.DattingAPPTheme
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.coroutines.cancellation.CancellationException
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
class HomeActivity : ComponentActivity() { class HomeActivity : ComponentActivity() {
private lateinit var permissionHelper: PermissionHelper private lateinit var permissionHelper: PermissionHelper
@ -109,8 +102,13 @@ class HomeActivity : ComponentActivity() {
if (currentType == PermissionHelper.PermissionType.OVERLAY) { if (currentType == PermissionHelper.PermissionType.OVERLAY) {
permissionHelper.requestOverlayPermission { isGranted -> permissionHelper.requestOverlayPermission { isGranted ->
val msg = if (isGranted) "Overlay Granted" else "Overlay Denied" if (!isGranted) {
Toast.makeText(this@HomeActivity, msg, Toast.LENGTH_SHORT).show() Toast.makeText(
this@HomeActivity,
"Overlay Denied",
Toast.LENGTH_SHORT
).show()
}
} }
} else { } else {
permissionHelper.requestPermissions(currentType) { isGranted -> permissionHelper.requestPermissions(currentType) { isGranted ->
@ -325,40 +323,41 @@ fun HomeScreen() {
// ========================================= // =========================================
4 -> { 4 -> {
// 1. Obtain instance of ViewModel and LocalContext // 1. Obtain instance of ViewModel and LocalContext
val viewModel: MyProfileViewModel = viewModel() val profileViewModel: ProfileViewModel = viewModel()
val context = LocalContext.current val context = LocalContext.current
// 2. Collect state from ViewModel // 2. Collect state from ViewModel
val uiState by viewModel.myProfileUiState.collectAsState() val uiState by profileViewModel.profileUiState.collectAsState()
// 3. Trigger network call once when screen loads // 3. Trigger network call once when screen loads
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.meetUserResponse(context) profileViewModel.profileResponse(context)
} }
// 4. Map UI state to Composable parameters // 4. Map UI state to Composable parameters
when (val state = uiState) { when (val state = uiState) {
is MyProfileUiState.Loading, MyProfileUiState.Idle -> { is ProfileUiState.Loading, ProfileUiState.Idle -> {
MyProfileScreen( MyProfileScreen(
profile = null, profileResponse = null,
isLoading = true, isLoading = true,
errorMessage = null, errorMessage = null,
// onRetry = { viewModel.meetUserResponse(context) } // onRetry = { viewModel.meetUserResponse(context) }
) )
} }
is MyProfileUiState.Success -> { is ProfileUiState.Success -> {
MyProfileScreen( MyProfileScreen(
profile = state.myProfile, // or state.response depending on your data class naming profileResponse = state.profileData, // or state.response depending on your data class naming
isLoading = false, isLoading = false,
errorMessage = null, errorMessage = null,
) )
} }
is MyProfileUiState.Error -> { is ProfileUiState.Error -> {
MyProfileScreen( MyProfileScreen(
profile = null, profileResponse = null,
isLoading = false, isLoading = false,
errorMessage = state.message, errorMessage = state.message,
onRetry = { viewModel.meetUserResponse(context) } onRetry = { profileViewModel.profileResponse(context) }
) )
} }
} }

105
app/src/main/java/com/example/dattingapp/homes/MyProfile.kt

@ -1,10 +1,5 @@
package com.example.dattingapp.homes package com.example.dattingapp.homes
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -16,8 +11,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.GridView
import androidx.compose.material.icons.outlined.PersonPin
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -34,12 +27,18 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.dattingapp.R import com.example.dattingapp.R
import com.example.dattingapp.homes.networkandapi.MyProfileResponse import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.ui.theme.BrandMagenta import com.example.dattingapp.ui.theme.BrandMagenta
import com.example.dattingapp.ui.theme.BrandPurple import com.example.dattingapp.ui.theme.BrandPurple
import com.example.dattingapp.ui.theme.DattingAPPTheme import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.textDark import com.example.dattingapp.ui.theme.textDark
import com.example.dattingapp.ui.theme.TextMuted import com.example.dattingapp.ui.theme.TextMuted
import com.example.dattingapp.homes.profile.ProfileData
import com.example.dattingapp.LikeANdMatchActivity
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat.startActivity
// ============================================================ // ============================================================
// MY PROFILE SCREEN // MY PROFILE SCREEN
@ -47,19 +46,23 @@ import com.example.dattingapp.ui.theme.TextMuted
@Composable @Composable
fun MyProfileScreen( fun MyProfileScreen(
profile: MyProfileResponse?, // Change from MyProfileData? to MyProfileResponse? profileResponse : UserResponse?, // Change from MyProfileData? to MyProfileResponse?
isLoading: Boolean, isLoading: Boolean,
errorMessage: String?, errorMessage: String?,
onRetry: () -> Unit = {}, onRetry: () -> Unit = {},
onEditClick: () -> Unit = {}, onEditClick: () -> Unit = {},
onUpgradeClick: () -> Unit = {}, onUpgradeClick: () -> Unit = {},
onSettingsClick: () -> Unit = {} onSettingsClick: () -> Unit = {},
onLikesClick: () -> Unit={},
onLikedClick: () -> Unit={},
onMatchClick: () -> Unit={}
) { ) {
// ======================================================== // ========================================================
// FULL SCREEN SCROLL // FULL SCREEN SCROLL
// ======================================================== // ========================================================
val context = LocalContext.current
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) { modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) {
@ -113,7 +116,7 @@ fun MyProfileScreen(
AsyncImage( AsyncImage(
// Access nested inner 'profile' first, then 'profileImageUrl' // Access nested inner 'profile' first, then 'profileImageUrl'
model = profile?.profile?.profileImageUrl, model = profileResponse?.profile?.profile_image_url,
contentDescription = "Profile Picture", contentDescription = "Profile Picture",
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
placeholder = painterResource(id = R.drawable.locationpermissionbg), placeholder = painterResource(id = R.drawable.locationpermissionbg),
@ -164,7 +167,7 @@ fun MyProfileScreen(
) { ) {
Text( Text(
text = profile?.profile?.fullName?:"", text = profileResponse?.profile?.full_name?:"",
fontSize = 22.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = BrandMagenta color = BrandMagenta
@ -207,7 +210,7 @@ fun MyProfileScreen(
Text( Text(
text = profile?.profile?.bio?:"", text = profileResponse?.profile?.bio?:"",
fontSize = 13.sp, fontSize = 13.sp,
color = TextMuted, color = TextMuted,
lineHeight = 18.sp lineHeight = 18.sp
@ -296,29 +299,36 @@ fun MyProfileScreen(
) { ) {
StatCard( StatCard(
number = "24", number = profileResponse?.liked_users_count?.toString() ?: "0",
label = "Likes", label = "Likes",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
onClick = {
val intent = Intent(context, LikeANdMatchActivity::class.java)
context.startActivity(intent)
onLikesClick()
}
) )
StatCard( StatCard(
number = "12", number = profileResponse?.liked_by_users_count?.toString() ?: "0",
label = "Liked", label = "Liked",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
onClick = { /* Optional: Handle Matches click */ }
) )
// StatCard(
// number = "Top",
// label = "Match",
// numberColor = BrandMagenta,
// modifier = Modifier.weight(1f)
// )
StatCard( StatCard(
number = "Top", number = profileResponse?.matches_count?.toString() ?: "0",
label = "Match",
numberColor = BrandMagenta,
modifier = Modifier.weight(1f)
)
StatCard(
number = "8",
label = "Matches", label = "Matches",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
onClick = { /* Optional: Handle Matches click */ }
) )
} }
Spacer( Spacer(
@ -383,12 +393,13 @@ private fun StatCard(
number: String, number: String,
label: String, label: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
numberColor: Color = textDark numberColor: Color = textDark,
onClick:()->Unit
) { ) {
Card( Card(
modifier = modifier, modifier = modifier.clickable { onClick() },
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
@ -423,39 +434,47 @@ private fun StatCard(
} }
} }
// ============================================================ // ============================================================
// PREVIEW // PREVIEW
// ============================================================ // ============================================================
// 1. Success State Preview (With Mock User Profile Data)
@Preview(showBackground = true, showSystemUi = true) @Preview(showBackground = true, showSystemUi = true)
@Composable @Composable
fun MyProfileScreenSuccessPreview() { fun MyProfileScreenSuccessPreview() {
DattingAPPTheme { DattingAPPTheme {
// Create dummy profile response matching your backend structure // Mock UserResponse matching your backend model hierarchy
val mockProfileResponse = MyProfileResponse( val mockUserResponse = UserResponse(
id = 1, id = 1,
emailOrPhone = "alex.dev@example.com", email_or_phone = "alex.dev@example.com",
status = "active", status = "active",
profile = com.example.dattingapp.homes.networkandapi.UserProfileDetails( profile = ProfileData(
fullName = "Alex", full_name = "Alex Morgan",
gender = "Male", gender = "Male",
birth_date = "1995-05-15",
city = "New York", city = "New York",
county = "Manhattan",
country = "USA", country = "USA",
relationship_goal = "Long-term partnership",
bio = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!", bio = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!",
profileImageUrl = "https://picsum.photos/300/300", // Sample remote URL profile_image_url = "https://picsum.photos/300/300",
videoUrl = null video_url = null,
interests = listOf("Hiking", "Cooking", "Photography")
), ),
createdAt = "2026-01-01" created_at = "2026-01-01",
posts = emptyList(),
liked_users = emptyList(),
liked_by_users = emptyList(),
matches = emptyList(),
liked_users_count = 24,
liked_by_users_count = 12,
matches_count = 8
) )
MyProfileScreen( MyProfileScreen(
profile = mockProfileResponse, profileResponse = mockUserResponse,
isLoading = false, isLoading = false,
errorMessage = null, errorMessage = null
onRetry = {},
onEditClick = {},
onUpgradeClick = {},
onSettingsClick = {}
) )
} }
} }

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

@ -1,11 +1,20 @@
package com.example.dattingapp.homes.networkandapi package com.example.dattingapp.homes.networkandapi
import com.example.dattingapp.dataclass.CommentResponse
import com.example.dattingapp.dataclass.CreateCommentRequest
import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.profile.FriendProfileResponse
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
import okhttp3.MultipartBody
import retrofit2.http.Multipart
import retrofit2.http.Part
import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.homes.profile.LikedUserResponse
interface HomeApiService { interface HomeApiService {
@ -15,7 +24,7 @@ interface HomeApiService {
): Response<List<MeetUserProfile>> ): Response<List<MeetUserProfile>>
@GET("api/v1/users/me") @GET("api/v1/users/me")
suspend fun getCurrentUserProfile(): Response<MyProfileResponse> suspend fun getCurrentUserProfile(): Response<UserResponse>
@POST("api/v1/meet/swipes") @POST("api/v1/meet/swipes")
suspend fun sendSwipe( suspend fun sendSwipe(
@ -27,5 +36,55 @@ interface HomeApiService {
@Path("user_id") userId: Int @Path("user_id") userId: Int
): Response<FriendProfileResponse> ): Response<FriendProfileResponse>
@GET("api/v1/users/{user_id}/posts")
suspend fun getUserPosts(
@Path("user_id") userId: Int,
@Query("limit") limit: Int = 10,
@Query("offset") offset: Int = 0
): List<PostResponse>
@GET("posts/{id}")
suspend fun getCreatePost(
@Path("id") postId: Int
): Response<PostResponse>
@POST("/api/v1/explore/posts")
suspend fun createPost(
@Body request: CreatePostRequest
): Response<CreatePostResponse>
@Multipart
@POST("api/v1/explore/posts/{post_id}/media")
suspend fun uploadPostMedia(
@Path("post_id") postId: Int,
@Query("position") position: Int,
@Part mediaFile: MultipartBody.Part
): Response<PostMediaResponse>
@GET("api/v1/users/me")
suspend fun getMyProfile(
): Response<UserResponse>
@GET("api/v1/users/me/liked")
suspend fun getLikedUsers(
): Response<List<LikedUserResponse>>
@GET("api/v1/explore/posts/{post_id}")
suspend fun getPostById(
@Path("post_id") postId: Int
): Response<ShowPostResponse>
@POST("api/v1/explore/posts/{post_id}/comments")
suspend fun createComment(
@Path("post_id") postId: Int,
@Body request: CreateCommentRequest
): Response<CommentResponse>
@GET("api/v1/explore/posts/{post_id}/comments")
suspend fun getPostComments(
@Path("post_id") postId: Int
): Response<List<CommentResponse>>
} }

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

@ -57,26 +57,23 @@ data class MeetUserProfile(
/** /**
* Standard API wrapper format for MyProfile * Standard API wrapper format for MyProfile
*/ */
data class MyProfileResponse( //data class MyProfileResponse(
@SerializedName("id") val id: Int?, // @SerializedName("id") val id: Int?,
@SerializedName("email_or_phone") val emailOrPhone: String?, // @SerializedName("email_or_phone") val emailOrPhone: String?,
@SerializedName("status") val status: String?, // @SerializedName("status") val status: String?,
@SerializedName("profile") val profile: UserProfileDetails?, // Nested profile details // @SerializedName("profile") val profile: UserProfileDetails?, // Nested profile details
@SerializedName("created_at") val createdAt: String? // @SerializedName("created_at") val createdAt: String?
) //)
//
data class UserProfileDetails( //data class UserProfileDetails(
@SerializedName("full_name") val fullName: String?, // @SerializedName("full_name") val fullName: String?,
@SerializedName("gender") val gender: String?, // @SerializedName("gender") val gender: String?,
@SerializedName("city") val city: String?, // @SerializedName("city") val city: String?,
@SerializedName("country") val country: String?, // @SerializedName("country") val country: String?,
@SerializedName("bio") val bio: String?, // @SerializedName("bio") val bio: String?,
@SerializedName("profile_image_url") val profileImageUrl: String?, // Resolved field // @SerializedName("profile_image_url") val profileImageUrl: String?, // Resolved field
@SerializedName("video_url") val videoUrl: String? // @SerializedName("video_url") val videoUrl: String?
) //)
data class MediaItem( data class MediaItem(
@SerializedName("id") @SerializedName("id")
@ -147,33 +144,75 @@ data class SwipeResponse(
val matchId: Long? val matchId: Long?
) )
//this is for fetch Post list of a User
data class PostResponse(
val id: Int,
val content: String,
@SerializedName("post_type") val postType: String,
val author: PostAuthor,
@SerializedName("like_count") val likeCount: Int,
@SerializedName("comment_count") val commentCount: Int,
@SerializedName("liked_by_me") val likedByMe: Boolean,
val media: List<String>,
@SerializedName("created_at") val createdAt: String
)
data class PostAuthor(
val id: Int,
@SerializedName("full_name") val fullName: String,
val city: String,
val country: String?,
@SerializedName("compatibility_percentage") val compatibilityPercentage: Int
)
// data class friend profile //this is for create post
data class Location(
val name: String?,
val latitude: Double?,
val longitude: Double?
)
data class FriendProfileMedia( data class CreatePostAuthor(
val id: Int, val id: Int,
@SerializedName("media_type") val full_name: String,
val mediaType: String?, val city: String,
val url: String? val country: String,
val compatibility_percentage: Int
)
// 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 FriendProfileResponse(
// Represents the full post response (Bottom Image)
data class CreatePostResponse(
val id: Int, val id: Int,
@SerializedName("full_name") val content: String,
val fullName: String?, val post_type: String,
val gender: String?, val content_format: String,
@SerializedName("birth_date") val feeling: String?,
val birthDate: String?, val location: Location?,
val city: String?, val author: CreatePostAuthor,
val country: String?, val like_count: Int,
@SerializedName("relationship_goal") val comment_count: Int,
val relationshipGoal: String?, val liked_by_me: Boolean,
val bio: String?, val media: List<Any>, // Change to specific model if media items have structure
@SerializedName("profile_image_url") val created_at: String
val profileImageUrl: String?, )
@SerializedName("video_url")
val videoUrl: String?,
@SerializedName("compatibility_percentage")
val compatibilityPercentage: Int?, data class PostMediaResponse(
val media: List<FriendProfileMedia>? val id: Int,
val media_type: String,
val mime_type: String,
val size_bytes: Int,
val position: Int,
val url: String
) )

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

@ -1,5 +1,7 @@
package com.example.dattingapp.homes.networkandapi package com.example.dattingapp.homes.networkandapi
import com.example.dattingapp.homes.profile.UserResponse
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -8,6 +10,20 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException
import android.net.Uri
import com.example.dattingapp.dataclass.Author
import com.example.dattingapp.dataclass.CommentResponse
import com.example.dattingapp.dataclass.CreateCommentRequest
import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.profile.FriendProfileResponse
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.io.FileOutputStream
import java.lang.Exception
import com.example.dattingapp.homes.profile.LikedUserResponse
sealed class MeetUiState { sealed class MeetUiState {
object Idle : MeetUiState() object Idle : MeetUiState()
@ -95,66 +111,6 @@ class MeetViewModel : ViewModel() {
// Myprofle ViewModel--------------------------------------------------------------------------------------- // Myprofle ViewModel---------------------------------------------------------------------------------------
sealed interface MyProfileUiState {
object Idle : MyProfileUiState
object Loading : MyProfileUiState
data class Success(val myProfile: MyProfileResponse) : MyProfileUiState // Ensure this is MyProfileData, not MyProfileResponse
data class Error(val message: String) : MyProfileUiState
}
class MyProfileViewModel : ViewModel() {
private val _myProfileUiState = MutableStateFlow<MyProfileUiState>(MyProfileUiState.Idle)
val myProfileUiState: StateFlow<MyProfileUiState> = _myProfileUiState.asStateFlow()
fun meetUserResponse(context: Context) {
if (_myProfileUiState.value is MyProfileUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_myProfileUiState.value = MyProfileUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_myProfileUiState.value = MyProfileUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
val response = HomeNetworkModule
.getApiService(appContext)
.getCurrentUserProfile()
if (!response.isSuccessful) {
response.errorBody()?.close()
val message = when (response.code()) {
401 -> "Authentication failed. Please log in again."
403 -> "The server refused access to these profiles."
404 -> "Meet endpoint was not found."
else -> "Request failed: HTTP ${response.code()}."
}
_myProfileUiState.value = MyProfileUiState.Error(message)
return@launch
}
// FIX: Use response.body() directly as profileData
val profileData = response.body()
Log.d("my profiler", "my data $profileData")
if (profileData != null) {
_myProfileUiState.value = MyProfileUiState.Success(profileData)
} else {
_myProfileUiState.value = MyProfileUiState.Error("Profile data is empty.")
}
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_myProfileUiState.value = MyProfileUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
}
@ -289,3 +245,463 @@ class FriendProfileViewModel : ViewModel() {
} }
} }
// this is for fetch user post
sealed interface PostUiState {
object Idle : PostUiState
object Loading : PostUiState
data class Success(val postData: List<PostResponse>) : PostUiState // Changed to List<PostResponse>
data class Error(val message: String) : PostUiState
}
class PostViewModel : ViewModel() {
private val _postUiState = MutableStateFlow<PostUiState>(PostUiState.Idle)
val postUiState: StateFlow<PostUiState> = _postUiState.asStateFlow()
fun fetchUserPost(context: Context, userId: Int) {
if (_postUiState.value is PostUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_postUiState.value = PostUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_postUiState.value = PostUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
// Returns List<PostResponse> directly
val posts = HomeNetworkModule
.getApiService(appContext)
.getUserPosts(
userId = userId,
limit = 20,
offset = 20
)
if (posts.isNotEmpty()) {
_postUiState.value = PostUiState.Success(posts)
} else {
_postUiState.value = PostUiState.Error("No posts found for this user.")
}
} catch (e: HttpException) {
// Handle HTTP errors via HttpException
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()}."
}
_postUiState.value = PostUiState.Error(message)
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_postUiState.value = PostUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
}
//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 Error(val message: String) : CreatePostUiState
}
class CreatePostViewModel : ViewModel() {
private val _cretePostUiState = MutableStateFlow<CreatePostUiState>(CreatePostUiState.Idle)
val cretePpostUiState: StateFlow<CreatePostUiState> = _cretePostUiState.asStateFlow()
fun createPost(context: Context, postInfo: CreatePostRequest) {
if (_cretePostUiState.value is CreatePostUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_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.")
return@launch
}
try {
// Returns CreatePostResponse directly
val createdPost = HomeNetworkModule
.getApiService(appContext)
.createPost(postInfo)
Log.d("CreatePostDebug", "Response Code: ${createdPost.code()}")
if (createdPost.isSuccessful && createdPost.body() != null) {
_cretePostUiState.value = CreatePostUiState.Success(createdPost.body()!!) // Use .body() here!
} else {
_cretePostUiState.value = CreatePostUiState.Error("No post data received from the server.")
}
} 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)
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_cretePostUiState.value = CreatePostUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
}
sealed interface PostMediaUiState {
object Idle : PostMediaUiState
object Loading : PostMediaUiState
data class Success(val postData: PostMediaResponse) : PostMediaUiState // Changed to List<PostResponse>
data class Error(val message: String) : PostMediaUiState
}
class PostMediaViewModel : ViewModel() {
private val _postMediaUiState = MutableStateFlow<PostMediaUiState>(PostMediaUiState.Idle)
val cretePpostUiState: StateFlow<PostMediaUiState> = _postMediaUiState.asStateFlow()
fun poostMedia(context: Context, postId: Int, position: Int, uri: Uri) {
if (_postMediaUiState.value is PostMediaUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_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.")
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.")
return@launch
}
val response = HomeNetworkModule
.getApiService(appContext)
.uploadPostMedia(postId = postId, position = position, mediaFile = multipartPart)
Log.d("CreatePostDebug", "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.")
}
} catch (e: HttpException) {
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()}."
}
_postMediaUiState.value = PostMediaUiState.Error(message)
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_postMediaUiState.value = PostMediaUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
// Helper to convert Uri to MultipartBody.Part
fun uriToMultipartPart(
context: Context,
uri: Uri,
paramName: String = "media"
): MultipartBody.Part? {
try {
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)
tempFile.deleteOnExit()
FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
val requestBody = tempFile.asRequestBody(mimeType.toMediaTypeOrNull())
return MultipartBody.Part.createFormData(paramName, tempFile.name, requestBody)
} catch (e: kotlin.Exception) {
e.printStackTrace()
return null
}
}
private fun getExtensionFromMimeType(mimeType: String): String {
return if (mimeType.contains("video")) ".mp4" else ".jpg"
}
}
sealed interface ProfileUiState {
object Idle : ProfileUiState
object Loading : ProfileUiState
data class Success(val profileData: UserResponse) : ProfileUiState // Changed to List<PostResponse>
data class Error(val message: String) : ProfileUiState
}
class ProfileViewModel : ViewModel() {
private val _profileUiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Idle)
val profileUiState: StateFlow<ProfileUiState> = _profileUiState.asStateFlow()
fun profileResponse(context: Context) {
if (_profileUiState.value is ProfileUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_profileUiState.value = ProfileUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_profileUiState.value = ProfileUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
// Returns CreatePostResponse directly
val createdPost = HomeNetworkModule
.getApiService(appContext)
.getMyProfile()
Log.d("CreatePostDebug", "Response Code: ${createdPost.code()}")
if (createdPost.isSuccessful && createdPost.body() != null) {
_profileUiState.value = ProfileUiState.Success(createdPost.body()!!) // Use .body() here!
} else {
_profileUiState.value = ProfileUiState.Error("No post data received from the server.")
}
} 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()}."
}
_profileUiState.value = ProfileUiState.Error(message)
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_profileUiState.value = ProfileUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
}
sealed interface LikedUiState {
object Idle : LikedUiState
object Loading : LikedUiState
data class Success(val LikeData : List<LikedUserResponse>) : LikedUiState // Changed to List<PostResponse>
data class Error(val message: String) : LikedUiState
}
class LikedViewModel : ViewModel() {
private val _likeUiState = MutableStateFlow<LikedUiState>(LikedUiState.Idle)
val likeUiState: StateFlow<LikedUiState> = _likeUiState.asStateFlow()
fun likedResponse(context: Context) {
if (_likeUiState.value is LikedUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_likeUiState.value = LikedUiState.Loading
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("MeetApi", "No access token in user_tokens preferences")
_likeUiState.value = LikedUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
// Returns CreatePostResponse directly
val createdPost = HomeNetworkModule
.getApiService(appContext)
.getLikedUsers()
Log.d("CreatePostDebug", "Response Code: ${createdPost.code()}")
if (createdPost.isSuccessful && createdPost.body() != null) {
_likeUiState.value = LikedUiState.Success(createdPost.body()!!) // Use .body() here!
} else {
_likeUiState.value = LikedUiState.Error("No post data received from the server.")
}
} 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()}."
}
_likeUiState.value = LikedUiState.Error(message)
} catch (e: Exception) {
Log.e("MeetApi", "Unexpected request failure", e)
_likeUiState.value = LikedUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
}
// --- UI State ---
sealed interface ShowPostUiState {
object Idle : ShowPostUiState
object Loading : ShowPostUiState
data class Success(val post: ShowPostResponse) : ShowPostUiState
data class Error(val message: String) : ShowPostUiState
}
// --- Post ViewModel ---
class ShowPostViewModel : ViewModel() {
private val _showPostUiState = MutableStateFlow<ShowPostUiState>(ShowPostUiState.Idle)
val showPostUiState: StateFlow<ShowPostUiState> = _showPostUiState.asStateFlow()
fun fetchPostById(context: Context, postId: Int) {
if (_showPostUiState.value is ShowPostUiState.Loading) return
val appContext = context.applicationContext
viewModelScope.launch {
_showPostUiState.value = ShowPostUiState.Loading
// Token check
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) {
Log.e("PostViewModel", "No access token found in preferences")
_showPostUiState.value = ShowPostUiState.Error("No access token found. Please log in again.")
return@launch
}
try {
// Call API via Network Module
val response = HomeNetworkModule
.getApiService(appContext)
.getPostById(postId)
if (response.isSuccessful && response.body() != null) {
_showPostUiState.value = ShowPostUiState.Success(response.body()!!)
} else {
val errorBody = response.errorBody()?.string()
Log.e("PostError", "Code: ${response.code()}, Error: $errorBody")
_showPostUiState.value = ShowPostUiState.Error("Failed to fetch post data from server. $errorBody")
}
} catch (e: HttpException) {
val message = when (e.code()) {
401 -> "Authentication failed. Please log in again."
403 -> "You do not have permission to view this post."
404 -> "Post not found."
else -> "Request failed with status code ${e.code()}."
}
_showPostUiState.value = ShowPostUiState.Error(message)
} catch (e: Exception) {
Log.e("PostViewModel", "Unexpected request failure", e)
_showPostUiState.value = ShowPostUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
fun addCommentLocallyAndServer(context: Context, postId: Int, content: String, currentUserName: String?) {
val appContext = context.applicationContext
val optimisticComment = CommentResponse(
id = (System.currentTimeMillis() % Int.MAX_VALUE).toInt(),
postId = postId,
content = content,
author = Author(
id = 0,
full_name = currentUserName ?: "You",
city = null,
country = null,
compatibilityPercentage = null
),
createdAt = "Just now"
)
val currentList = _commentsUiState.value.toMutableList()
// Add the new comment at index 0 so it's instantly at the top
currentList.add(0, optimisticComment)
_commentsUiState.value = currentList
viewModelScope.launch {
try {
val request = CreateCommentRequest(content = content)
val response = HomeNetworkModule
.getApiService(appContext)
.createComment(postId, request)
if (response.isSuccessful) {
fetchComments(appContext, postId)
} else {
Log.e("CommentError", "Failed to sync comment with server")
}
} catch (e: Exception) {
Log.e("CommentViewModel", "Exception while posting comment", e)
}
}
}
private val _commentsUiState = MutableStateFlow<List<CommentResponse>>(emptyList())
val commentsUiState: StateFlow<List<CommentResponse>> = _commentsUiState.asStateFlow()
fun fetchComments(context: Context, postId: Int) {
val appContext = context.applicationContext
viewModelScope.launch {
try {
val response = HomeNetworkModule
.getApiService(appContext)
.getPostComments(postId)
if (response.isSuccessful && response.body() != null) {
_commentsUiState.value = response.body()!!
} else {
Log.e("CommentError", "Failed to fetch comments: ${response.errorBody()?.string()}")
}
} catch (e: Exception) {
Log.e("CommentViewModel", "Exception while fetching comments", e)
}
}
}
}

72
app/src/main/java/com/example/dattingapp/homes/profile/FrienProfileResponse.kt

@ -0,0 +1,72 @@
package com.example.dattingapp.homes.profile
import kotlinx.serialization.Serializable
@Serializable
data class FriendProfileResponse(
val id: Int,
val full_name: String,
val gender: String?,
val birth_date: String?,
val city: String?,
val county: String?,
val country: String?,
val relationship_goal: String?,
val bio: String?,
val profile_image_url: String?,
val video_url: String?,
val distance_km: Double?,
val compatibility_percentage: Int,
val media: List<FriendUserMedia>,
val interests: List<String>,
val posts: List<FriendPost>
)
@Serializable
data class FriendUserMedia(
val id: Int,
val media_type: String,
val url: String
)
@Serializable
data class FriendPost(
val id: Int,
val content: String?,
val post_type: String,
val content_format: String,
val feeling: String?,
val location: FrienLocation?,
val author: FriendAuthor,
val like_count: Int,
val comment_count: Int,
val liked_by_me: Boolean,
val media: List<FrienPostMedia>,
val created_at: String
)
@Serializable
data class FrienLocation(
val name: String,
val latitude: Double,
val longitude: Double
)
@Serializable
data class FriendAuthor(
val id: Int,
val full_name: String?,
val city: String?,
val country: String?,
val compatibility_percentage: Int
)
@Serializable
data class FrienPostMedia(
val id: Int,
val media_type: String,
val mime_type: String?,
val size_bytes: Long?,
val position: Int?,
val url: String
)

107
app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt

@ -0,0 +1,107 @@
package com.example.dattingapp.homes.profile
data class UserResponse(
val id: Int,
val email_or_phone: String,
val status: String,
val profile: ProfileData,
val created_at: String,
val posts: List<Post>,
val liked_users: List<LikedUser>,
val liked_by_users: List<Any>,
val matches: List<Any>,
val liked_users_count: Int,
val liked_by_users_count: Int,
val matches_count: Int
)
data class ProfileData(
val full_name: String,
val gender: String?,
val birth_date: String?,
val city: String?,
val county: String?,
val country: String?,
val relationship_goal: String?,
val bio: String?,
val profile_image_url: String?,
val video_url: String?,
val interests: List<String>
)
data class Post(
val id: Int,
val content: String?,
val post_type: String,
val content_format: String,
val feeling: String?,
val location: Location?,
val author: Author,
val like_count: Int,
val comment_count: Int,
val liked_by_me: Boolean,
val media: List<Media>,
val created_at: String
)
data class Location(
val name: String,
val latitude: Double,
val longitude: Double
)
data class Author(
val id: Int,
val full_name: String?,
val city: String?,
val country: String?,
val compatibility_percentage: Int
)
data class Media(
val id: Int,
val media_type: String,
val mime_type: String?,
val size_bytes: Long?,
val position: Int?,
val url: String
)
data class LikedUser(
val id: Int,
val full_name: String?,
val gender: String?,
val birth_date: String?,
val city: String?,
val county: String?,
val country: String?,
val relationship_goal: String?,
val bio: String?,
val profile_image_url: String?,
val video_url: String?,
val distance_km: Double?,
val compatibility_percentage: Int,
val media: List<UserMedia>
)
data class UserMedia(
val id: Int,
val media_type: String,
val url: String
)
data class LikedUserResponse(
val id: Int,
val full_name: String,
val gender: String,
val birth_date: String,
val city: String,
val county: String?,
val country: String,
val relationship_goal: String,
val bio: String,
val profile_image_url: String?,
val video_url: String?,
val distance_km: Double?,
val compatibility_percentage: Int,
val media: List<Media> // Adjust if media contains custom objects
)

168
app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt

@ -56,7 +56,6 @@ import com.example.dattingapp.ui.theme.PinkGradientStart
import com.example.dattingapp.ui.theme.PurpleGradientEnd import com.example.dattingapp.ui.theme.PurpleGradientEnd
import com.example.dattingapp.ui.theme.textDark import com.example.dattingapp.ui.theme.textDark
import com.example.dattingapp.ui.theme.textPink import com.example.dattingapp.ui.theme.textPink
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.Cake import androidx.compose.material.icons.filled.Cake
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -75,8 +74,32 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Looper
import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.material.icons.filled.LocationOn
// Permission check
import androidx.core.content.ContextCompat
class RegistrationScreen : ComponentActivity() { class RegistrationScreen : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@ -120,9 +143,11 @@ fun RegistrationFlowContainer(
var confirmPasswordError by remember { mutableStateOf<String?>(null) } var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var selectedBirthDateMillis by rememberSaveable { mutableStateOf<Long?>(null) } var selectedBirthDateMillis by rememberSaveable { mutableStateOf<Long?>(null) }
var addressInput by rememberSaveable { mutableStateOf("") } // var addressInput by rememberSaveable { mutableStateOf("") }
var selectedGender by rememberSaveable { mutableStateOf("") } var selectedGender by rememberSaveable { mutableStateOf("") }
var selectedRealationStatus by remember { mutableStateOf("") } var selectedRealationStatus by remember { mutableStateOf("") }
var userLat by rememberSaveable { mutableStateOf<Double?>(null) }
var userLng by rememberSaveable { mutableStateOf<Double?>(null) }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -316,8 +341,12 @@ fun RegistrationFlowContainer(
onDateSelected = { selectedBirthDateMillis = it } onDateSelected = { selectedBirthDateMillis = it }
) )
5 -> AddressCompose( 5 -> AddressCompose(
addressText = addressInput, latitude = userLat,
onAddressChange = { addressInput = it } longitude = userLng,
onLocationFetched = { lat, lng ->
userLat = lat
userLng = lng
}
) )
6 -> VibeCheckScreen( 6 -> VibeCheckScreen(
selectedRealationStatus = selectedRealationStatus ?: "", selectedRealationStatus = selectedRealationStatus ?: "",
@ -381,9 +410,7 @@ fun RegistrationFlowContainer(
} else currentStep++ } else currentStep++
} }
5 -> { 5 -> {
if (addressInput.trim().isEmpty()) { currentStep++
Toast.makeText(context, "Enter location", Toast.LENGTH_SHORT).show()
} else currentStep++
} }
6 -> { 6 -> {
if (selectedRealationStatus.isNullOrEmpty()) { if (selectedRealationStatus.isNullOrEmpty()) {
@ -412,7 +439,7 @@ fun RegistrationFlowContainer(
fullName = nameInput.trim(), fullName = nameInput.trim(),
gender = selectedGender.lowercase(Locale.ROOT), gender = selectedGender.lowercase(Locale.ROOT),
birthDate = formattedDate, birthDate = formattedDate,
city = addressInput.trim(), city = "lat: $userLat" + "lng: $userLng",
bio = null, bio = null,
videoIntro = null, videoIntro = null,
profileImage = null, profileImage = null,
@ -1095,8 +1122,67 @@ private fun ValidationRequirementRow(
@Composable @Composable
fun AddressCompose( fun AddressCompose(
addressText: String = "", addressText: String = "",
onAddressChange: (String) -> Unit = {} latitude: Double? = null,
longitude: Double? = null,
onLocationFetched: (Double, Double) -> Unit = { _, _ -> }
) { ) {
val context = LocalContext.current
var isLoading by remember { mutableStateOf(false) }
val locationManager = remember {
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@SuppressLint("MissingPermission")
fun fetchLocation() {
isLoading = true
val hasGps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val hasNetwork = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
if (!hasGps && !hasNetwork) {
isLoading = false
Toast.makeText(context, "请打开手机定位服务", Toast.LENGTH_SHORT).show()
return
}
// 1) Try last known location first (instant)
val lastKnown = listOf(
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER
).mapNotNull { provider ->
try { locationManager.getLastKnownLocation(provider) } catch (e: SecurityException) { null }
}.maxByOrNull { it.time }
if (lastKnown != null) {
isLoading = false
onLocationFetched(lastKnown.latitude, lastKnown.longitude)
return
}
// 2) No cached fix → request a fresh single update
val provider = if (hasGps) LocationManager.GPS_PROVIDER else LocationManager.NETWORK_PROVIDER
try {
locationManager.requestSingleUpdate(
provider,
{ location ->
isLoading = false
onLocationFetched(location.latitude, location.longitude)
},
Looper.getMainLooper()
)
} catch (e: SecurityException) {
isLoading = false
}
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
if (result.values.any { it }) fetchLocation()
else Toast.makeText(context, "需要定位权限才能获取位置", Toast.LENGTH_SHORT).show()
}
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
@ -1116,23 +1202,61 @@ fun AddressCompose(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField( Button(
value = addressText, onClick = {
onValueChange = onAddressChange, val granted = ContextCompat.checkSelfPermission(
modifier = Modifier.fillMaxWidth(), context, Manifest.permission.ACCESS_FINE_LOCATION
placeholder = { Text("Enter city or location", color = Color.Gray) }, ) == PackageManager.PERMISSION_GRANTED ||
singleLine = true, ContextCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
if (granted) fetchLocation()
else permissionLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors( colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF8F9FA)),
focusedContainerColor = Color(0xFFF8F9FA), enabled = !isLoading
unfocusedContainerColor = Color(0xFFF8F9FA), ) {
focusedBorderColor = Color(0xFFFF4081), if (isLoading) {
unfocusedBorderColor = Color.Transparent CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = Color(0xFFFF4081)
)
} else {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = null,
tint = Color(0xFFFF4081)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = if (latitude != null) "位置已获取 ✓" else "获取我的位置",
color = Color(0xFF333333)
) )
}
}
if (latitude != null && longitude != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "📍 ${"%.5f".format(latitude)}, ${"%.5f".format(longitude)}",
fontSize = 13.sp,
color = Color.Gray
) )
} }
} }
} }
}
data class GenderPreferenceOption( data class GenderPreferenceOption(
val id: String, val id: String,
val title: String, val title: String,

2
app/src/main/res/values/strings.xml

@ -89,6 +89,8 @@
<string name="title_activity_setting_screen">SettingScreen</string> <string name="title_activity_setting_screen">SettingScreen</string>
<string name="title_activity_upload_image">UploadImageActivity</string> <string name="title_activity_upload_image">UploadImageActivity</string>
<string name="title_activity_upload_intro_video">UploadIntroVideoActivity</string> <string name="title_activity_upload_intro_video">UploadIntroVideoActivity</string>
<string name="title_activity_like_and_match">LikeANdMatchActivity</string>
<string name="title_activity_show_posting">ShowPostingActivity</string>
</resources> </resources>
Loading…
Cancel
Save