From f02cbea3c8477b9f46e256f25e15b10cb0a06de7 Mon Sep 17 00:00:00 2001 From: osman160128 Date: Tue, 13 Oct 2026 14:56:06 +0600 Subject: [PATCH] comment section --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 29 +- .../com/example/dattingapp/FriendProfile.kt | 736 +++++++++++++----- .../dattingapp/LikeANdMatchActivity.kt | 301 +++++++ .../example/dattingapp/LikeAndMatchScreen.kt | 252 ------ .../example/dattingapp/ShowPostingActivity.kt | 447 +++++++++++ .../example/dattingapp/dataclass/DataClass.kt | 61 ++ .../dattingapp/homes/CreatePostScreen.kt | 181 ++++- .../example/dattingapp/homes/HomeActivity.kt | 43 +- .../com/example/dattingapp/homes/MyProfile.kt | 107 +-- .../homes/networkandapi/HomeApiService.kt | 61 +- .../homes/networkandapi/HomeDataClass.kt | 127 +-- .../homes/networkandapi/HomeViewModelClass.kt | 536 +++++++++++-- .../homes/profile/FrienProfileResponse.kt | 72 ++ .../homes/profile/UserResponseData.kt | 107 +++ .../registration/RegistrationScreen.kt | 170 +++- app/src/main/res/values/strings.xml | 2 + 17 files changed, 2564 insertions(+), 669 deletions(-) create mode 100644 app/src/main/java/com/example/dattingapp/LikeANdMatchActivity.kt delete mode 100644 app/src/main/java/com/example/dattingapp/LikeAndMatchScreen.kt create mode 100644 app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt create mode 100644 app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt create mode 100644 app/src/main/java/com/example/dattingapp/homes/profile/FrienProfileResponse.kt create mode 100644 app/src/main/java/com/example/dattingapp/homes/profile/UserResponseData.kt 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