diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2724bdb..ff18782 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -64,6 +64,7 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.6.0")
implementation("androidx.navigation:navigation-compose:2.9.3")
implementation("androidx.media3:media3-exoplayer:1.4.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
// Media3 UI components (PlayerView)
implementation("androidx.media3:media3-ui:1.4.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b5ac541..85d585c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/FriendProfile.kt b/app/src/main/java/com/example/dattingapp/FriendProfile.kt
index 5d2d709..0f05455 100644
--- a/app/src/main/java/com/example/dattingapp/FriendProfile.kt
+++ b/app/src/main/java/com/example/dattingapp/FriendProfile.kt
@@ -1,125 +1,107 @@
package com.example.dattingapp
import android.content.Context
+import android.content.Intent
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-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.clickable
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.TextSnippet
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.Close
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.MusicNote
import androidx.compose.material.icons.filled.Star
+import androidx.compose.material.icons.filled.TextSnippet
import androidx.compose.material.icons.outlined.LocationOn
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-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.material3.*
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
+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.res.painterResource
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
-import com.example.dattingapp.homes.networkandapi.FriendProfileMedia
-import com.example.dattingapp.homes.networkandapi.FriendProfileResponse
-import com.example.dattingapp.homes.networkandapi.FriendProfileUiState
-import com.example.dattingapp.homes.networkandapi.FriendProfileViewModel
-import com.example.dattingapp.homes.networkandapi.MeetUserProfile
-import com.example.dattingapp.homes.networkandapi.SwipeRequest
-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 com.example.dattingapp.homes.networkandapi.*
+import com.example.dattingapp.homes.profile.FrienPostMedia
+import com.example.dattingapp.homes.profile.FriendAuthor
+import com.example.dattingapp.homes.profile.FriendPost
+import com.example.dattingapp.homes.profile.FriendProfileResponse
+import com.example.dattingapp.ui.theme.*
import java.time.LocalDate
import java.time.Period
import java.time.format.DateTimeFormatter
-
-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.material3.CircularProgressIndicator
-import androidx.compose.material3.Text
-import androidx.compose.ui.unit.dp
+import kotlin.jvm.java
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.encodeToString
class FriendProfile : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
- // Read extras passed from the Intent
val userId = intent.getIntExtra("USER_ID", -1)
setContent {
DattingAPPTheme {
val context = LocalContext.current
- val viewModel: FriendProfileViewModel = viewModel()
+ val friendViewModel: FriendProfileViewModel = viewModel()
+ val postViewModel: PostViewModel = viewModel()
- // 2. Collect state from ViewModel
- val uiState by viewModel.friendProfileUiState.collectAsState()
+ val profileUiState by friendViewModel.friendProfileUiState.collectAsState()
+ val postUiState by postViewModel.postUiState.collectAsState()
- // 3. Trigger network call once when screen loads
- LaunchedEffect(Unit) {
- viewModel.fetchUserProfile(context,userId)
+ LaunchedEffect(userId) {
+ if (userId != -1) {
+ friendViewModel.fetchUserProfile(context, userId)
+ postViewModel.fetchUserPost(context, userId)
+ }
}
- when (val state = uiState) {
+
+ when (val state = profileUiState) {
is FriendProfileUiState.Loading -> {
- // Show loading indicator
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
}
is FriendProfileUiState.Success -> {
- val friendProfileData = state.friendProfile
- FriendProfileScreen(friendProfileData,context)
-
+ FriendProfileScreen(
+ friendProfileData = state.friendProfile,
+ context = context,
+ postUiState = postUiState
+ )
}
is FriendProfileUiState.Error -> {
- // Show error message
- }
- FriendProfileUiState.Idle -> {
- // Idle state
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text(text = state.message, color = MaterialTheme.colorScheme.error)
+ }
}
+ FriendProfileUiState.Idle -> {}
}
-
}
}
}
@@ -129,75 +111,73 @@ class FriendProfile : ComponentActivity() {
fun FriendProfileScreen(
friendProfileData: FriendProfileResponse,
context: Context,
- swipeViewModel: SwipeViewModel= viewModel() ,
- onBackClick:()->Unit={},
- onMoreClick:()->Unit={},
- onMessageClick:()->Unit={},
- onLikeClick:()->Unit={},
- onStarClick:()->Unit={},
- onCancelClick:()->Unit={},
- ){
-
-
- val age = remember(friendProfileData.birthDate) {
- calculateAge(friendProfileData.birthDate)
+ postUiState: PostUiState = PostUiState.Idle,
+ swipeViewModel: SwipeViewModel = viewModel(),
+ onBackClick: () -> Unit = {},
+ onMoreClick: () -> Unit = {},
+ onMessageClick: () -> Unit = {},
+ onLikeClick: () -> Unit = {},
+ onStarClick: () -> Unit = {},
+ onCancelClick: () -> Unit = {}
+) {
+ val age = remember(friendProfileData.birth_date) {
+ calculateAge(friendProfileData.birth_date)
}
-
-
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.verticalScroll(rememberScrollState())
) {
-
+ // Header Cover & Profile Avatar
Box(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
- ){
+ ) {
Image(
- painter = painterResource(id=R.drawable.gallarypermissionbg),
+ painter = painterResource(id = R.drawable.gallarypermissionbg),
contentDescription = "cover photo",
- modifier = Modifier.fillMaxWidth().height(200.dp)
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
.clip(RoundedCornerShape(bottomStart = 10.dp, bottomEnd = 20.dp)),
contentScale = ContentScale.Crop
)
Row(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 25.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 25.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
- ){
+ ) {
IconButton(
- onClick=onBackClick,
+ onClick = onBackClick,
modifier = Modifier.size(20.dp).clip(CircleShape)
-
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "back button",
+ contentDescription = "back button"
)
}
IconButton(
- onClick=onBackClick,
+ onClick = onMoreClick,
modifier = Modifier.size(20.dp).clip(CircleShape)
-
) {
Icon(
imageVector = Icons.Default.MoreHoriz,
- contentDescription = "back button",
+ contentDescription = "more options"
)
}
-
}
- // Assuming 'profileImageUrl' is passed into your composable from FriendProfileResponse
+
AsyncImage(
- model = friendProfileData?.profileImageUrl, // Pass the String URL from FriendProfileResponse
+ model = friendProfileData.profile_image_url,
contentDescription = "Profile Avatar",
- placeholder = painterResource(id = R.drawable.locationpermissionbg), // Placeholder while loading
- error = painterResource(id = R.drawable.locationpermissionbg), // Fallback if URL fails or is null
+ placeholder = painterResource(id = R.drawable.locationpermissionbg),
+ error = painterResource(id = R.drawable.locationpermissionbg),
modifier = Modifier
.padding(start = 20.dp)
.align(Alignment.BottomStart)
@@ -210,18 +190,17 @@ fun FriendProfileScreen(
Spacer(modifier = Modifier.height(12.dp))
- Column(modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 20.dp)
- ){
-
-
+ // Profile Main Details
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ ) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
-
- val displayName = friendProfileData.fullName ?: "User"
+ val displayName = friendProfileData.full_name ?: "User"
val nameAndAge = if (age != null) "$displayName, $age" else displayName
Text(
text = nameAndAge,
@@ -229,12 +208,6 @@ fun FriendProfileScreen(
fontWeight = FontWeight.Bold,
color = textDark
)
- Text(
- text = "24",
- fontSize = 24.sp,
- fontWeight = FontWeight.Bold,
- color = textDark
- )
Surface(
shape = RoundedCornerShape(12.dp),
color = BadgePink
@@ -251,19 +224,19 @@ fun FriendProfileScreen(
modifier = Modifier.size(10.dp)
)
Text(
- text = "98% Match",
+ text = "${friendProfileData.county?: 0}% Match",
color = Color.White,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
}
}
-
}
Row(
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp)
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier.padding(vertical = 4.dp)
) {
Icon(
imageVector = Icons.Outlined.LocationOn,
@@ -272,41 +245,22 @@ fun FriendProfileScreen(
modifier = Modifier.size(14.dp)
)
Text(
- text = "2 miles away",
- 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",
+ text = "${friendProfileData.city ?: "Unknown"}, ${friendProfileData.country ?: ""}",
fontSize = 12.sp,
color = Color.Gray
)
}
-
Surface(
- onClick = {
- // Handle bio click action here (e.g., expand bio or navigate)
- },
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
shape = RoundedCornerShape(12.dp),
color = Color(0xFFF9F9F9),
shadowElevation = 1.dp
) {
- val displayBio= friendProfileData.bio ?: ""
Text(
- text = displayBio,
+ text = friendProfileData.bio ?: "",
fontSize = 13.sp,
color = Color(0xFF4A4A4A),
modifier = Modifier.padding(12.dp),
@@ -315,6 +269,7 @@ fun FriendProfileScreen(
)
}
+ // Action Buttons Row
Row(
modifier = Modifier
.fillMaxWidth()
@@ -322,7 +277,6 @@ fun FriendProfileScreen(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
- // Message Button (Gradient Background)
Button(
onClick = onMessageClick,
modifier = Modifier
@@ -351,110 +305,292 @@ fun FriendProfileScreen(
)
}
- // Heart Button
IconButton(
onClick = {
- // Option 1: Invoke callback directly if handleSwipeAction is inside the parent screen
onLikeClick()
-
- // 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
- )
+ handleLikeAndDislikeAction(context, "right", friendProfileData, swipeViewModel)
},
modifier = Modifier
.size(48.dp)
- .background(
- color = purpleButtonColor,
- shape = RoundedCornerShape(10.dp)
- )
+ .background(color = purpleButtonColor, shape = RoundedCornerShape(10.dp))
) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Like",
- tint = Color(0xFFE2C4FF), // Soft light purple icon color
+ tint = Color(0xFFE2C4FF),
modifier = Modifier.size(20.dp)
)
}
- // Star Button
IconButton(
onClick = {
onStarClick()
- handleLikeAndDislikeAction(
- context = context,
- direction = "right", // Or "like", matching your API endpoint expected value
- profile = friendProfileData, // Your MeetUserProfile instance
- swipeViewModel = swipeViewModel
- )
+ handleLikeAndDislikeAction(context, "right", friendProfileData, swipeViewModel)
},
modifier = Modifier
.size(48.dp)
- .background(
- color = purpleButtonColor,
- shape = RoundedCornerShape(10.dp)
- )
+ .background(color = purpleButtonColor, shape = RoundedCornerShape(10.dp))
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = "Favorite",
- tint = Color(0xFFE2C4FF), // Soft light purple icon color
+ tint = Color(0xFFE2C4FF),
modifier = Modifier.size(20.dp)
)
}
+
IconButton(
onClick = {
- onStarClick()
- handleLikeAndDislikeAction(
- context = context,
- direction = "right", // Or "like", matching your API endpoint expected value
- profile = friendProfileData, // Your MeetUserProfile instance
- swipeViewModel = swipeViewModel
- )
- },
+ onCancelClick()
+ handleLikeAndDislikeAction(context, "left", friendProfileData, swipeViewModel)
+ },
modifier = Modifier
.size(48.dp)
- .background(
- color = purpleButtonColor,
- shape = RoundedCornerShape(10.dp)
- )
+ .background(color = purpleButtonColor, shape = RoundedCornerShape(10.dp))
) {
Icon(
imageVector = Icons.Default.Close,
- contentDescription = "Favorite",
- tint = Color(0xFFE2C4FF), // Soft light purple icon color
+ contentDescription = "Dislike",
+ tint = Color(0xFFE2C4FF),
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) {
+ // Distribute posts into two columns
+ val columns = remember(posts) {
+ val col1 = mutableListOf()
+ val col2 = mutableListOf()
+
+ posts.forEachIndexed { index, post ->
+ if (index % 2 == 0) {
+ col1.add(post)
+ } else {
+ col2.add(post)
}
+ }
+ Pair(col1, col2)
+ }
+ 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)
+ }
}
-//
-// LazyVerticalGrid(
-// 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)
-// }
-// }
}
+}
+@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(
context: Context,
- direction:String,
+ direction: String,
profile: FriendProfileResponse,
swipeViewModel: SwipeViewModel
) {
@@ -463,8 +599,6 @@ private fun handleLikeAndDislikeAction(
swipeViewModel.sendSwipeResponse(context, swipeRequest)
}
-
-//calculte age
private fun calculateAge(birthDateStr: String?): Int? {
if (birthDateStr.isNullOrBlank()) return null
return try {
@@ -483,28 +617,224 @@ fun FriendProfileScreenPreview() {
DattingAPPTheme {
val mockProfileResponse = FriendProfileResponse(
id = 101,
- fullName = "Elena Rostova",
+ full_name = "Elena Rostova",
gender = "Female",
- birthDate = "1999-05-14",
+ birth_date = "1999-05-14",
city = "San Francisco",
+ county = "San Francisco County",
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.",
- profileImageUrl = "https://via.placeholder.com/150",
- videoUrl = null,
- compatibilityPercentage = 98,
- media = null
+ profile_image_url = "https://via.placeholder.com/150",
+ video_url = null,
+ distance_km = 2.5,
+ 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(
friendProfileData = mockProfileResponse,
context = LocalContext.current,
+ postUiState = PostUiState.Success(mockPosts),
onBackClick = {},
onMoreClick = {},
onMessageClick = {},
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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt b/app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt
new file mode 100644
index 0000000..b0d29fc
--- /dev/null
+++ b/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,
+ 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
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/LikeAndMatchScreen.kt b/app/src/main/java/com/example/dattingapp/LikeAndMatchScreen.kt
deleted file mode 100644
index d614a48..0000000
--- a/app/src/main/java/com/example/dattingapp/LikeAndMatchScreen.kt
+++ /dev/null
@@ -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,
- val imageUrl: String,
- val hasUnread: Boolean = false
-)
-
-@Composable
-fun ProfileGrid(profiles: List, 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)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt b/app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt
new file mode 100644
index 0000000..b3e79ee
--- /dev/null
+++ b/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,
+ 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,
+ 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
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt b/app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt
new file mode 100644
index 0000000..4850d47
--- /dev/null
+++ b/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 = 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?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt b/app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt
index 4dc2051..1f97580 100644
--- a/app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt
+++ b/app/src/main/java/com/example/dattingapp/homes/CreatePostScreen.kt
@@ -1,6 +1,7 @@
package com.example.dattingapp.homes
-
+import com.example.dattingapp.homes.networkandapi.Location
+import com.example.dattingapp.homes.networkandapi.CreatePostRequest
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
@@ -46,13 +47,27 @@ import androidx.media3.ui.PlayerView
import coil.compose.rememberAsyncImagePainter
import com.example.dattingapp.homes.networkandapi.VibeFeeling
import com.example.dattingapp.homes.networkandapi.vibeFeelingsList
-import java.io.File
import com.example.dattingapp.ui.theme.LightPinkBg
import com.example.dattingapp.ui.theme.LightPurpleBg
import com.example.dattingapp.ui.theme.DarkOverlay
import com.example.dattingapp.ui.theme.LightBlueBg
-
+import android.Manifest
+import android.content.pm.PackageManager
+import android.location.LocationManager
+import android.widget.Toast
+import androidx.core.content.ContextCompat
import androidx.compose.foundation.lazy.grid.items
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.example.dattingapp.homes.networkandapi.CreatePostViewModel
+import com.example.dattingapp.homes.networkandapi.CreatePostUiState
+import android.util.Log
+
+
+import android.content.Context
+import com.example.dattingapp.homes.networkandapi.PostMediaViewModel
+
+import java.io.File
// Sealed class representing selectable media types (Photo or Video)
sealed class MediaContentItem(val uri: Uri) {
@@ -72,6 +87,14 @@ fun CreateVibePostScreen(
// Holds selected media items (photos, videos, or both)
val selectedMediaList = remember { mutableStateListOf() }
+ // 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(null) }
var tempCameraVideoUri by remember { mutableStateOf(null) }
@@ -80,6 +103,9 @@ fun CreateVibePostScreen(
var showFeelingSheet by remember { mutableStateOf(false) }
var selectedFeeling by remember { mutableStateOf(null) }
+
+
+
// Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously)
val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia()
@@ -124,6 +150,69 @@ fun CreateVibePostScreen(
}
}
+
+ // Location States
+ var latitude by remember { mutableStateOf(null) }
+ var longitude by remember { mutableStateOf(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(
bottomBar = {
Row(
@@ -152,7 +241,8 @@ fun CreateVibePostScreen(
VibeOptionChip(
icon = Icons.Outlined.MyLocation,
backgroundColor = LightBlueBg,
- contentColor = Color(0xFF1E88E5)
+ contentColor = Color(0xFF1E88E5),
+ onClick = { onLocationChipClick() }
)
}
item {
@@ -174,9 +264,85 @@ fun CreateVibePostScreen(
Spacer(modifier = Modifier.width(8.dp))
+ // 2. React to UI state changes (Handles post success and triggers media uploads)
// 2. Share button on the right
+
+ val context = LocalContext.current
+ val createPostViewModel: CreatePostViewModel = viewModel()
+ val postMediaViewModel: PostMediaViewModel = viewModel()
+ val uiState by createPostViewModel.cretePpostUiState.collectAsState()
+
+ LaunchedEffect(uiState) {
+ when (val state = uiState) {
+ is CreatePostUiState.Success -> {
+ val postId = state.postData.id
+ Log.d("CreatePostDebug", "Post created successfully with ID: $postId. Starting media uploads...")
+
+ if (selectedMediaList.isEmpty()) {
+ Log.d("CreatePostDebug", "No media attached. Flow complete.")
+ onShareClick() // Navigate away or finish screen
+ } else {
+ // Loop through and upload media sequentially
+ for ((index, item) in selectedMediaList.withIndex()) {
+ val uri = when (item) {
+ is MediaContentItem.Photo -> item.uri
+ is MediaContentItem.Video -> item.uri
+ }
+
+ val position = index + 1 // Position starts at 1
+
+ if (uri != null && uri.toString().isNotBlank()) {
+ Log.d("CreatePostDebug", "Uploading media at position $position...")
+ postMediaViewModel.poostMedia(context,postId,index,uri)
+
+ // Call your media upload function here
+ // val isUploaded = createPostViewModel.uploadPostMediaItem(context, postId, position, uri)
+ }
+ }
+ Log.d("CreatePostDebug", "All media processed.")
+ onShareClick() // Navigate away or finish screen after media upload
+ }
+ }
+ is CreatePostUiState.Error -> {
+ Log.e("CreatePostDebug", "Error: ${state.message}")
+ }
+ else -> {}
+ }
+ }
Button(
- 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
.wrapContentWidth()
.height(40.dp),
@@ -293,7 +459,7 @@ fun CreateVibePostScreen(
)
Spacer(modifier = Modifier.width(8.dp))
Text(
- text = "Blue Bottle Coffee, Soho NYC",
+ text = "Lat: $latitude, Lng: $longitude",
fontWeight = FontWeight.Medium,
fontSize = 13.sp
)
@@ -676,6 +842,9 @@ fun FeelingPickerBottomSheet(
}
}
+
+
+
@Preview(
name = "Create Vibe Post Screen",
showBackground = true,
diff --git a/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt b/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt
index 02cb259..9499cf6 100644
--- a/app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt
+++ b/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.platform.LocalContext
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.MeetUserProfile
import com.example.dattingapp.homes.networkandapi.MeetViewModel
-import com.example.dattingapp.homes.networkandapi.MyProfileUiState
-import com.example.dattingapp.homes.networkandapi.MyProfileViewModel
+import com.example.dattingapp.homes.networkandapi.ProfileViewModel
+import com.example.dattingapp.homes.networkandapi.ProfileUiState
import com.example.dattingapp.permission.PermissionHelper
import com.example.dattingapp.ui.theme.DattingAPPTheme
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() {
private lateinit var permissionHelper: PermissionHelper
@@ -109,8 +102,13 @@ class HomeActivity : ComponentActivity() {
if (currentType == PermissionHelper.PermissionType.OVERLAY) {
permissionHelper.requestOverlayPermission { isGranted ->
- val msg = if (isGranted) "Overlay Granted" else "Overlay Denied"
- Toast.makeText(this@HomeActivity, msg, Toast.LENGTH_SHORT).show()
+ if (!isGranted) {
+ Toast.makeText(
+ this@HomeActivity,
+ "Overlay Denied",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
}
} else {
permissionHelper.requestPermissions(currentType) { isGranted ->
@@ -325,40 +323,41 @@ fun HomeScreen() {
// =========================================
4 -> {
// 1. Obtain instance of ViewModel and LocalContext
- val viewModel: MyProfileViewModel = viewModel()
+ val profileViewModel: ProfileViewModel = viewModel()
val context = LocalContext.current
// 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
LaunchedEffect(Unit) {
- viewModel.meetUserResponse(context)
+ profileViewModel.profileResponse(context)
}
// 4. Map UI state to Composable parameters
+
when (val state = uiState) {
- is MyProfileUiState.Loading, MyProfileUiState.Idle -> {
+ is ProfileUiState.Loading, ProfileUiState.Idle -> {
MyProfileScreen(
- profile = null,
+ profileResponse = null,
isLoading = true,
errorMessage = null,
-// onRetry = { viewModel.meetUserResponse(context) }
+// onRetry = { viewModel.meetUserResponse(context) }
)
}
- is MyProfileUiState.Success -> {
+ is ProfileUiState.Success -> {
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,
errorMessage = null,
)
}
- is MyProfileUiState.Error -> {
+ is ProfileUiState.Error -> {
MyProfileScreen(
- profile = null,
+ profileResponse = null,
isLoading = false,
errorMessage = state.message,
- onRetry = { viewModel.meetUserResponse(context) }
+ onRetry = { profileViewModel.profileResponse(context) }
)
}
}
diff --git a/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt b/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
index d28820d..aabd872 100644
--- a/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
+++ b/app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
@@ -1,10 +1,5 @@
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.background
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.filled.Check
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.material3.*
import androidx.compose.runtime.*
@@ -34,12 +27,18 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.example.dattingapp.R
-import com.example.dattingapp.homes.networkandapi.MyProfileResponse
+import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.ui.theme.BrandMagenta
import com.example.dattingapp.ui.theme.BrandPurple
import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.textDark
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
@@ -47,19 +46,23 @@ import com.example.dattingapp.ui.theme.TextMuted
@Composable
fun MyProfileScreen(
- profile: MyProfileResponse?, // Change from MyProfileData? to MyProfileResponse?
+ profileResponse : UserResponse?, // Change from MyProfileData? to MyProfileResponse?
isLoading: Boolean,
errorMessage: String?,
onRetry: () -> Unit = {},
onEditClick: () -> Unit = {},
onUpgradeClick: () -> Unit = {},
- onSettingsClick: () -> Unit = {}
+ onSettingsClick: () -> Unit = {},
+ onLikesClick: () -> Unit={},
+ onLikedClick: () -> Unit={},
+ onMatchClick: () -> Unit={}
+
) {
// ========================================================
// FULL SCREEN SCROLL
// ========================================================
-
+ val context = LocalContext.current
val scrollState = rememberScrollState()
Column(
modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) {
@@ -113,7 +116,7 @@ fun MyProfileScreen(
AsyncImage(
// Access nested inner 'profile' first, then 'profileImageUrl'
- model = profile?.profile?.profileImageUrl,
+ model = profileResponse?.profile?.profile_image_url,
contentDescription = "Profile Picture",
contentScale = ContentScale.Crop,
placeholder = painterResource(id = R.drawable.locationpermissionbg),
@@ -164,7 +167,7 @@ fun MyProfileScreen(
) {
Text(
- text = profile?.profile?.fullName?:"",
+ text = profileResponse?.profile?.full_name?:"",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = BrandMagenta
@@ -207,7 +210,7 @@ fun MyProfileScreen(
Text(
- text = profile?.profile?.bio?:"",
+ text = profileResponse?.profile?.bio?:"",
fontSize = 13.sp,
color = TextMuted,
lineHeight = 18.sp
@@ -296,29 +299,36 @@ fun MyProfileScreen(
) {
StatCard(
- number = "24",
+ number = profileResponse?.liked_users_count?.toString() ?: "0",
label = "Likes",
- modifier = Modifier.weight(1f)
+ modifier = Modifier.weight(1f),
+ onClick = {
+ val intent = Intent(context, LikeANdMatchActivity::class.java)
+ context.startActivity(intent)
+ onLikesClick()
+ }
)
StatCard(
- number = "12",
+ number = profileResponse?.liked_by_users_count?.toString() ?: "0",
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(
- number = "Top",
- label = "Match",
- numberColor = BrandMagenta,
- modifier = Modifier.weight(1f)
- )
- StatCard(
- number = "8",
+ number = profileResponse?.matches_count?.toString() ?: "0",
label = "Matches",
- modifier = Modifier.weight(1f)
+ modifier = Modifier.weight(1f),
+ onClick = { /* Optional: Handle Matches click */ }
)
}
Spacer(
@@ -383,12 +393,13 @@ private fun StatCard(
number: String,
label: String,
modifier: Modifier = Modifier,
- numberColor: Color = textDark
+ numberColor: Color = textDark,
+ onClick:()->Unit
) {
Card(
- modifier = modifier,
+ modifier = modifier.clickable { onClick() },
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
@@ -423,39 +434,47 @@ private fun StatCard(
}
}
+
// ============================================================
// PREVIEW
// ============================================================
-// 1. Success State Preview (With Mock User Profile Data)
+
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun MyProfileScreenSuccessPreview() {
DattingAPPTheme {
- // Create dummy profile response matching your backend structure
- val mockProfileResponse = MyProfileResponse(
+ // Mock UserResponse matching your backend model hierarchy
+ val mockUserResponse = UserResponse(
id = 1,
- emailOrPhone = "alex.dev@example.com",
+ email_or_phone = "alex.dev@example.com",
status = "active",
- profile = com.example.dattingapp.homes.networkandapi.UserProfileDetails(
- fullName = "Alex",
+ profile = ProfileData(
+ full_name = "Alex Morgan",
gender = "Male",
+ birth_date = "1995-05-15",
city = "New York",
+ county = "Manhattan",
country = "USA",
+ relationship_goal = "Long-term partnership",
bio = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!",
- profileImageUrl = "https://picsum.photos/300/300", // Sample remote URL
- videoUrl = null
+ profile_image_url = "https://picsum.photos/300/300",
+ 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(
- profile = mockProfileResponse,
+ profileResponse = mockUserResponse,
isLoading = false,
- errorMessage = null,
- onRetry = {},
- onEditClick = {},
- onUpgradeClick = {},
- onSettingsClick = {}
+ errorMessage = null
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
index e8bdbb5..78ea62a 100644
--- a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
+++ b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
@@ -1,11 +1,20 @@
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.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
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 {
@@ -15,7 +24,7 @@ interface HomeApiService {
): Response>
@GET("api/v1/users/me")
- suspend fun getCurrentUserProfile(): Response
+ suspend fun getCurrentUserProfile(): Response
@POST("api/v1/meet/swipes")
suspend fun sendSwipe(
@@ -27,5 +36,55 @@ interface HomeApiService {
@Path("user_id") userId: Int
): Response
+ @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
+
+ @GET("posts/{id}")
+ suspend fun getCreatePost(
+ @Path("id") postId: Int
+ ): Response
+
+ @POST("/api/v1/explore/posts")
+ suspend fun createPost(
+ @Body request: CreatePostRequest
+ ): Response
+
+
+ @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
+
+ @GET("api/v1/users/me")
+ suspend fun getMyProfile(
+ ): Response
+
+ @GET("api/v1/users/me/liked")
+ suspend fun getLikedUsers(
+ ): Response>
+
+ @GET("api/v1/explore/posts/{post_id}")
+ suspend fun getPostById(
+ @Path("post_id") postId: Int
+ ): Response
+
+
+ @POST("api/v1/explore/posts/{post_id}/comments")
+ suspend fun createComment(
+ @Path("post_id") postId: Int,
+ @Body request: CreateCommentRequest
+ ): Response
+
+ @GET("api/v1/explore/posts/{post_id}/comments")
+ suspend fun getPostComments(
+ @Path("post_id") postId: Int
+ ): Response>
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
index 4b66daf..d972131 100644
--- a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
+++ b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeDataClass.kt
@@ -57,26 +57,23 @@ data class MeetUserProfile(
/**
* Standard API wrapper format for MyProfile
*/
-data class MyProfileResponse(
- @SerializedName("id") val id: Int?,
- @SerializedName("email_or_phone") val emailOrPhone: String?,
- @SerializedName("status") val status: String?,
- @SerializedName("profile") val profile: UserProfileDetails?, // Nested profile details
- @SerializedName("created_at") val createdAt: String?
-)
-
-data class UserProfileDetails(
- @SerializedName("full_name") val fullName: String?,
- @SerializedName("gender") val gender: String?,
- @SerializedName("city") val city: String?,
- @SerializedName("country") val country: String?,
- @SerializedName("bio") val bio: String?,
- @SerializedName("profile_image_url") val profileImageUrl: String?, // Resolved field
- @SerializedName("video_url") val videoUrl: String?
-)
-
-
-
+//data class MyProfileResponse(
+// @SerializedName("id") val id: Int?,
+// @SerializedName("email_or_phone") val emailOrPhone: String?,
+// @SerializedName("status") val status: String?,
+// @SerializedName("profile") val profile: UserProfileDetails?, // Nested profile details
+// @SerializedName("created_at") val createdAt: String?
+//)
+//
+//data class UserProfileDetails(
+// @SerializedName("full_name") val fullName: String?,
+// @SerializedName("gender") val gender: String?,
+// @SerializedName("city") val city: String?,
+// @SerializedName("country") val country: String?,
+// @SerializedName("bio") val bio: String?,
+// @SerializedName("profile_image_url") val profileImageUrl: String?, // Resolved field
+// @SerializedName("video_url") val videoUrl: String?
+//)
data class MediaItem(
@SerializedName("id")
@@ -147,33 +144,75 @@ data class SwipeResponse(
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,
+ @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,
- @SerializedName("media_type")
- val mediaType: String?,
- val url: String?
+ val full_name: String,
+ val city: 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,
- @SerializedName("full_name")
- val fullName: String?,
- val gender: String?,
- @SerializedName("birth_date")
- val birthDate: String?,
- val city: String?,
- val country: String?,
- @SerializedName("relationship_goal")
- val relationshipGoal: String?,
- val bio: String?,
- @SerializedName("profile_image_url")
- val profileImageUrl: String?,
- @SerializedName("video_url")
- val videoUrl: String?,
- @SerializedName("compatibility_percentage")
- val compatibilityPercentage: Int?,
- val media: List?
-)
\ No newline at end of file
+ val content: String,
+ val post_type: String,
+ val content_format: String,
+ val feeling: String?,
+ val location: Location?,
+ val author: CreatePostAuthor,
+ val like_count: Int,
+ val comment_count: Int,
+ val liked_by_me: Boolean,
+ val media: List, // Change to specific model if media items have structure
+ val created_at: String
+)
+
+
+
+data class PostMediaResponse(
+ val id: Int,
+ val media_type: String,
+ val mime_type: String,
+ val size_bytes: Int,
+ val position: Int,
+ val url: String
+)
diff --git a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
index 5ff3dde..a2d8ef1 100644
--- a/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
+++ b/app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
@@ -1,5 +1,7 @@
package com.example.dattingapp.homes.networkandapi
+
+import com.example.dattingapp.homes.profile.UserResponse
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
@@ -8,6 +10,20 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
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 {
object Idle : MeetUiState()
@@ -95,66 +111,6 @@ class MeetViewModel : 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.Idle)
- val myProfileUiState: StateFlow = _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) : PostUiState // Changed to List
+ data class Error(val message: String) : PostUiState
+}
+
+class PostViewModel : ViewModel() {
+
+ private val _postUiState = MutableStateFlow(PostUiState.Idle)
+ val postUiState: StateFlow = _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 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
+ data class Error(val message: String) : CreatePostUiState
+}
+
+class CreatePostViewModel : ViewModel() {
+
+ private val _cretePostUiState = MutableStateFlow(CreatePostUiState.Idle)
+ val cretePpostUiState: StateFlow = _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
+ data class Error(val message: String) : PostMediaUiState
+}
+
+
+class PostMediaViewModel : ViewModel() {
+
+ private val _postMediaUiState = MutableStateFlow(PostMediaUiState.Idle)
+ val cretePpostUiState: StateFlow = _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
+ data class Error(val message: String) : ProfileUiState
+}
+
+class ProfileViewModel : ViewModel() {
+
+ private val _profileUiState = MutableStateFlow(ProfileUiState.Idle)
+ val profileUiState: StateFlow = _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) : LikedUiState // Changed to List
+ data class Error(val message: String) : LikedUiState
+}
+
+class LikedViewModel : ViewModel() {
+
+ private val _likeUiState = MutableStateFlow(LikedUiState.Idle)
+ val likeUiState: StateFlow = _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.Idle)
+ val showPostUiState: StateFlow = _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>(emptyList())
+ val commentsUiState: StateFlow> = _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)
+ }
+ }
+ }
+
+}
+
diff --git a/app/src/main/java/com/example/dattingapp/homes/profile/FrienProfileResponse.kt b/app/src/main/java/com/example/dattingapp/homes/profile/FrienProfileResponse.kt
new file mode 100644
index 0000000..0789017
--- /dev/null
+++ b/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,
+ val interests: List,
+ val posts: List
+)
+
+@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,
+ 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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt b/app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt
new file mode 100644
index 0000000..2c7e0b7
--- /dev/null
+++ b/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,
+ val liked_users: List,
+ val liked_by_users: List,
+ val matches: List,
+ 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
+)
+
+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,
+ 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
+)
+
+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 // Adjust if media contains custom objects
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt b/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt
index 3e1bb06..9389b95 100644
--- a/app/src/main/java/com/example/dattingapp/registration/RegistrationScreen.kt
+++ b/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.textDark
import com.example.dattingapp.ui.theme.textPink
-import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.Cake
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -75,8 +74,32 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
@@ -120,9 +143,11 @@ fun RegistrationFlowContainer(
var confirmPasswordError by remember { mutableStateOf(null) }
var selectedBirthDateMillis by rememberSaveable { mutableStateOf(null) }
- var addressInput by rememberSaveable { mutableStateOf("") }
+// var addressInput by rememberSaveable { mutableStateOf("") }
var selectedGender by rememberSaveable { mutableStateOf("") }
var selectedRealationStatus by remember { mutableStateOf("") }
+ var userLat by rememberSaveable { mutableStateOf(null) }
+ var userLng by rememberSaveable { mutableStateOf(null) }
val scrollState = rememberScrollState()
@@ -316,8 +341,12 @@ fun RegistrationFlowContainer(
onDateSelected = { selectedBirthDateMillis = it }
)
5 -> AddressCompose(
- addressText = addressInput,
- onAddressChange = { addressInput = it }
+ latitude = userLat,
+ longitude = userLng,
+ onLocationFetched = { lat, lng ->
+ userLat = lat
+ userLng = lng
+ }
)
6 -> VibeCheckScreen(
selectedRealationStatus = selectedRealationStatus ?: "",
@@ -381,9 +410,7 @@ fun RegistrationFlowContainer(
} else currentStep++
}
5 -> {
- if (addressInput.trim().isEmpty()) {
- Toast.makeText(context, "Enter location", Toast.LENGTH_SHORT).show()
- } else currentStep++
+ currentStep++
}
6 -> {
if (selectedRealationStatus.isNullOrEmpty()) {
@@ -412,7 +439,7 @@ fun RegistrationFlowContainer(
fullName = nameInput.trim(),
gender = selectedGender.lowercase(Locale.ROOT),
birthDate = formattedDate,
- city = addressInput.trim(),
+ city = "lat: $userLat" + "lng: $userLng",
bio = null,
videoIntro = null,
profileImage = null,
@@ -1095,8 +1122,67 @@ private fun ValidationRequirementRow(
@Composable
fun AddressCompose(
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(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
@@ -1116,20 +1202,58 @@ fun AddressCompose(
Spacer(modifier = Modifier.height(12.dp))
- OutlinedTextField(
- value = addressText,
- onValueChange = onAddressChange,
- modifier = Modifier.fillMaxWidth(),
- placeholder = { Text("Enter city or location", color = Color.Gray) },
- singleLine = true,
+ Button(
+ onClick = {
+ val granted = ContextCompat.checkSelfPermission(
+ context, Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED ||
+ 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),
- colors = OutlinedTextFieldDefaults.colors(
- focusedContainerColor = Color(0xFFF8F9FA),
- unfocusedContainerColor = Color(0xFFF8F9FA),
- focusedBorderColor = Color(0xFFFF4081),
- unfocusedBorderColor = Color.Transparent
+ colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFF8F9FA)),
+ enabled = !isLoading
+ ) {
+ if (isLoading) {
+ 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
)
- )
+ }
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 81f8ef8..2df7fac 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -89,6 +89,8 @@
SettingScreen
UploadImageActivity
UploadIntroVideoActivity
+ LikeANdMatchActivity
+ ShowPostingActivity
\ No newline at end of file