Browse Source

update comment

master
osman160128 2 weeks ago
parent
commit
770e5767fc
  1. 4
      app/build.gradle.kts
  2. 6
      app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt
  3. 166
      app/src/main/java/com/example/dattingapp/ShowPostingActivity.kt
  4. 2
      app/src/main/java/com/example/dattingapp/dataclass/DataClass.kt
  5. 440
      app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt
  6. 42
      app/src/main/java/com/example/dattingapp/homes/HomeActivity.kt
  7. 2
      app/src/main/java/com/example/dattingapp/homes/MyProfile.kt
  8. 11
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeApiService.kt
  9. 139
      app/src/main/java/com/example/dattingapp/homes/networkandapi/HomeViewModelClass.kt
  10. 12
      app/src/main/java/com/example/dattingapp/homes/networkandapi/TokenManager.kt
  11. 17
      app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt
  12. 8
      app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt
  13. 19
      app/src/main/java/com/example/dattingapp/ui/theme/Color.kt
  14. 2
      gradle/libs.versions.toml

4
app/build.gradle.kts

@ -41,6 +41,7 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
@ -90,9 +91,6 @@ dependencies {
// For Hilt ViewModel support in Jetpack Compose (if using Compose) // For Hilt ViewModel support in Jetpack Compose (if using Compose)
implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
// For Hilt support in Fragment / Activity
implementation("androidx.fragment:fragment-ktx:1.8.3")

6
app/src/main/java/com/example/dattingapp/Login/LoginActivity.kt

@ -3,6 +3,7 @@ package com.example.dattingapp.Login
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -269,12 +270,15 @@ fun LoginScreenContent(
// Save retrieved tokens via TokenManager // Save retrieved tokens via TokenManager
val tokenManager = TokenManager.getInstance(context) val tokenManager = TokenManager.getInstance(context)
val id=loginData.user?.profile?.full_name
Log.d("useridlogin","$id")
tokenManager.saveTokens( tokenManager.saveTokens(
accessToken = loginData.accessToken ?: "", accessToken = loginData.accessToken ?: "",
refreshToken = loginData.refreshToken ?: "", refreshToken = loginData.refreshToken ?: "",
userId = loginData.user?.id ?: -1,
currenUserName = loginData.user?.profile?.full_name
) )
Toast.makeText(context, "Login successful!", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Login successful!", Toast.LENGTH_SHORT).show()
onLoginSuccess() onLoginSuccess()
} else { } else {

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

@ -1,5 +1,4 @@
package com.example.dattingapp package com.example.dattingapp
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -26,6 +25,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import android.util.Log import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@ -43,9 +43,14 @@ import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.networkandapi.ShowPostUiState import com.example.dattingapp.homes.networkandapi.ShowPostUiState
import com.example.dattingapp.homes.networkandapi.ShowPostViewModel import com.example.dattingapp.homes.networkandapi.ShowPostViewModel
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.CardDefaults
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.example.dattingapp.dataclass.CommentResponse import com.example.dattingapp.dataclass.CommentResponse
import com.example.dattingapp.dataclass.Location import com.example.dattingapp.dataclass.Location
import com.example.dattingapp.homes.networkandapi.TokenManager
import com.example.dattingapp.ui.theme.TextPostTextColor
val AccentPink = Color(0xFFD81B60) val AccentPink = Color(0xFFD81B60)
val CommentBubbleBg = Color(0xFFF6F6F6) val CommentBubbleBg = Color(0xFFF6F6F6)
@ -174,6 +179,11 @@ fun PostCard(
} }
// --- 2. Post Text Content --- // --- 2. Post Text Content ---
// --- 3. Media Carousel (Images) ---
if (post.media.isNotEmpty()) {
if (!post.content.isNullOrEmpty()) { if (!post.content.isNullOrEmpty()) {
Text( Text(
text = post.content, text = post.content,
@ -183,14 +193,13 @@ fun PostCard(
.padding(horizontal = 12.dp, vertical = 4.dp) .padding(horizontal = 12.dp, vertical = 4.dp)
) )
} }
Card(
// --- 3. Media Carousel (Images) --- shape = RoundedCornerShape(16.dp),
if (post.media.isNotEmpty()) {
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(350.dp) .padding(start = 10.dp, end = 10.dp)
.padding(vertical = 4.dp) .height(360.dp),
) { ) {
AsyncImage( AsyncImage(
model = post.media.first().url, model = post.media.first().url,
@ -199,6 +208,36 @@ fun PostCard(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
}else{
Card(
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp)
.height(360.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White // your background color
)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = post.content.orEmpty(),
color = TextPostTextColor,
fontSize = if ((post.content?.length ?: 0) > 120) 18.sp else 24.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
maxLines = 8,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
} }
// --- 4. Interaction Buttons --- // --- 4. Interaction Buttons ---
@ -232,21 +271,32 @@ fun PostCard(
CommentSection( CommentSection(
comments = comments, comments = comments,
postId = post.id, postId = post.id,
currentUserName = post.author?.full_name // see note below postUserName = post.author?.full_name // see note below
) )
} }
} }
@Composable @Composable
fun CommentSection( fun CommentSection(
comments: List<CommentResponse>, comments: List<CommentResponse>,
postId: Int, postId: Int,
currentUserName: String?, postUserName: String?,
postUserId: Int? = null,
viewModel: ShowPostViewModel = viewModel() viewModel: ShowPostViewModel = viewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
var newCommentText by remember { mutableStateOf("") } var newCommentText by remember { mutableStateOf("") }
var menuCommentId by remember { mutableStateOf<Int?>(null) }
var editingCommentId by remember { mutableStateOf<Int?>(null) }
var editText by remember { mutableStateOf("") }
val tokenManager = remember { TokenManager.getInstance(context) }
val currentUserName = remember { tokenManager.currentUserName() }
// ✅ get the logged-in user's id once
val currentUserId = remember { tokenManager.getUserId() }
Column( Column(
modifier = Modifier modifier = Modifier
@ -259,7 +309,8 @@ fun CommentSection(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
) )
// Bottom Comment Input Field Bar
// Comment input bar
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -307,8 +358,25 @@ fun CommentSection(
) )
} }
} }
// List of fetched comments
// List of comments
comments.reversed().forEach { comment -> comments.reversed().forEach { comment ->
val isEditing = editingCommentId == comment.id
// ✅ Permission checks per comment
// adjust `comment.author?.id` to your actual model field (e.g. comment.user_id)
val commentAuthorId = comment.author?.id
val isCommentOwner = commentAuthorId != null && commentAuthorId == currentUserId
val isPostOwner = postUserId != null && postUserId == currentUserId
val canEdit = isCommentOwner
val canDelete = isCommentOwner || isPostOwner
val hasAnyAction = canEdit || canDelete
Log.d(
"CommentPerm",
"current=$currentUserId | commentAuthor=$commentAuthorId | postOwner=$postUserId | canEdit=$canEdit canDelete=$canDelete"
)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -331,10 +399,16 @@ fun CommentSection(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Box(modifier = Modifier.weight(1f)) {
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.background(CommentBubbleBg, shape = RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(CommentBubbleBg)
// ✅ only open the menu if the user can actually do something
.clickable(enabled = !isEditing && hasAnyAction) {
menuCommentId = comment.id
}
.padding(horizontal = 12.dp, vertical = 8.dp) .padding(horizontal = 12.dp, vertical = 8.dp)
) { ) {
Text( Text(
@ -343,17 +417,79 @@ fun CommentSection(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
if (isEditing) {
BasicTextField(
value = editText,
onValueChange = { editText = it },
modifier = Modifier.fillMaxWidth(),
textStyle = MaterialTheme.typography.bodyMedium
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
editingCommentId = null
editText = ""
}) { Text("Cancel") }
TextButton(onClick = {
if (editText.isNotBlank()) {
// viewModel.editComment(
// context = context,
// postId = postId,
// commentId = comment.id,
// newContent = editText
// )
editingCommentId = null
editText = ""
}
}) { Text("Save", color = AccentPink) }
}
} else {
Text( Text(
text = comment.content, text = comment.content,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
} }
} }
DropdownMenu(
expanded = menuCommentId == comment.id,
onDismissRequest = { menuCommentId = null }
) {
// ✅ Edit: only the comment author
if (canEdit) {
DropdownMenuItem(
text = { Text("Edit") },
onClick = {
editText = comment.content
editingCommentId = comment.id
menuCommentId = null
}
)
}
// ✅ Delete: comment author OR post owner
if (canDelete) {
DropdownMenuItem(
text = { Text("Delete") },
onClick = {
menuCommentId = null
viewModel.deleteComment(
context = context,
postId = postId,
commentId = comment.id
)
}
)
}
}
}
}
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)

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

@ -15,7 +15,7 @@ data class ShowPostResponse(
@SerialName("like_count") val like_count: Int, @SerialName("like_count") val like_count: Int,
@SerialName("comment_count") val comment_count: Int = 0, @SerialName("comment_count") val comment_count: Int = 0,
@SerialName("liked_by_me") val liked_by_me: Boolean = false, @SerialName("liked_by_me") val liked_by_me: Boolean = false,
val media: List<MediaItem> = emptyList(), @SerialName("media")val media: List<MediaItem> = emptyList(),
@SerialName("created_at") val createdAt: String? @SerialName("created_at") val createdAt: String?
) )

440
app/src/main/java/com/example/dattingapp/homes/ExploreScreen.kt

@ -1,15 +1,18 @@
package com.example.dattingapp.homes package com.example.dattingapp.homes
import android.os.Bundle import android.content.Intent
import androidx.activity.ComponentActivity import androidx.compose.foundation.BorderStroke
import androidx.activity.compose.setContent import androidx.media3.common.MediaItem as ExoMediaItem
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -20,27 +23,38 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import coil.compose.AsyncImage
import com.example.dattingapp.ShowPostingActivity
import com.example.dattingapp.dataclass.Author
import com.example.dattingapp.dataclass.Location
import com.example.dattingapp.dataclass.MediaItem
import com.example.dattingapp.dataclass.ShowPostResponse
import com.example.dattingapp.homes.networkandapi.PostResponse
import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.ui.theme.DattingAPPTheme import com.example.dattingapp.ui.theme.DattingAPPTheme
import com.example.dattingapp.ui.theme.OverlayShadow
import com.example.dattingapp.ui.theme.TextPostTextColor
import com.example.dattingapp.ui.theme.lightChipBg import com.example.dattingapp.ui.theme.lightChipBg
import com.example.dattingapp.ui.theme.primaryColor import com.example.dattingapp.ui.theme.primaryColor
class MeetActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
DattingAPPTheme {
VibeCheckHomeScreen()
}
}
}
}
// Data Models // Data Models
data class VibeCategory( data class VibeCategory(
@ -68,10 +82,13 @@ data class UserMatch(
@Composable @Composable
fun VibeCheckHomeScreen( fun VibeCheckHomeScreen(
modifier: Modifier = Modifier feedResponse: List<ShowPostResponse> = emptyList(), // Change from MyProfileData? to MyProfileResponse?
isLoading: Boolean,
errorMessage: String?,
onRetry: () -> Unit = {},
) { ) {
Scaffold( Scaffold(
modifier = modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
VibeCheckTopAppBar(primaryColor) VibeCheckTopAppBar(primaryColor)
} }
@ -87,45 +104,22 @@ fun VibeCheckHomeScreen(
SearchBarSection() SearchBarSection()
} }
item { // item {
VibeFilterChipsSection( // VibeFilterChipsSection(
selectedColor = primaryColor, // selectedColor = primaryColor,
unselectedBg = lightChipBg // unselectedBg = lightChipBg
) // )
} // }
item { // One card per post from the API (replaces both hardcoded cards)
items(feedResponse, key = { it.id }) { post ->
FeaturedEventCard( FeaturedEventCard(
data = EventCardData( post = post,
id = "1",
title = "Coffee & Pottery",
description = "Get your hands dirty and your caffeine fix. Perfect for a low-pressure first date.",
imageUrl = "",
tagLabel = "Live",
isTrending = false,
tags = listOf("Creative", "Chill")
),
badgeColor = primaryColor badgeColor = primaryColor
) )
} }
item {
MostMatchingSection(primaryColor)
}
item {
FeaturedEventCard(
data = EventCardData(
id = "2",
title = "Sunset Beach Volley",
description = "Join a casual pickup game down at the pier. Bring sunscreen and good vibes.",
imageUrl = "",
tagLabel = "TRENDING",
isTrending = true
),
badgeColor = Color(0xFF7C4DFF)
)
}
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
@ -266,56 +260,103 @@ private fun VibeFilterChipsSection(
@Composable @Composable
private fun FeaturedEventCard( private fun FeaturedEventCard(
data: EventCardData, post: ShowPostResponse,
badgeColor: Color badgeColor: Color
) { ) {
val context = LocalContext.current
val hasMedia = post.media.isNotEmpty()
val tags = listOfNotNull(post.feeling, post.location?.name)
// White over photos, dark over the white text-only card
val contentColor = if (hasMedia) Color.White else TextPostTextColor
val shadowStyle = if (hasMedia) TextStyle(shadow = OverlayShadow) else TextStyle.Default
Card( Card(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
border = if (hasMedia) null else BorderStroke(1.dp, Color(0xFFEDEDED)),
colors = CardDefaults.cardColors(
containerColor = if (hasMedia) Color.DarkGray else Color.White
),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(360.dp) .height(360.dp)
.clickable {
val intent = Intent(context, ShowPostingActivity::class.java).apply {
putExtra("Post_ID", post.id)
}
context.startActivity(intent)
}
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
// 1. Content: swipeable media, or big centered text
if (hasMedia) {
PostMediaPager(media = post.media, modifier = Modifier.fillMaxSize())
// 2. Top scrim (keeps the badge readable)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.background(Color.DarkGray) .height(90.dp)
.align(Alignment.TopCenter)
.background(
Brush.verticalGradient(
listOf(Color.Black.copy(alpha = 0.45f), Color.Transparent)
)
)
) )
// 3. Bottom scrim (keeps author, text and tags readable)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.height(180.dp)
.align(Alignment.BottomCenter)
.background( .background(
Brush.verticalGradient( Brush.verticalGradient(
colors = listOf( listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f))
Color.Transparent, )
Color.Black.copy(alpha = 0.85f)
),
startY = 200f
) )
) )
} else {
Text(
text = post.content.orEmpty(),
color = TextPostTextColor,
fontSize = if ((post.content?.length ?: 0) > 120) 18.sp else 24.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
maxLines = 8,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 32.dp)
) )
}
// 4. Badge (it has its own colored background, so white text is fine)
Surface( Surface(
color = badgeColor, color = badgeColor,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
modifier = Modifier modifier = Modifier.padding(12.dp).align(Alignment.TopStart)
.padding(12.dp)
.align(Alignment.TopStart)
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = if (!data.isTrending) Icons.Default.RadioButtonChecked else Icons.Default.Bolt, imageVector = when (post.postType) {
"video" -> Icons.Default.Videocam
"image" -> Icons.Default.Image
else -> Icons.Default.TextFields
},
contentDescription = null, contentDescription = null,
tint = Color.White, tint = Color.White,
modifier = Modifier.size(12.dp) modifier = Modifier.size(12.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text( Text(
text = data.tagLabel, text = post.postType?.uppercase() ?: "POST",
color = Color.White, color = Color.White,
fontSize = 10.sp, fontSize = 10.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -323,69 +364,89 @@ private fun FeaturedEventCard(
} }
} }
if (!data.isTrending) { // 5. Like / comment, on a dark pill when over a photo
Column( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.BottomEnd)
.padding(end = 12.dp), .padding(end = 12.dp, bottom = 16.dp)
verticalArrangement = Arrangement.spacedBy(12.dp) .clip(RoundedCornerShape(24.dp))
.padding(vertical = 4.dp, horizontal = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) // space BETWEEN like and comment groups
) { ) {
IconButton(onClick = { }) { // Like + count
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon( Icon(
imageVector = Icons.Outlined.ThumbUp, imageVector = if (post.liked_by_me) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = "Like", contentDescription = "Like",
tint = Color.White tint = if (post.liked_by_me) Color.Red else contentColor,
modifier = Modifier
.size(28.dp)
.clickable { }
)
Text(
text = "${post.like_count}",
color = contentColor,
fontSize = 12.sp,
style = shadowStyle
) )
} }
IconButton(onClick = { }) {
// Comment + count
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon( Icon(
imageVector = Icons.Outlined.ChatBubbleOutline, imageVector = Icons.Outlined.ChatBubbleOutline,
contentDescription = "Comment", contentDescription = "Comment",
tint = Color.White tint = contentColor,
modifier = Modifier
.size(28.dp)
.clickable { }
) )
} Text(
IconButton(onClick = { }) { text = "${post.comment_count}",
Icon( color = contentColor,
imageVector = Icons.Outlined.BookmarkBorder, fontSize = 12.sp,
contentDescription = "Save", style = shadowStyle
tint = Color.White
) )
} }
} }
}
Column( // 6. Bottom info
modifier = Modifier Column(modifier = Modifier.align(Alignment.BottomStart)
.align(Alignment.BottomStart) .padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 72.dp)) {
.padding(16.dp)
) {
Text( Text(
text = data.title, text = post.author?.full_name ?: "Anonymous",
color = Color.White, color = contentColor,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
style = shadowStyle
) )
Spacer(modifier = Modifier.height(4.dp)) if (hasMedia && !post.content.isNullOrBlank()) {
Spacer(Modifier.height(4.dp))
Text( Text(
text = data.description, text = post.content,
color = Color.LightGray, color = Color.White,
fontSize = 12.sp, fontSize = 13.sp,
lineHeight = 16.sp lineHeight = 17.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = shadowStyle
) )
}
if (data.tags.isNotEmpty()) { if (tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
data.tags.forEach { tag -> tags.forEach { tag ->
Surface( Surface(
color = Color.White.copy(alpha = 0.2f), color = if (hasMedia) Color.Black.copy(alpha = 0.45f)
else Color(0xFFF1F1F1),
shape = RoundedCornerShape(6.dp) shape = RoundedCornerShape(6.dp)
) { ) {
Text( Text(
text = tag, text = tag,
color = Color.White, color = contentColor,
fontSize = 10.sp, fontSize = 10.sp,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
) )
@ -397,6 +458,100 @@ private fun FeaturedEventCard(
} }
} }
} }
@Composable
private fun PostMediaPager(media: List<MediaItem>, modifier: Modifier = Modifier) {
val pagerState = rememberPagerState(pageCount = { media.size })
Box(modifier) {
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
val item = media[page]
if (item.mediaType == "video") {
PostVideoPlayer(
url = item.url,
isActive = pagerState.currentPage == page
)
} else {
AsyncImage(
model = item.url,
contentDescription = "Post media ${page + 1}",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
}
// "1/3" counter, only when there is more than one item
if (media.size > 1) {
Surface(
color = Color.Black.copy(alpha = 0.5f),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp)
) {
Text(
text = "${pagerState.currentPage + 1}/${media.size}",
color = Color.White,
fontSize = 11.sp,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
}
}
@Composable
private fun PostVideoPlayer(url: String, isActive: Boolean) {
val context = LocalContext.current
var isPlaying by remember { mutableStateOf(false) }
val player = remember(url) {
ExoPlayer.Builder(context).build().apply {
setMediaItem(ExoMediaItem.fromUri(url))
repeatMode = Player.REPEAT_MODE_ONE
prepare()
}
}
// Pause when the user swipes to another page
LaunchedEffect(isActive) {
if (!isActive) player.pause()
}
DisposableEffect(player) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(playing: Boolean) { isPlaying = playing }
}
player.addListener(listener)
onDispose {
player.removeListener(listener)
player.release()
}
}
Box(
modifier = Modifier
.fillMaxSize()
.clickable { if (player.isPlaying) player.pause() else player.play() },
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { ctx ->
PlayerView(ctx).apply {
this.player = player
useController = false
}
},
modifier = Modifier.fillMaxSize()
)
if (!isPlaying) {
Icon(
imageVector = Icons.Default.PlayCircle,
contentDescription = "Play",
tint = Color.White.copy(alpha = 0.9f),
modifier = Modifier.size(64.dp)
)
}
}
}
@Composable @Composable
private fun MostMatchingSection( private fun MostMatchingSection(
@ -460,19 +615,90 @@ private fun MostMatchingSection(
} }
} }
@Preview( private val mockFeed = listOf(
showBackground = true, ShowPostResponse(
showSystemUi = true, id = 1,
widthDp = 360, content = "Exploring the city and enjoying some coffee! ☕🏙️",
heightDp = 800 postType = "image",
contentFormat = "markdown",
feeling = "happy 😊",
location = Location(name = "Shanghai", latitude = 31.23, longitude = 121.47),
author = Author(
id = 49,
full_name = "Alex Rivera",
city = "Shanghai",
country = "China",
compatibilityPercentage = 95
),
like_count = 12,
comment_count = 3,
liked_by_me = true,
media = listOf(
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"
),
ShowPostResponse(
id = 2,
content = "Text only post with no media.",
postType = "text",
contentFormat = "markdown",
feeling = null,
location = null,
author = Author(
id = 54,
full_name = "Sarah",
city = "Beijing",
country = "China",
compatibilityPercentage = 88
),
like_count = 0,
comment_count = 0,
liked_by_me = false,
media = emptyList(),
createdAt = "2026-10-13T07:45:15"
)
) )
@Preview(showBackground = true, showSystemUi = true, widthDp = 360, heightDp = 800)
@Composable @Composable
fun VibeCheckHomeScreenPreview() { fun VibeCheckHomeScreenPreview() {
MaterialTheme { DattingAPPTheme {
Surface(modifier = Modifier.fillMaxSize()) {
VibeCheckHomeScreen( VibeCheckHomeScreen(
modifier = Modifier.fillMaxSize() feedResponse = mockFeed,
isLoading = false,
errorMessage = null
)
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Composable
fun VibeCheckHomeScreenLoadingPreview() {
DattingAPPTheme {
VibeCheckHomeScreen(
feedResponse = emptyList(),
isLoading = true,
errorMessage = null
) )
} }
} }
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Composable
fun VibeCheckHomeScreenErrorPreview() {
DattingAPPTheme {
VibeCheckHomeScreen(
feedResponse = emptyList(),
isLoading = false,
errorMessage = "Failed to load feed (401)"
)
}
} }

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

@ -33,6 +33,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.dattingapp.homes.networkandapi.ExploreFeedViewModel
import com.example.dattingapp.homes.networkandapi.FeedUiState
import com.example.dattingapp.homes.networkandapi.MeetUiState import com.example.dattingapp.homes.networkandapi.MeetUiState
import com.example.dattingapp.homes.networkandapi.MeetViewModel import com.example.dattingapp.homes.networkandapi.MeetViewModel
import com.example.dattingapp.homes.networkandapi.ProfileViewModel import com.example.dattingapp.homes.networkandapi.ProfileViewModel
@ -304,7 +306,45 @@ fun HomeScreen() {
// EXPLORE // EXPLORE
// ========================================= // =========================================
1 -> { 1 -> {
VibeCheckHomeScreen() // 1. Obtain instance of ViewModel and LocalContext
val feedViewModel: ExploreFeedViewModel = viewModel()
val context = LocalContext.current
// 2. Collect state from ViewModel
val uiState by feedViewModel.uiState.collectAsState()
// 3. Trigger network call once when screen loads
LaunchedEffect(Unit) {
feedViewModel.loadFeed(context)
}
// 4. Map UI state to Composable parameters
when (val state = uiState) {
is FeedUiState.Loading, FeedUiState.Idle -> {
VibeCheckHomeScreen(
feedResponse = emptyList(),
isLoading = true,
errorMessage = null,
// onRetry = { viewModel.meetUserResponse(context) }
)
}
is FeedUiState.Success -> {
VibeCheckHomeScreen(
feedResponse = state.posts, // or state.response depending on your data class naming
isLoading = false,
errorMessage = null,
)
}
is FeedUiState.Error -> {
VibeCheckHomeScreen(
feedResponse = emptyList(),
isLoading = false,
errorMessage = state.message,
onRetry = { feedViewModel.loadFeed(context) }
)
}
}
} }
// ========================================= // =========================================
// CREATE POST // CREATE POST

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

@ -36,6 +36,7 @@ import com.example.dattingapp.ui.theme.TextMuted
import com.example.dattingapp.homes.profile.ProfileData import com.example.dattingapp.homes.profile.ProfileData
import com.example.dattingapp.LikeANdMatchActivity import com.example.dattingapp.LikeANdMatchActivity
import android.content.Intent import android.content.Intent
import android.util.Log
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
@ -208,6 +209,7 @@ fun MyProfileScreen(
// BIO // BIO
// ================================================= // =================================================
Log.d("my id","${profileResponse?.id}" )
Text( Text(
text = profileResponse?.profile?.bio?:"", text = profileResponse?.profile?.bio?:"",

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

@ -15,6 +15,7 @@ import retrofit2.http.Multipart
import retrofit2.http.Part import retrofit2.http.Part
import com.example.dattingapp.homes.profile.UserResponse import com.example.dattingapp.homes.profile.UserResponse
import com.example.dattingapp.homes.profile.LikedUserResponse import com.example.dattingapp.homes.profile.LikedUserResponse
import retrofit2.http.DELETE
interface HomeApiService { interface HomeApiService {
@ -87,4 +88,14 @@ interface HomeApiService {
suspend fun getPostComments( suspend fun getPostComments(
@Path("post_id") postId: Int @Path("post_id") postId: Int
): Response<List<CommentResponse>> ): Response<List<CommentResponse>>
// ✅ what it should be (if baseUrl is just the domain)
@DELETE("api/v1/explore/comments/{comment_id}")
suspend fun deleteComment(@Path("comment_id") commentId: Int): Response<Unit>
@GET("api/v1/explore/feed")
suspend fun getExploreFeed(
@Query("limit") limit: Int = 20,
@Query("offset") offset: Int = 0,
): Response<List<ShowPostResponse>>
} }

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

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import android.net.Uri import android.net.Uri
import android.widget.Toast
import com.example.dattingapp.dataclass.Author import com.example.dattingapp.dataclass.Author
import com.example.dattingapp.dataclass.CommentResponse import com.example.dattingapp.dataclass.CommentResponse
import com.example.dattingapp.dataclass.CreateCommentRequest import com.example.dattingapp.dataclass.CreateCommentRequest
@ -24,6 +25,7 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.lang.Exception import java.lang.Exception
import com.example.dattingapp.homes.profile.LikedUserResponse import com.example.dattingapp.homes.profile.LikedUserResponse
import org.json.JSONObject
sealed class MeetUiState { sealed class MeetUiState {
object Idle : MeetUiState() object Idle : MeetUiState()
@ -704,5 +706,142 @@ class ShowPostViewModel : ViewModel() {
} }
} }
fun deleteComment(context: Context, postId: Int, commentId: Int) {
val appContext = context.applicationContext
val previousList = _commentsUiState.value
_commentsUiState.value = previousList.filter { it.id != commentId }
viewModelScope.launch {
try {
val response = HomeNetworkModule
.getApiService(appContext)
.deleteComment(commentId)
if (response.isSuccessful) {
Log.d("CommentError", "Delete success: code=${response.code()} commentId=$commentId")
fetchComments(appContext, postId)
} else {
// ---- read the RAW error body first, so nothing is lost ----
val rawError = try {
response.errorBody()?.string() ?: "(empty error body)"
} catch (e: Exception) {
"(failed to read error body: ${e.message})"
}
// ---- try to extract "detail" from the JSON ----
val detail = try {
JSONObject(rawError).optString("detail")
} catch (e: Exception) { "" }
// ---- human-readable reason per status code ----
val reason = when (response.code()) {
401 -> "Not authorized (token missing/expired — log in again)"
403 -> "Not allowed: $detail"
404 -> "Comment not found (wrong id or path)"
405 -> "Server doesn't accept DELETE on this path"
500 -> "Server error: $detail"
else -> detail.ifBlank { rawError }
}
// ---- FULL diagnosis in Logcat (filter: CommentError) ----
Log.e(
"CommentError",
"Delete failed\n" +
" code = ${response.code()}\n" +
" message = ${response.message()}\n" +
" url = ${response.raw().request.url}\n" +
" raw body = $rawError\n" +
" parsed = $reason"
)
_commentsUiState.value = previousList
Toast.makeText(
appContext,
"Delete failed (${response.code()}): $reason",
Toast.LENGTH_LONG
).show()
} }
} catch (e: Exception) {
// ---- network-level failure: no internet, timeout, DNS, SSL... ----
Log.e(
"CommentError",
"Exception while deleting commentId=$commentId: ${e.javaClass.simpleName}: ${e.message}",
e
)
_commentsUiState.value = previousList
Toast.makeText(
appContext,
"Delete failed: ${e.javaClass.simpleName} — ${e.message ?: "no details"}",
Toast.LENGTH_LONG
).show()
}
}
}
}
sealed interface FeedUiState {
object Idle:FeedUiState
object Loading : FeedUiState
data class Success(val posts: List<ShowPostResponse>, val endReached: Boolean) : FeedUiState
data class Error(val message: String) : FeedUiState
}
class ExploreFeedViewModel : ViewModel() {
private val _uiState = MutableStateFlow<FeedUiState>(FeedUiState.Loading)
val uiState: StateFlow<FeedUiState> = _uiState.asStateFlow()
private val pageSize = 20
private var isLoadingMore = false
fun loadFeed(context: Context) {
val appContext = context.applicationContext
viewModelScope.launch {
_uiState.value = FeedUiState.Loading
try {
val response = HomeNetworkModule.getApiService(appContext)
.getExploreFeed(limit = pageSize, offset = 0)
if (response.isSuccessful && response.body() != null) {
val posts = response.body()!!
_uiState.value = FeedUiState.Success(posts, endReached = posts.size < pageSize)
} else {
_uiState.value = FeedUiState.Error("Failed to load feed (${response.code()})")
}
} catch (e: Exception) {
Log.e("ExploreFeed", "Feed request failed", e)
_uiState.value = FeedUiState.Error("Request failed: ${e.javaClass.simpleName}")
}
}
}
fun loadMore(context: Context) {
val current = _uiState.value as? FeedUiState.Success ?: return
if (isLoadingMore || current.endReached) return
isLoadingMore = true
val appContext = context.applicationContext
viewModelScope.launch {
try {
val response = HomeNetworkModule.getApiService(appContext)
.getExploreFeed(limit = pageSize, offset = current.posts.size)
if (response.isSuccessful && response.body() != null) {
val newPosts = response.body()!!
_uiState.value = FeedUiState.Success(
posts = current.posts + newPosts,
endReached = newPosts.size < pageSize
)
}
} catch (e: Exception) {
Log.e("ExploreFeed", "Load more failed", e)
} finally {
isLoadingMore = false
}
}
}
}

12
app/src/main/java/com/example/dattingapp/homes/networkandapi/TokenManager.kt

@ -11,7 +11,7 @@ class TokenManager private constructor(context: Context) {
Context.MODE_PRIVATE Context.MODE_PRIVATE
) )
fun saveTokens(accessToken: String, refreshToken: String?) { fun saveTokens(accessToken: String, refreshToken: String?,userId: Int,currenUserName:String?) {
val cleanToken = accessToken.trim() val cleanToken = accessToken.trim()
.replace( .replace(
Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), Regex("^Bearer\\s+", RegexOption.IGNORE_CASE),
@ -22,12 +22,22 @@ class TokenManager private constructor(context: Context) {
val saved = preferences.edit() val saved = preferences.edit()
.putString(KEY_ACCESS_TOKEN, cleanToken) .putString(KEY_ACCESS_TOKEN, cleanToken)
.putString(KEY_REFRESH_TOKEN, refreshToken) .putString(KEY_REFRESH_TOKEN, refreshToken)
.putInt("user_id", userId)
.putString("currenUserName", currenUserName)
.commit() .commit()
// Do not log the token itself. // Do not log the token itself.
Log.d("TokenManager", "Tokens saved: $saved") Log.d("TokenManager", "Tokens saved: $saved")
} }
fun getUserId(): Int {
return preferences.getInt("user_id", -1)
}
fun currentUserName(): String? = preferences.getString("full_name", null)
fun getAccessToken(): String? { fun getAccessToken(): String? {
return preferences.getString(KEY_ACCESS_TOKEN, null) return preferences.getString(KEY_ACCESS_TOKEN, null)
?.trim() ?.trim()

17
app/src/main/java/com/example/dattingapp/registration/RegisterRequest.kt

@ -59,5 +59,20 @@ data class LoginResponse(
val refreshToken: String?, val refreshToken: String?,
@SerializedName("token_type") @SerializedName("token_type")
val tokenType: String? val tokenType: String?,
@SerializedName("user") // ✅ matches the JSON key
val user: LoginUser? = null
)
data class LoginUser(
@SerializedName("id")
val id: Int,
@SerializedName("profile")
val profile: LoginProfile? = null
)
data class LoginProfile(
@SerializedName("full_name")
val full_name: String? = null
) )

8
app/src/main/java/com/example/dattingapp/registration/RegistrationViewModel.kt

@ -37,10 +37,10 @@ class RegistrationViewModel : ViewModel() {
if (response.isSuccessful && response.body() != null) { if (response.isSuccessful && response.body() != null) {
val body = response.body()!! val body = response.body()!!
TokenManager.getInstance(context.applicationContext).saveTokens( // TokenManager.getInstance(context.applicationContext).saveTokens(
accessToken = body.accessToken, // accessToken = body.accessToken,
refreshToken = body.refreshToken // refreshToken = body.refreshToken
) // )
Log.d(TAG, "Registration successful. Access token saved.") Log.d(TAG, "Registration successful. Access token saved.")
_uiState.value = RegistrationUiState.Success(body) _uiState.value = RegistrationUiState.Success(body)

19
app/src/main/java/com/example/dattingapp/ui/theme/Color.kt

@ -1,7 +1,10 @@
package com.example.dattingapp.ui.theme package com.example.dattingapp.ui.theme
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.luminance
val Purple80 = Color(0xFFD0BCFF) val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC) val PurpleGrey80 = Color(0xFFCCC2DC)
@ -104,14 +107,6 @@ val RecipientBubbleBg = Color(0xFFC2185B)
val AppBackground = Color(0xFFF9F8FD) val AppBackground = Color(0xFFF9F8FD)
val SubtextColor = Color(0xFFB0BEC5) val SubtextColor = Color(0xFFB0BEC5)
val InputFieldBg = Color(0xFFF0EFF4) val InputFieldBg = Color(0xFFF0EFF4)
// Color Palette matching the UI
val ScreenBackground = Color(0xFFFAF9FD)
val SectionHeaderColor = Color(0xFF757575)
val CardBackgroundColor = Color.White
val LinkTextColor = Color(0xFF7B1FA2)
val UnreadDotColor = Color(0xFFD32F2F)
val gradientBrush = Brush.horizontalGradient( val gradientBrush = Brush.horizontalGradient(
colors = listOf( colors = listOf(
Color(0xFFB00040), // Dark Pink/Magenta Color(0xFFB00040), // Dark Pink/Magenta
@ -119,3 +114,11 @@ colors = listOf(
) )
) )
val purpleButtonColor = Color(0xFF6B00F5) val purpleButtonColor = Color(0xFF6B00F5)
// Same post id always gives the same color
val OverlayShadow = Shadow(
color = Color.Black.copy(alpha = 0.6f),
offset = Offset(0f, 2f),
blurRadius = 6f
)
val TextPostTextColor = Color(0xFF1E1E1E)

2
gradle/libs.versions.toml

@ -18,6 +18,7 @@ navigationFragmentKtx = "2.6.0"
navigationUiKtx = "2.6.0" navigationUiKtx = "2.6.0"
material3 = "1.4.0" material3 = "1.4.0"
ui = "1.12.0" ui = "1.12.0"
foundation = "1.12.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -44,6 +45,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3",
media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3UiVersion" } media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3UiVersion" }
media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3ExoplayerVersion" } media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3ExoplayerVersion" }
androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }

Loading…
Cancel
Save