30 changed files with 1719 additions and 1553 deletions
@ -0,0 +1 @@ |
|||
DattingAPP |
|||
@ -0,0 +1,5 @@ |
|||
import android.app.Application |
|||
import dagger.hilt.android.HiltAndroidApp |
|||
|
|||
@HiltAndroidApp |
|||
class MainApplication : Application() |
|||
@ -0,0 +1,19 @@ |
|||
package com.example.dattingapp |
|||
|
|||
|
|||
import android.app.Application |
|||
import dagger.hilt.android.HiltAndroidApp |
|||
|
|||
@HiltAndroidApp |
|||
class DatingApp : Application() { |
|||
|
|||
override fun onCreate() { |
|||
super.onCreate() |
|||
instance = this |
|||
} |
|||
|
|||
companion object { |
|||
lateinit var instance: DatingApp |
|||
private set |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
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.Scaffold |
|||
import androidx.compose.material3.Text |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.tooling.preview.Preview |
|||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|||
|
|||
class ExploreScreen : ComponentActivity() { |
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
enableEdgeToEdge() |
|||
setContent { |
|||
DattingAPPTheme { |
|||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> |
|||
Greeting2( |
|||
name = "Android", |
|||
modifier = Modifier.padding(innerPadding) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
fun Greeting2(name: String, modifier: Modifier = Modifier) { |
|||
Text( |
|||
text = "Hello $name!", |
|||
modifier = modifier |
|||
) |
|||
} |
|||
|
|||
@Preview(showBackground = true) |
|||
@Composable |
|||
fun GreetingPreview2() { |
|||
DattingAPPTheme { |
|||
Greeting2("Android") |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
package com.example.dattingapp |
|||
|
|||
import android.app.Application |
|||
import dagger.hilt.android.HiltAndroidApp |
|||
|
|||
@HiltAndroidApp |
|||
class MainApplication : Application() |
|||
@ -1,301 +0,0 @@ |
|||
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.Scaffold |
|||
import androidx.compose.material3.Text |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.tooling.preview.Preview |
|||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|||
import androidx.compose.foundation.Image |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material.icons.Icons |
|||
import androidx.compose.material.icons.filled.* |
|||
import androidx.compose.material.icons.outlined.Close |
|||
import androidx.compose.material.icons.outlined.LocationOn |
|||
import androidx.compose.material.icons.outlined.Search |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.draw.shadow |
|||
import androidx.compose.ui.graphics.Brush |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.layout.ContentScale |
|||
import androidx.compose.ui.res.painterResource |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import com.example.dattingapp.R |
|||
import com.example.dattingapp.ui.theme.BrandPink |
|||
import com.example.dattingapp.ui.theme.BrandPurple |
|||
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|||
|
|||
class MeetScreen : ComponentActivity() { |
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
enableEdgeToEdge() |
|||
setContent { |
|||
DattingAPPTheme { |
|||
MeetDiscoveryScreen() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@Composable |
|||
fun MeetDiscoveryScreen( |
|||
modifier: Modifier = Modifier, |
|||
onAddClick: () -> Unit = {}, |
|||
onSearchClick: () -> Unit = {}, |
|||
onSuperLikeClick: () -> Unit = {}, |
|||
onRejectClick: () -> Unit = {}, |
|||
onLikeClick: () -> Unit = {} |
|||
) { |
|||
Box( |
|||
modifier = modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF8F9FA)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 16.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
// TOP BAR |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(top = 12.dp, bottom = 12.dp), |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
IconButton( |
|||
onClick = onAddClick, |
|||
modifier = Modifier |
|||
.size(38.dp) |
|||
.clip(CircleShape) |
|||
.background(Color(0xFFEBEBEB)) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Add, |
|||
contentDescription = "Add", |
|||
tint = BrandPink, |
|||
modifier = Modifier.size(20.dp) |
|||
) |
|||
} |
|||
|
|||
Text( |
|||
text = "VibeCheck", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = BrandPurple |
|||
) |
|||
|
|||
IconButton(onClick = onSearchClick) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.Search, |
|||
contentDescription = "Search", |
|||
tint = BrandPink, |
|||
modifier = Modifier.size(26.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// PROFILE SWIPE CARD |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.weight(1f) |
|||
.padding(bottom = 16.dp) |
|||
.clip(RoundedCornerShape(20.dp)) |
|||
) { |
|||
// Background Profile Image |
|||
Image( |
|||
painter = painterResource(id = R.drawable.locationpermissionbg), |
|||
contentDescription = "User Profile Image", |
|||
modifier = Modifier.fillMaxSize(), |
|||
contentScale = ContentScale.Crop |
|||
) |
|||
|
|||
// Dark Bottom Gradient Overlay |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background( |
|||
Brush.verticalGradient( |
|||
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f)), |
|||
startY = 350f |
|||
) |
|||
) |
|||
) |
|||
|
|||
// RIGHT-SIDE ACTION BUTTONS (Moved lower using BottomEnd alignment) |
|||
Column( |
|||
modifier = Modifier |
|||
.align(Alignment.BottomEnd) |
|||
.padding(end = 14.dp, bottom = 20.dp), |
|||
verticalArrangement = Arrangement.spacedBy(10.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
// Star (Super Like) Button |
|||
IconButton( |
|||
onClick = onSuperLikeClick, |
|||
modifier = Modifier |
|||
.size(50.dp) |
|||
.shadow(6.dp, CircleShape) |
|||
.clip(CircleShape) |
|||
.background(Color.White) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Star, |
|||
contentDescription = "Super Like", |
|||
tint = BrandPurple, |
|||
modifier = Modifier.size(24.dp) |
|||
) |
|||
} |
|||
|
|||
// Pass (X) Button |
|||
IconButton( |
|||
onClick = onRejectClick, |
|||
modifier = Modifier |
|||
.size(54.dp) |
|||
.shadow(6.dp, CircleShape) |
|||
.clip(CircleShape) |
|||
.background(Color.White) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.Close, |
|||
contentDescription = "Reject", |
|||
tint = Color(0xFF555555), |
|||
modifier = Modifier.size(26.dp) |
|||
) |
|||
} |
|||
|
|||
// Like (Heart) Button |
|||
IconButton( |
|||
onClick = onLikeClick, |
|||
modifier = Modifier |
|||
.size(60.dp) |
|||
.shadow(8.dp, CircleShape) |
|||
.clip(CircleShape) |
|||
.background(BrandPink) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Favorite, |
|||
contentDescription = "Like", |
|||
tint = Color.White, |
|||
modifier = Modifier.size(30.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// BOTTOM PROFILE DETAILS |
|||
Column( |
|||
modifier = Modifier |
|||
.align(Alignment.BottomStart) |
|||
.padding(16.dp) |
|||
.fillMaxWidth(0.70f) |
|||
) { |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
Text( |
|||
text = "Alex, 24", |
|||
fontSize = 26.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.White |
|||
) |
|||
|
|||
// 98% Match Badge |
|||
Surface( |
|||
shape = RoundedCornerShape(10.dp), |
|||
color = BrandPink |
|||
) { |
|||
Row( |
|||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(2.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Verified, |
|||
contentDescription = null, |
|||
tint = Color.White, |
|||
modifier = Modifier.size(10.dp) |
|||
) |
|||
Text( |
|||
text = "98%", |
|||
color = Color.White, |
|||
fontSize = 10.sp, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(4.dp)) |
|||
|
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(4.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.LocationOn, |
|||
contentDescription = null, |
|||
tint = Color.White.copy(alpha = 0.9f), |
|||
modifier = Modifier.size(14.dp) |
|||
) |
|||
Text( |
|||
text = "2 miles away • Active recently", |
|||
fontSize = 13.sp, |
|||
color = Color.White.copy(alpha = 0.9f) |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(10.dp)) |
|||
|
|||
// INTEREST CHIPS |
|||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { |
|||
InterestChip(text = "Photography") |
|||
InterestChip(text = "Dogs") |
|||
} |
|||
Spacer(modifier = Modifier.height(6.dp)) |
|||
InterestChip(text = "Coffee Shop Lover") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun InterestChip(text: String) { |
|||
Surface( |
|||
shape = RoundedCornerShape(16.dp), |
|||
color = Color.White.copy(alpha = 0.3f) |
|||
) { |
|||
Text( |
|||
text = text, |
|||
color = Color.White, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
@Preview(showBackground = true, showSystemUi = true) |
|||
@Composable |
|||
fun MeetDiscoveryScreenPreview() { |
|||
DattingAPPTheme { |
|||
MeetDiscoveryScreen() |
|||
} |
|||
} |
|||
@ -1,931 +0,0 @@ |
|||
package com.example.dattingapp |
|||
|
|||
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 |
|||
import androidx.compose.foundation.rememberScrollState |
|||
import androidx.compose.foundation.verticalScroll |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
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.* |
|||
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.res.painterResource |
|||
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 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 |
|||
|
|||
|
|||
// ============================================================ |
|||
// MY PROFILE ACTIVITY |
|||
// ============================================================ |
|||
|
|||
class MyProfile : ComponentActivity() { |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
|
|||
super.onCreate(savedInstanceState) |
|||
|
|||
enableEdgeToEdge() |
|||
|
|||
setContent { |
|||
|
|||
DattingAPPTheme { |
|||
|
|||
MyProfileScreen( |
|||
|
|||
onEditClick = { |
|||
|
|||
Toast.makeText( |
|||
this, |
|||
"Edit Profile Clicked", |
|||
Toast.LENGTH_SHORT |
|||
).show() |
|||
|
|||
// TODO: |
|||
// startActivity( |
|||
// Intent( |
|||
// this, |
|||
// EditProfileActivity::class.java |
|||
// ) |
|||
// ) |
|||
}, |
|||
|
|||
onUpgradeClick = { |
|||
|
|||
Toast.makeText( |
|||
this, |
|||
"Upgrade to Premium Clicked", |
|||
Toast.LENGTH_SHORT |
|||
).show() |
|||
|
|||
}, |
|||
|
|||
onSettingsClick = { |
|||
|
|||
Toast.makeText( |
|||
this, |
|||
"Settings Clicked", |
|||
Toast.LENGTH_SHORT |
|||
).show() |
|||
|
|||
// TODO: |
|||
// startActivity( |
|||
// Intent( |
|||
// this, |
|||
// SettingsActivity::class.java |
|||
// ) |
|||
// ) |
|||
} |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
// ============================================================ |
|||
// MY PROFILE SCREEN |
|||
// ============================================================ |
|||
|
|||
@Composable |
|||
fun MyProfileScreen( |
|||
|
|||
onEditClick: () -> Unit = {}, |
|||
|
|||
onUpgradeClick: () -> Unit = {}, |
|||
|
|||
onSettingsClick: () -> Unit = {} |
|||
|
|||
) { |
|||
|
|||
var selectedTab by remember { |
|||
|
|||
mutableIntStateOf(0) |
|||
} |
|||
|
|||
|
|||
// ======================================================== |
|||
// FULL SCREEN SCROLL |
|||
// ======================================================== |
|||
|
|||
val scrollState = |
|||
rememberScrollState() |
|||
|
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color.White) |
|||
.verticalScroll(scrollState) |
|||
) { |
|||
|
|||
|
|||
// ==================================================== |
|||
// HEADER |
|||
// ==================================================== |
|||
|
|||
Box( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(260.dp) |
|||
) { |
|||
|
|||
|
|||
// ================================================= |
|||
// COVER IMAGE |
|||
// ================================================= |
|||
|
|||
Image( |
|||
|
|||
painter = |
|||
painterResource( |
|||
id = |
|||
R.drawable.locationpermissionbg |
|||
), |
|||
|
|||
contentDescription = |
|||
"Cover Banner", |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(200.dp), |
|||
|
|||
contentScale = |
|||
ContentScale.Crop |
|||
) |
|||
|
|||
|
|||
// ================================================= |
|||
// SETTINGS BUTTON |
|||
// ================================================= |
|||
|
|||
IconButton( |
|||
|
|||
onClick = |
|||
onSettingsClick, |
|||
|
|||
modifier = Modifier |
|||
.align( |
|||
Alignment.TopEnd |
|||
) |
|||
.padding( |
|||
top = 16.dp, |
|||
end = 16.dp |
|||
) |
|||
.size(36.dp) |
|||
.clip( |
|||
CircleShape |
|||
) |
|||
.background( |
|||
Color.Black.copy( |
|||
alpha = 0.3f |
|||
) |
|||
) |
|||
) { |
|||
|
|||
Icon( |
|||
|
|||
imageVector = |
|||
Icons.Outlined.Settings, |
|||
|
|||
contentDescription = |
|||
"Settings", |
|||
|
|||
tint = |
|||
Color.White, |
|||
|
|||
modifier = |
|||
Modifier.size(20.dp) |
|||
) |
|||
} |
|||
|
|||
|
|||
// ================================================= |
|||
// PROFILE IMAGE |
|||
// ================================================= |
|||
|
|||
Box( |
|||
|
|||
modifier = Modifier |
|||
.align( |
|||
Alignment.BottomStart |
|||
) |
|||
.padding( |
|||
start = 24.dp |
|||
) |
|||
.size(110.dp) |
|||
) { |
|||
|
|||
|
|||
Image( |
|||
|
|||
painter = |
|||
painterResource( |
|||
id = |
|||
R.drawable.locationpermissionbg |
|||
), |
|||
|
|||
contentDescription = |
|||
"Profile Picture", |
|||
|
|||
contentScale = |
|||
ContentScale.Crop, |
|||
|
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.clip( |
|||
CircleShape |
|||
) |
|||
.border( |
|||
width = 4.dp, |
|||
color = Color.White, |
|||
shape = CircleShape |
|||
) |
|||
) |
|||
|
|||
|
|||
// ================================================= |
|||
// VERIFIED BADGE |
|||
// ================================================= |
|||
|
|||
Box( |
|||
|
|||
modifier = Modifier |
|||
.align( |
|||
Alignment.BottomEnd |
|||
) |
|||
.size(28.dp) |
|||
.clip( |
|||
CircleShape |
|||
) |
|||
.background( |
|||
BrandMagenta |
|||
) |
|||
.border( |
|||
width = 2.dp, |
|||
color = Color.White, |
|||
shape = CircleShape |
|||
), |
|||
|
|||
contentAlignment = |
|||
Alignment.Center |
|||
) { |
|||
|
|||
Icon( |
|||
|
|||
imageVector = |
|||
Icons.Default.Check, |
|||
|
|||
contentDescription = |
|||
"Verified", |
|||
|
|||
tint = |
|||
Color.White, |
|||
|
|||
modifier = |
|||
Modifier.size(16.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
// ==================================================== |
|||
// USER INFORMATION |
|||
// ==================================================== |
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding( |
|||
horizontal = 24.dp, |
|||
vertical = 8.dp |
|||
) |
|||
) { |
|||
|
|||
|
|||
// ================================================= |
|||
// NAME + EDIT |
|||
// ================================================= |
|||
|
|||
Row( |
|||
|
|||
verticalAlignment = |
|||
Alignment.CenterVertically, |
|||
|
|||
horizontalArrangement = |
|||
Arrangement.spacedBy(8.dp) |
|||
) { |
|||
|
|||
Text( |
|||
|
|||
text = |
|||
"Alex, 28", |
|||
|
|||
fontSize = |
|||
22.sp, |
|||
|
|||
fontWeight = |
|||
FontWeight.Bold, |
|||
|
|||
color = |
|||
BrandMagenta |
|||
) |
|||
|
|||
|
|||
// EDIT BUTTON |
|||
|
|||
OutlinedButton( |
|||
|
|||
onClick = |
|||
onEditClick, |
|||
|
|||
modifier = |
|||
Modifier.height(30.dp), |
|||
|
|||
contentPadding = |
|||
PaddingValues( |
|||
horizontal = 10.dp, |
|||
vertical = 0.dp |
|||
), |
|||
|
|||
border = |
|||
ButtonDefaults |
|||
.outlinedButtonBorder |
|||
.copy( |
|||
width = 1.dp |
|||
) |
|||
) { |
|||
|
|||
Icon( |
|||
|
|||
imageVector = |
|||
Icons.Outlined.Edit, |
|||
|
|||
contentDescription = |
|||
null, |
|||
|
|||
tint = |
|||
BrandMagenta, |
|||
|
|||
modifier = |
|||
Modifier.size(14.dp) |
|||
) |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.width(4.dp) |
|||
) |
|||
|
|||
|
|||
Text( |
|||
|
|||
text = |
|||
"Edit", |
|||
|
|||
fontSize = |
|||
12.sp, |
|||
|
|||
color = |
|||
BrandMagenta, |
|||
|
|||
fontWeight = |
|||
FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(4.dp) |
|||
) |
|||
|
|||
|
|||
// ================================================= |
|||
// BIO |
|||
// ================================================= |
|||
|
|||
Text( |
|||
|
|||
text = |
|||
"Adventure seeker, dog lover, and amateur chef. Let's find a vibe!", |
|||
|
|||
fontSize = |
|||
13.sp, |
|||
|
|||
color = |
|||
TextMuted, |
|||
|
|||
lineHeight = |
|||
18.sp |
|||
) |
|||
} |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(12.dp) |
|||
) |
|||
|
|||
|
|||
// ==================================================== |
|||
// PREMIUM BANNER |
|||
// ==================================================== |
|||
|
|||
Box( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding( |
|||
horizontal = 16.dp |
|||
) |
|||
.clip( |
|||
RoundedCornerShape(16.dp) |
|||
) |
|||
.background( |
|||
|
|||
brush = |
|||
Brush.horizontalGradient( |
|||
|
|||
colors = |
|||
listOf( |
|||
BrandMagenta, |
|||
BrandPurple |
|||
) |
|||
) |
|||
) |
|||
.padding(16.dp) |
|||
) { |
|||
|
|||
Row( |
|||
|
|||
modifier = |
|||
Modifier.fillMaxWidth(), |
|||
|
|||
horizontalArrangement = |
|||
Arrangement.SpaceBetween, |
|||
|
|||
verticalAlignment = |
|||
Alignment.CenterVertically |
|||
) { |
|||
|
|||
Column { |
|||
|
|||
Text( |
|||
|
|||
text = |
|||
"VibeCheck Premium", |
|||
|
|||
color = |
|||
Color.White, |
|||
|
|||
fontSize = |
|||
16.sp, |
|||
|
|||
fontWeight = |
|||
FontWeight.Bold |
|||
) |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(2.dp) |
|||
) |
|||
|
|||
|
|||
Text( |
|||
|
|||
text = |
|||
"Unlock unlimited likes & more", |
|||
|
|||
color = |
|||
Color.White.copy( |
|||
alpha = 0.8f |
|||
), |
|||
|
|||
fontSize = |
|||
12.sp |
|||
) |
|||
} |
|||
|
|||
|
|||
// UPGRADE BUTTON |
|||
|
|||
Button( |
|||
|
|||
onClick = |
|||
onUpgradeClick, |
|||
|
|||
colors = |
|||
ButtonDefaults.buttonColors( |
|||
|
|||
containerColor = |
|||
Color.White |
|||
), |
|||
|
|||
shape = |
|||
RoundedCornerShape(20.dp), |
|||
|
|||
contentPadding = |
|||
PaddingValues( |
|||
horizontal = 14.dp, |
|||
vertical = 4.dp |
|||
) |
|||
) { |
|||
|
|||
Text( |
|||
|
|||
text = |
|||
"Upgrade", |
|||
|
|||
color = |
|||
BrandMagenta, |
|||
|
|||
fontSize = |
|||
12.sp, |
|||
|
|||
fontWeight = |
|||
FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(16.dp) |
|||
) |
|||
|
|||
|
|||
// ==================================================== |
|||
// STATISTICS |
|||
// ==================================================== |
|||
|
|||
Row( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding( |
|||
horizontal = 16.dp |
|||
), |
|||
|
|||
horizontalArrangement = |
|||
Arrangement.spacedBy(8.dp) |
|||
) { |
|||
|
|||
StatCard( |
|||
|
|||
number = |
|||
"24", |
|||
|
|||
label = |
|||
"Likes", |
|||
|
|||
modifier = |
|||
Modifier.weight(1f) |
|||
) |
|||
|
|||
|
|||
StatCard( |
|||
|
|||
number = |
|||
"12", |
|||
|
|||
label = |
|||
"Liked", |
|||
|
|||
modifier = |
|||
Modifier.weight(1f) |
|||
) |
|||
|
|||
|
|||
StatCard( |
|||
|
|||
number = |
|||
"Top", |
|||
|
|||
label = |
|||
"Match", |
|||
|
|||
numberColor = |
|||
BrandMagenta, |
|||
|
|||
modifier = |
|||
Modifier.weight(1f) |
|||
) |
|||
|
|||
|
|||
StatCard( |
|||
|
|||
number = |
|||
"8", |
|||
|
|||
label = |
|||
"Matches", |
|||
|
|||
modifier = |
|||
Modifier.weight(1f) |
|||
) |
|||
} |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(16.dp) |
|||
) |
|||
|
|||
|
|||
// ==================================================== |
|||
// TABS |
|||
// ==================================================== |
|||
|
|||
TabRow( |
|||
|
|||
selectedTabIndex = |
|||
selectedTab, |
|||
|
|||
containerColor = |
|||
Color.White, |
|||
|
|||
contentColor = |
|||
BrandMagenta, |
|||
|
|||
divider = { |
|||
|
|||
HorizontalDivider( |
|||
color = |
|||
Color(0xFFEEEEEE) |
|||
) |
|||
} |
|||
) { |
|||
|
|||
|
|||
// ================================================= |
|||
// GRID TAB |
|||
// ================================================= |
|||
|
|||
Tab( |
|||
|
|||
selected = |
|||
selectedTab == 0, |
|||
|
|||
onClick = { |
|||
|
|||
selectedTab = 0 |
|||
}, |
|||
|
|||
icon = { |
|||
|
|||
Icon( |
|||
|
|||
imageVector = |
|||
Icons.Outlined.GridView, |
|||
|
|||
contentDescription = |
|||
"Grid View" |
|||
) |
|||
} |
|||
) |
|||
|
|||
|
|||
// ================================================= |
|||
// TAGGED TAB |
|||
// ================================================= |
|||
|
|||
Tab( |
|||
|
|||
selected = |
|||
selectedTab == 1, |
|||
|
|||
onClick = { |
|||
|
|||
selectedTab = 1 |
|||
}, |
|||
|
|||
icon = { |
|||
|
|||
Icon( |
|||
|
|||
imageVector = |
|||
Icons.Outlined.PersonPin, |
|||
|
|||
contentDescription = |
|||
"Tagged View" |
|||
) |
|||
} |
|||
) |
|||
} |
|||
|
|||
|
|||
// ==================================================== |
|||
// MEDIA GRID |
|||
// ==================================================== |
|||
|
|||
ProfileMediaGrid() |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(30.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
|
|||
// ============================================================ |
|||
// PROFILE MEDIA GRID |
|||
// ============================================================ |
|||
|
|||
@Composable |
|||
private fun ProfileMediaGrid() { |
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(2.dp) |
|||
) { |
|||
|
|||
// 6 rows × 3 columns = 18 images |
|||
|
|||
repeat(6) { rowIndex -> |
|||
|
|||
Row( |
|||
|
|||
modifier = |
|||
Modifier.fillMaxWidth() |
|||
) { |
|||
|
|||
repeat(3) { columnIndex -> |
|||
|
|||
val imageIndex = |
|||
rowIndex * 3 + |
|||
columnIndex |
|||
|
|||
|
|||
Image( |
|||
|
|||
painter = |
|||
painterResource( |
|||
id = |
|||
R.drawable.locationpermissionbg |
|||
), |
|||
|
|||
contentDescription = |
|||
"Profile Image $imageIndex", |
|||
|
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.aspectRatio(1f) |
|||
.padding(1.dp), |
|||
|
|||
contentScale = |
|||
ContentScale.Crop |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
// ============================================================ |
|||
// STAT CARD |
|||
// ============================================================ |
|||
|
|||
@Composable |
|||
private fun StatCard( |
|||
|
|||
number: String, |
|||
|
|||
label: String, |
|||
|
|||
modifier: Modifier = Modifier, |
|||
|
|||
numberColor: Color = textDark |
|||
|
|||
) { |
|||
|
|||
Card( |
|||
|
|||
modifier = |
|||
modifier, |
|||
|
|||
shape = |
|||
RoundedCornerShape(12.dp), |
|||
|
|||
colors = |
|||
CardDefaults.cardColors( |
|||
|
|||
containerColor = |
|||
Color(0xFFFAFAFA) |
|||
), |
|||
|
|||
border = |
|||
CardDefaults |
|||
.outlinedCardBorder() |
|||
.copy( |
|||
width = 0.5.dp |
|||
) |
|||
) { |
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding( |
|||
vertical = 12.dp |
|||
), |
|||
|
|||
horizontalAlignment = |
|||
Alignment.CenterHorizontally |
|||
) { |
|||
|
|||
Text( |
|||
|
|||
text = |
|||
number, |
|||
|
|||
fontSize = |
|||
16.sp, |
|||
|
|||
fontWeight = |
|||
FontWeight.Bold, |
|||
|
|||
color = |
|||
numberColor |
|||
) |
|||
|
|||
|
|||
Spacer( |
|||
modifier = |
|||
Modifier.height(2.dp) |
|||
) |
|||
|
|||
|
|||
Text( |
|||
|
|||
text = |
|||
label, |
|||
|
|||
fontSize = |
|||
11.sp, |
|||
|
|||
color = |
|||
TextMuted |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
// ============================================================ |
|||
// PREVIEW |
|||
// ============================================================ |
|||
|
|||
@Preview( |
|||
|
|||
showBackground = true, |
|||
|
|||
showSystemUi = true, |
|||
|
|||
name = "My Profile" |
|||
) |
|||
@Composable |
|||
fun MyProfileScreenPreview() { |
|||
|
|||
DattingAPPTheme { |
|||
|
|||
MyProfileScreen() |
|||
} |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
package com.example.dattingapp |
|||
package com.example.dattingapp.homes |
|||
|
|||
import android.os.Bundle |
|||
import androidx.activity.ComponentActivity |
|||
@ -0,0 +1,385 @@ |
|||
package com.example.dattingapp.homes |
|||
|
|||
import android.util.Log |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material.icons.Icons |
|||
import androidx.compose.material.icons.filled.* |
|||
import androidx.compose.material.icons.outlined.Close |
|||
import androidx.compose.material.icons.outlined.LocationOn |
|||
import androidx.compose.material.icons.outlined.Search |
|||
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.shadow |
|||
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.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import coil.compose.AsyncImage |
|||
import coil.request.ImageRequest |
|||
import com.example.dattingapp.homes.networkandapi.MeetUserProfile |
|||
import com.example.dattingapp.homes.networkandapi.TokenManager |
|||
import com.example.dattingapp.ui.theme.BrandPink |
|||
import com.example.dattingapp.ui.theme.BrandPurple |
|||
import java.time.LocalDate |
|||
import java.time.Period |
|||
import java.time.format.DateTimeFormatter |
|||
|
|||
@Composable |
|||
fun MeetDiscoveryScreen( |
|||
modifier: Modifier = Modifier, |
|||
profiles: List<MeetUserProfile> = emptyList(), |
|||
isLoading: Boolean = false, |
|||
errorMessage: String? = null, |
|||
onAddClick: () -> Unit = {}, |
|||
onSearchClick: () -> Unit = {}, |
|||
onSuperLikeClick: (MeetUserProfile) -> Unit = {}, |
|||
onRejectClick: (MeetUserProfile) -> Unit = {}, |
|||
onLikeClick: (MeetUserProfile) -> Unit = {} |
|||
) { |
|||
// Index tracking current active user profile (starts at 0) |
|||
var currentIndex by remember { mutableIntStateOf(0) } |
|||
|
|||
Box( |
|||
modifier = modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF8F9FA)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 16.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
// TOP BAR |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(top = 12.dp, bottom = 12.dp), |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
IconButton( |
|||
onClick = onAddClick, |
|||
modifier = Modifier |
|||
.size(38.dp) |
|||
.clip(CircleShape) |
|||
.background(Color(0xFFEBEBEB)) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Add, |
|||
contentDescription = "Add", |
|||
tint = BrandPink, |
|||
modifier = Modifier.size(20.dp) |
|||
) |
|||
} |
|||
|
|||
Text( |
|||
text = "VibeCheck", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = BrandPurple |
|||
) |
|||
|
|||
IconButton(onClick = onSearchClick) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.Search, |
|||
contentDescription = "Search", |
|||
tint = BrandPink, |
|||
modifier = Modifier.size(26.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// MAIN CONTENT CARD |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.weight(1f) |
|||
.padding(bottom = 16.dp), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
when { |
|||
isLoading -> { |
|||
CircularProgressIndicator(color = BrandPink) |
|||
} |
|||
|
|||
errorMessage != null -> { |
|||
Text( |
|||
text = errorMessage, |
|||
color = MaterialTheme.colorScheme.error, |
|||
fontSize = 16.sp |
|||
) |
|||
} |
|||
|
|||
profiles.isNotEmpty() && currentIndex < profiles.size -> { |
|||
// Current profile starting at index 0 |
|||
val currentProfile = profiles[currentIndex] |
|||
|
|||
ProfileCardItem( |
|||
profile = currentProfile, |
|||
onSuperLike = { |
|||
onSuperLikeClick(currentProfile) |
|||
currentIndex++ |
|||
}, |
|||
onReject = { |
|||
onRejectClick(currentProfile) |
|||
currentIndex++ |
|||
}, |
|||
onLike = { |
|||
onLikeClick(currentProfile) |
|||
currentIndex++ |
|||
} |
|||
) |
|||
} |
|||
|
|||
else -> { |
|||
Text( |
|||
text = "No more profiles available!", |
|||
fontSize = 16.sp, |
|||
color = Color.Gray |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun ProfileCardItem( |
|||
profile: MeetUserProfile, |
|||
onSuperLike: () -> Unit, |
|||
onReject: () -> Unit, |
|||
onLike: () -> Unit |
|||
) { |
|||
// Extract primary photo URL from media list |
|||
val photoUrl = remember(profile) { |
|||
profile.media |
|||
?.firstOrNull { it.mediaType.equals("image", ignoreCase = true) || it.url != null } |
|||
?.fullMediaUrl |
|||
} |
|||
|
|||
Log.d("photourl", "$photoUrl") |
|||
// Helper to calculate age from birth_date string (formatted e.g. "YYYY-MM-DD") |
|||
val age = remember(profile.birthDate) { |
|||
calculateAge(profile.birthDate) |
|||
} |
|||
|
|||
// Location display formatting |
|||
val locationText = remember(profile) { |
|||
val loc = listOfNotNull(profile.city, profile.country).joinToString(", ") |
|||
val dist = profile.distanceKm?.let { "${it.toInt()} km away" } |
|||
listOfNotNull(dist, loc.takeIf { it.isNotBlank() }).joinToString(" • ") |
|||
} |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.clip(RoundedCornerShape(20.dp)) |
|||
) { |
|||
// BACKEND PROFILE IMAGE |
|||
val context = LocalContext.current |
|||
val token = remember(context) { |
|||
TokenManager.getInstance(context).getAccessToken() |
|||
} |
|||
|
|||
AsyncImage( |
|||
model = ImageRequest.Builder(context) |
|||
.data(photoUrl) |
|||
.apply { |
|||
if (!token.isNullOrBlank()) { |
|||
// Formatting matches "Bearer <token>" or raw token based on backend preference |
|||
val authHeader = if (token.startsWith("Bearer ", ignoreCase = true)) token else "Bearer $token" |
|||
addHeader("Authorization", authHeader) |
|||
} |
|||
} |
|||
.crossfade(true) |
|||
.build(), |
|||
contentDescription = "Profile Image", |
|||
modifier = Modifier.fillMaxSize(), |
|||
contentScale = ContentScale.Crop, |
|||
onError = { errorState -> |
|||
Log.e("CoilStatus", "Failed to load image", errorState.result.throwable) |
|||
} |
|||
) |
|||
|
|||
// DARK GRADIENT OVERLAY |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background( |
|||
Brush.verticalGradient( |
|||
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.85f)), |
|||
startY = 350f |
|||
) |
|||
) |
|||
) |
|||
|
|||
// ACTION BUTTONS (Right Side) |
|||
Column( |
|||
modifier = Modifier |
|||
.align(Alignment.BottomEnd) |
|||
.padding(end = 14.dp, bottom = 20.dp), |
|||
verticalArrangement = Arrangement.spacedBy(10.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
// Super Like (Star) |
|||
IconButton( |
|||
onClick = onSuperLike, |
|||
modifier = Modifier |
|||
.size(50.dp) |
|||
.shadow(6.dp, CircleShape) |
|||
.clip(CircleShape) |
|||
.background(Color.White) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Star, |
|||
contentDescription = "Super Like", |
|||
tint = BrandPurple, |
|||
modifier = Modifier.size(24.dp) |
|||
) |
|||
} |
|||
|
|||
// Reject (X) |
|||
IconButton( |
|||
onClick = onReject, |
|||
modifier = Modifier |
|||
.size(54.dp) |
|||
.shadow(6.dp, CircleShape) |
|||
.clip(CircleShape) |
|||
.background(Color.White) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.Close, |
|||
contentDescription = "Reject", |
|||
tint = Color(0xFF555555), |
|||
modifier = Modifier.size(26.dp) |
|||
) |
|||
} |
|||
|
|||
// Like (Heart) |
|||
IconButton( |
|||
onClick = onLike, |
|||
modifier = Modifier |
|||
.size(60.dp) |
|||
.shadow(8.dp, CircleShape) |
|||
.clip(CircleShape) |
|||
.background(BrandPink) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Favorite, |
|||
contentDescription = "Like", |
|||
tint = Color.White, |
|||
modifier = Modifier.size(30.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// PROFILE DETAILS (Bottom Left) |
|||
Column( |
|||
modifier = Modifier |
|||
.align(Alignment.BottomStart) |
|||
.padding(16.dp) |
|||
.fillMaxWidth(0.70f) |
|||
) { |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
// Name & Age |
|||
val displayName = profile.fullName ?: "User" |
|||
val nameAndAge = if (age != null) "$displayName, $age" else displayName |
|||
|
|||
Text( |
|||
text = nameAndAge, |
|||
fontSize = 26.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color.White |
|||
) |
|||
|
|||
// Compatibility / Match Percentage Badge |
|||
profile.compatibilityPercentage?.let { match -> |
|||
Surface( |
|||
shape = RoundedCornerShape(10.dp), |
|||
color = BrandPink |
|||
) { |
|||
Row( |
|||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(2.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Verified, |
|||
contentDescription = null, |
|||
tint = Color.White, |
|||
modifier = Modifier.size(10.dp) |
|||
) |
|||
Text( |
|||
text = "$match%", |
|||
color = Color.White, |
|||
fontSize = 10.sp, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(4.dp)) |
|||
|
|||
// Location & Distance |
|||
if (locationText.isNotBlank()) { |
|||
Row( |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(4.dp) |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Outlined.LocationOn, |
|||
contentDescription = null, |
|||
tint = Color.White.copy(alpha = 0.9f), |
|||
modifier = Modifier.size(14.dp) |
|||
) |
|||
Text( |
|||
text = locationText, |
|||
fontSize = 13.sp, |
|||
color = Color.White.copy(alpha = 0.9f) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// Bio |
|||
profile.bio?.takeIf { it.isNotBlank() }?.let { bio -> |
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
Text( |
|||
text = bio, |
|||
fontSize = 12.sp, |
|||
color = Color.White.copy(alpha = 0.85f), |
|||
maxLines = 2 |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Utility to calculate age from date string (e.g. "1999-05-15") |
|||
private fun calculateAge(birthDateStr: String?): Int? { |
|||
if (birthDateStr.isNull_or_blank()) 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 |
|||
} |
|||
} |
|||
|
|||
private fun String?.isNull_or_blank(): Boolean = this == null || this.trim().isEmpty() |
|||
@ -0,0 +1,445 @@ |
|||
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 |
|||
import androidx.compose.foundation.rememberScrollState |
|||
import androidx.compose.foundation.verticalScroll |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
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.* |
|||
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.res.painterResource |
|||
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 |
|||
import com.example.dattingapp.R |
|||
import com.example.dattingapp.homes.networkandapi.MyProfileResponse |
|||
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 |
|||
|
|||
// ============================================================ |
|||
// MY PROFILE SCREEN |
|||
// ============================================================ |
|||
|
|||
@Composable |
|||
fun MyProfileScreen( |
|||
|
|||
profile: MyProfileResponse?, // Change from MyProfileData? to MyProfileResponse? |
|||
isLoading: Boolean, |
|||
errorMessage: String?, |
|||
onRetry: () -> Unit = {}, |
|||
onEditClick: () -> Unit = {}, |
|||
onUpgradeClick: () -> Unit = {}, |
|||
onSettingsClick: () -> Unit = {} |
|||
|
|||
) { |
|||
|
|||
|
|||
// ======================================================== |
|||
// FULL SCREEN SCROLL |
|||
// ======================================================== |
|||
|
|||
val scrollState = rememberScrollState() |
|||
|
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier.fillMaxSize().background(Color.White).verticalScroll(scrollState)) { |
|||
|
|||
|
|||
// ==================================================== |
|||
// HEADER |
|||
// ==================================================== |
|||
|
|||
Box( |
|||
|
|||
modifier = Modifier.fillMaxWidth().height(260.dp) |
|||
) { |
|||
// ================================================= |
|||
// COVER IMAGE |
|||
// ================================================= |
|||
|
|||
Image( |
|||
painter = painterResource( |
|||
id = R.drawable.locationpermissionbg |
|||
), |
|||
|
|||
contentDescription = "Cover Banner", |
|||
|
|||
modifier = Modifier.fillMaxWidth().height(200.dp), |
|||
contentScale = ContentScale.Crop |
|||
) |
|||
// ================================================= |
|||
// SETTINGS BUTTON |
|||
// ================================================= |
|||
|
|||
IconButton( |
|||
onClick = onSettingsClick, |
|||
modifier = Modifier.align(Alignment.TopEnd) |
|||
.padding(top = 16.dp, end = 16.dp) |
|||
.size(36.dp) |
|||
.clip(CircleShape) |
|||
.background(Color.Black.copy(alpha = 0.3f) |
|||
) |
|||
) { |
|||
|
|||
Icon( |
|||
|
|||
imageVector = Icons.Outlined.Settings, |
|||
|
|||
contentDescription = "Settings", |
|||
tint = Color.White, |
|||
modifier = Modifier.size(20.dp) |
|||
) |
|||
} |
|||
// ================================================= |
|||
// PROFILE IMAGE |
|||
// ================================================= |
|||
Box( |
|||
modifier = Modifier.align(Alignment.BottomStart) |
|||
.padding(start = 24.dp) |
|||
.size(110.dp) |
|||
) { |
|||
|
|||
|
|||
AsyncImage( |
|||
// Access nested inner 'profile' first, then 'profileImageUrl' |
|||
model = profile?.profile?.profileImageUrl, |
|||
contentDescription = "Profile Picture", |
|||
contentScale = ContentScale.Crop, |
|||
placeholder = painterResource(id = R.drawable.locationpermissionbg), |
|||
error = painterResource(id = R.drawable.locationpermissionbg), |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.clip(CircleShape) |
|||
.border(width = 4.dp, color = Color.White, shape = CircleShape) |
|||
) |
|||
|
|||
// ================================================= |
|||
// VERIFIED BADGE |
|||
// ================================================= |
|||
Box( |
|||
modifier = Modifier.align(Alignment.BottomEnd).size(28.dp).clip(CircleShape) |
|||
.background(BrandMagenta) |
|||
.border(width = 2.dp, color = Color.White, shape = CircleShape |
|||
), |
|||
|
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Icon( |
|||
imageVector = Icons.Default.Check, |
|||
contentDescription = "Verified", |
|||
tint = Color.White, |
|||
modifier = Modifier.size(16.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
// ==================================================== |
|||
// USER INFORMATION |
|||
// ==================================================== |
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier.fillMaxWidth() |
|||
.padding(horizontal = 24.dp, vertical = 8.dp) |
|||
) { |
|||
// ================================================= |
|||
// NAME + EDIT |
|||
// ================================================= |
|||
|
|||
Row( |
|||
|
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
|
|||
Text( |
|||
text = "Alex, 28", |
|||
fontSize = 22.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = BrandMagenta |
|||
) |
|||
|
|||
|
|||
// EDIT BUTTON |
|||
|
|||
OutlinedButton( |
|||
onClick = onEditClick, |
|||
modifier = Modifier.height(30.dp), |
|||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), |
|||
border = ButtonDefaults |
|||
.outlinedButtonBorder |
|||
.copy(width = 1.dp) |
|||
) { |
|||
|
|||
Icon( |
|||
imageVector = Icons.Outlined.Edit, |
|||
contentDescription = null, |
|||
tint = BrandMagenta, |
|||
modifier = Modifier.size(14.dp) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.width(4.dp)) |
|||
|
|||
Text( |
|||
text = "Edit", |
|||
fontSize = 12.sp, |
|||
color = BrandMagenta, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(4.dp)) |
|||
// ================================================= |
|||
// BIO |
|||
// ================================================= |
|||
|
|||
Text( |
|||
|
|||
text = "Adventure seeker, dog lover, and amateur chef. Let's find a vibe!", |
|||
fontSize = 13.sp, |
|||
color = TextMuted, |
|||
lineHeight = 18.sp |
|||
) |
|||
} |
|||
|
|||
|
|||
Spacer( |
|||
modifier = Modifier.height(12.dp) |
|||
) |
|||
// ==================================================== |
|||
// PREMIUM BANNER |
|||
// ==================================================== |
|||
Box( |
|||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) |
|||
.clip(RoundedCornerShape(16.dp)) |
|||
.background( |
|||
brush = Brush.horizontalGradient( |
|||
colors = listOf(BrandMagenta, BrandPurple) |
|||
) |
|||
) |
|||
.padding(16.dp) |
|||
) { |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Column { |
|||
|
|||
Text( |
|||
text = "VibeCheck Premium", |
|||
color = Color.White, |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
|
|||
Spacer( |
|||
modifier = Modifier.height(2.dp) |
|||
) |
|||
|
|||
Text( |
|||
|
|||
text = "Unlock unlimited likes & more", |
|||
color = Color.White.copy(alpha = 0.8f), |
|||
fontSize = 12.sp |
|||
) |
|||
} |
|||
// UPGRADE BUTTON |
|||
|
|||
Button( |
|||
|
|||
onClick = onUpgradeClick, |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = Color.White |
|||
), |
|||
shape = RoundedCornerShape(20.dp), |
|||
contentPadding = PaddingValues( |
|||
horizontal = 14.dp, |
|||
vertical = 4.dp |
|||
) |
|||
) { |
|||
|
|||
Text( |
|||
|
|||
text = "Upgrade", |
|||
color = BrandMagenta, |
|||
fontSize = 12.sp, |
|||
fontWeight = FontWeight.Bold |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
Spacer( |
|||
modifier = Modifier.height(16.dp) |
|||
) |
|||
|
|||
// ==================================================== |
|||
// STATISTICS |
|||
// ==================================================== |
|||
|
|||
Row( |
|||
|
|||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
|
|||
StatCard( |
|||
number = "24", |
|||
label = "Likes", |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
|
|||
StatCard( |
|||
|
|||
number = "12", |
|||
label = "Liked", |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
|
|||
|
|||
StatCard( |
|||
number = "Top", |
|||
label = "Match", |
|||
numberColor = BrandMagenta, |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
StatCard( |
|||
number = "8", |
|||
label = "Matches", |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
} |
|||
Spacer( |
|||
modifier = Modifier.height(16.dp) |
|||
) |
|||
|
|||
ProfileMediaGrid() |
|||
|
|||
Spacer( |
|||
modifier = Modifier.height(30.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
|
|||
// ============================================================ |
|||
// PROFILE MEDIA GRID |
|||
// ============================================================ |
|||
|
|||
@Composable |
|||
private fun ProfileMediaGrid() { |
|||
|
|||
Column( |
|||
|
|||
modifier = Modifier.fillMaxWidth().padding(2.dp) |
|||
) { |
|||
// 6 rows × 3 columns = 18 images |
|||
|
|||
repeat(6) { rowIndex -> |
|||
|
|||
Row( |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
|
|||
repeat(3) { columnIndex -> |
|||
|
|||
val imageIndex = rowIndex * 3 + columnIndex |
|||
|
|||
|
|||
Image( |
|||
painter = painterResource( |
|||
id = R.drawable.locationpermissionbg |
|||
), |
|||
|
|||
contentDescription = "Profile Image $imageIndex", |
|||
modifier = Modifier.weight(1f).aspectRatio(1f).padding(1.dp), |
|||
contentScale = ContentScale.Crop |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
// ============================================================ |
|||
// STAT CARD |
|||
// ============================================================ |
|||
|
|||
@Composable |
|||
private fun StatCard( |
|||
number: String, |
|||
label: String, |
|||
modifier: Modifier = Modifier, |
|||
numberColor: Color = textDark |
|||
|
|||
) { |
|||
|
|||
Card( |
|||
modifier = modifier, |
|||
shape = RoundedCornerShape(12.dp), |
|||
|
|||
colors = CardDefaults.cardColors( |
|||
containerColor = Color(0xFFFAFAFA) |
|||
), |
|||
|
|||
border = |
|||
CardDefaults.outlinedCardBorder() |
|||
.copy(width = 0.5.dp) |
|||
) { |
|||
|
|||
Column( |
|||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Text( |
|||
text = number, |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = numberColor |
|||
) |
|||
Spacer( |
|||
modifier = Modifier.height(2.dp) |
|||
) |
|||
|
|||
Text( |
|||
text = label, |
|||
fontSize = 11.sp, |
|||
color = TextMuted |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ============================================================ |
|||
// PREVIEW |
|||
// ============================================================ |
|||
@ -0,0 +1,17 @@ |
|||
package com.example.dattingapp.homes.networkandapi |
|||
|
|||
import retrofit2.Response |
|||
import retrofit2.http.GET |
|||
import retrofit2.http.Query |
|||
|
|||
interface HomeApiService { |
|||
|
|||
@GET("api/v1/meet") |
|||
suspend fun getMeetProfiles( |
|||
@Query("limit") limit: Int = 20 |
|||
): Response<List<MeetUserProfile>> |
|||
|
|||
@GET("api/v1/users/me") |
|||
suspend fun getCurrentUserProfile(): Response<MyProfileResponse> |
|||
|
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
package com.example.dattingapp.homes.networkandapi |
|||
|
|||
import com.google.gson.annotations.SerializedName |
|||
|
|||
private const val MEDIA_BASE_URL = "https://melove.youthome.com.cn" |
|||
|
|||
private fun resolveMediaUrl(value: String?): String? { |
|||
val path = value?.trim()?.takeIf { it.isNotBlank() } ?: return null |
|||
|
|||
return when { |
|||
path.startsWith("https://", ignoreCase = true) -> path |
|||
path.startsWith("http://", ignoreCase = true) -> path // (Note: original code duplicated https:// twice) |
|||
path.startsWith("//") -> "https:$path" |
|||
else -> "$MEDIA_BASE_URL/${path.trimStart('/')}" |
|||
} |
|||
} |
|||
|
|||
data class MeetUserProfile( |
|||
@SerializedName("id") |
|||
val id: Int, |
|||
|
|||
@SerializedName("full_name") |
|||
val fullName: String?, |
|||
|
|||
@SerializedName("gender") |
|||
val gender: String?, |
|||
|
|||
@SerializedName("birth_date") |
|||
val birthDate: String?, |
|||
|
|||
@SerializedName("city") |
|||
val city: String?, |
|||
|
|||
@SerializedName("country") |
|||
val country: String?, |
|||
|
|||
@SerializedName("bio") |
|||
val bio: String?, |
|||
|
|||
@SerializedName("video_url") |
|||
val videoUrl: String?, |
|||
|
|||
@SerializedName("distance_km") |
|||
val distanceKm: Double?, |
|||
|
|||
@SerializedName("compatibility_percentage") |
|||
val compatibilityPercentage: Int?, |
|||
|
|||
@SerializedName("media") |
|||
val media: List<MediaItem>? |
|||
) { |
|||
val fullVideoUrl: String? |
|||
get() = resolveMediaUrl(videoUrl) |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 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 MediaItem( |
|||
@SerializedName("id") |
|||
val id: Int, |
|||
|
|||
@SerializedName("media_type") |
|||
val mediaType: String?, |
|||
|
|||
@SerializedName("url") |
|||
val url: String? |
|||
) { |
|||
val fullMediaUrl: String? |
|||
get() = resolveMediaUrl(url) |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
package com.example.dattingapp.homes.networkandapi |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
import okhttp3.OkHttpClient |
|||
import okhttp3.logging.HttpLoggingInterceptor |
|||
import retrofit2.Retrofit |
|||
import retrofit2.converter.gson.GsonConverterFactory |
|||
import java.util.concurrent.TimeUnit |
|||
|
|||
object HomeNetworkModule { |
|||
|
|||
private const val BASE_URL = "https://melove.youthome.com.cn/" |
|||
|
|||
@Volatile |
|||
private var apiService: HomeApiService? = null |
|||
|
|||
fun getApiService(context: Context): HomeApiService { |
|||
return apiService ?: synchronized(this) { |
|||
apiService ?: createApi(context.applicationContext) |
|||
.also { apiService = it } |
|||
} |
|||
} |
|||
|
|||
private fun createApi(context: Context): HomeApiService { |
|||
val tokenManager = TokenManager.getInstance(context) |
|||
|
|||
val loggingInterceptor = HttpLoggingInterceptor { message -> |
|||
Log.d("MeetHttp", message) |
|||
}.apply { |
|||
// Logs request/status lines without profile bodies. |
|||
level = HttpLoggingInterceptor.Level.BASIC |
|||
redactHeader("Authorization") |
|||
redactHeader("Cookie") |
|||
} |
|||
|
|||
val client = OkHttpClient.Builder() |
|||
.connectTimeout(30, TimeUnit.SECONDS) |
|||
.readTimeout(30, TimeUnit.SECONDS) |
|||
.callTimeout(60, TimeUnit.SECONDS) |
|||
.addInterceptor { chain -> |
|||
val token = tokenManager.getAccessToken() |
|||
?.replace(Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), "")?.trim()?.takeIf { it.isNotBlank() } |
|||
|
|||
Log.d("MeetApi", "Access token available: ${token != null}") |
|||
|
|||
val request = chain.request().newBuilder() |
|||
.header("Accept", "application/json") |
|||
.apply { |
|||
if (token != null) { |
|||
header("Authorization", "Bearer $token") |
|||
} |
|||
} |
|||
.build() |
|||
|
|||
chain.proceed(request) |
|||
} |
|||
.addInterceptor(loggingInterceptor) |
|||
.build() |
|||
|
|||
return Retrofit.Builder() |
|||
.baseUrl(BASE_URL) |
|||
.client(client) |
|||
.addConverterFactory(GsonConverterFactory.create()) |
|||
.build() |
|||
.create(HomeApiService::class.java) |
|||
} |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
package com.example.dattingapp.homes.networkandapi |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
import androidx.lifecycle.ViewModel |
|||
import androidx.lifecycle.viewModelScope |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.StateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import kotlinx.coroutines.launch |
|||
|
|||
sealed class MeetUiState { |
|||
object Idle : MeetUiState() |
|||
object Loading : MeetUiState() |
|||
data class Success(val response: List<MeetUserProfile>) : MeetUiState() |
|||
data class Error(val message: String) : MeetUiState() |
|||
} |
|||
|
|||
class MeetViewModel : ViewModel() { |
|||
|
|||
private val _meetUiState = MutableStateFlow<MeetUiState>(MeetUiState.Idle) |
|||
val meetUiState: StateFlow<MeetUiState> = _meetUiState.asStateFlow() |
|||
|
|||
fun meetUserResponse(context: Context) { |
|||
if (_meetUiState.value is MeetUiState.Loading) return |
|||
|
|||
val appContext = context.applicationContext |
|||
|
|||
viewModelScope.launch { |
|||
_meetUiState.value = MeetUiState.Loading |
|||
|
|||
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { |
|||
Log.e("MeetApi", "No access token in user_tokens preferences") |
|||
_meetUiState.value = MeetUiState.Error("No access token found. Please log in again.") |
|||
return@launch |
|||
} |
|||
|
|||
try { |
|||
val response = HomeNetworkModule |
|||
.getApiService(appContext) |
|||
.getMeetProfiles() |
|||
|
|||
Log.d("MeetApi", "HTTP Response Code: ${response.code()}") |
|||
|
|||
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()}." |
|||
} |
|||
_meetUiState.value = MeetUiState.Error(message) |
|||
return@launch |
|||
} |
|||
|
|||
val profiles = response.body() |
|||
|
|||
if (profiles.isNullOrEmpty()) { |
|||
_meetUiState.value = MeetUiState.Error("No profiles available.") |
|||
} else { |
|||
// ------------------------------------------------------------- |
|||
// LOGGING IMAGE DATA FROM BACKEND RESPONSE |
|||
// ------------------------------------------------------------- |
|||
profiles.forEachIndexed { index, profile -> |
|||
val name = profile.fullName ?: "No Name (ID: ${profile.id})" |
|||
val mediaList = profile.media |
|||
|
|||
if (mediaList.isNullOrEmpty()) { |
|||
Log.w("MeetMediaCheck", "[$index] Profile: $name -> No media items returned from backend (media is empty/null)") |
|||
} else { |
|||
Log.d("MeetMediaCheck", "[$index] Profile: $name -> Found ${mediaList.size} media item(s)") |
|||
mediaList.forEachIndexed { mediaIdx, media -> |
|||
Log.d( |
|||
"MeetMediaCheck", |
|||
" └── Item #$mediaIdx: type='${media.mediaType}', raw_url='${media.url}', resolved_full_url='${media.fullMediaUrl}'" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
// ------------------------------------------------------------- |
|||
|
|||
_meetUiState.value = MeetUiState.Success(profiles) |
|||
} |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e("MeetApi", "Unexpected request failure", e) |
|||
_meetUiState.value = MeetUiState.Error("Request failed: ${e.javaClass.simpleName}") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
sealed interface MyProfileUiState { |
|||
object Idle : MyProfileUiState |
|||
object Loading : MyProfileUiState |
|||
data class Success(val myProfile: MyProfileResponse) : MyProfileUiState // Ensure this is MyProfileData, not MyProfileResponse |
|||
data class Error(val message: String) : MyProfileUiState |
|||
} |
|||
class MyProfileViewModel : ViewModel() { |
|||
|
|||
private val _myProfileUiState = MutableStateFlow<MyProfileUiState>(MyProfileUiState.Idle) |
|||
val myProfileUiState: StateFlow<MyProfileUiState> = _myProfileUiState.asStateFlow() |
|||
|
|||
fun meetUserResponse(context: Context) { |
|||
if (_myProfileUiState.value is MyProfileUiState.Loading) return |
|||
|
|||
val appContext = context.applicationContext |
|||
|
|||
viewModelScope.launch { |
|||
_myProfileUiState.value = MyProfileUiState.Loading |
|||
|
|||
if (TokenManager.getInstance(appContext).getAccessToken().isNullOrBlank()) { |
|||
Log.e("MeetApi", "No access token in user_tokens preferences") |
|||
_myProfileUiState.value = MyProfileUiState.Error("No access token found. Please log in again.") |
|||
return@launch |
|||
} |
|||
|
|||
try { |
|||
val response = HomeNetworkModule |
|||
.getApiService(appContext) |
|||
.getCurrentUserProfile() |
|||
|
|||
if (!response.isSuccessful) { |
|||
response.errorBody()?.close() |
|||
val message = when (response.code()) { |
|||
401 -> "Authentication failed. Please log in again." |
|||
403 -> "The server refused access to these profiles." |
|||
404 -> "Meet endpoint was not found." |
|||
else -> "Request failed: HTTP ${response.code()}." |
|||
} |
|||
_myProfileUiState.value = MyProfileUiState.Error(message) |
|||
return@launch |
|||
} |
|||
|
|||
// FIX: Use response.body() directly as profileData |
|||
val profileData = response.body() |
|||
|
|||
Log.d("my profiler", "my data $profileData") |
|||
|
|||
if (profileData != null) { |
|||
_myProfileUiState.value = MyProfileUiState.Success(profileData) |
|||
} else { |
|||
_myProfileUiState.value = MyProfileUiState.Error("Profile data is empty.") |
|||
} |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e("MeetApi", "Unexpected request failure", e) |
|||
_myProfileUiState.value = MyProfileUiState.Error("Request failed: ${e.javaClass.simpleName}") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
package com.example.dattingapp.homes.networkandapi |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
|
|||
class TokenManager private constructor(context: Context) { |
|||
|
|||
private val preferences = |
|||
context.applicationContext.getSharedPreferences( |
|||
PREF_NAME, |
|||
Context.MODE_PRIVATE |
|||
) |
|||
|
|||
fun saveTokens(accessToken: String, refreshToken: String?) { |
|||
val cleanToken = accessToken.trim() |
|||
.replace( |
|||
Regex("^Bearer\\s+", RegexOption.IGNORE_CASE), |
|||
"" |
|||
) |
|||
.trim() |
|||
|
|||
val saved = preferences.edit() |
|||
.putString(KEY_ACCESS_TOKEN, cleanToken) |
|||
.putString(KEY_REFRESH_TOKEN, refreshToken) |
|||
.commit() |
|||
|
|||
// Do not log the token itself. |
|||
Log.d("TokenManager", "Tokens saved: $saved") |
|||
} |
|||
|
|||
fun getAccessToken(): String? { |
|||
return preferences.getString(KEY_ACCESS_TOKEN, null) |
|||
?.trim() |
|||
?.takeIf { it.isNotBlank() } |
|||
} |
|||
|
|||
fun getRefreshToken(): String? { |
|||
return preferences.getString(KEY_REFRESH_TOKEN, null) |
|||
?.takeIf { it.isNotBlank() } |
|||
} |
|||
|
|||
// Checks local token presence, not server validity or expiration. |
|||
fun isLoggedIn(): Boolean { |
|||
return !getAccessToken().isNullOrBlank() |
|||
} |
|||
|
|||
fun clearTokens() { |
|||
preferences.edit().clear().commit() |
|||
} |
|||
|
|||
companion object { |
|||
private const val PREF_NAME = "user_tokens" |
|||
private const val KEY_ACCESS_TOKEN = "access_token" |
|||
private const val KEY_REFRESH_TOKEN = "refresh_token" |
|||
|
|||
@Volatile |
|||
private var INSTANCE: TokenManager? = null |
|||
|
|||
fun getInstance(context: Context): TokenManager { |
|||
return INSTANCE ?: synchronized(this) { |
|||
INSTANCE ?: TokenManager(context.applicationContext) |
|||
.also { INSTANCE = it } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,51 +0,0 @@ |
|||
package com.example.dattingapp.registration |
|||
|
|||
import android.content.Context |
|||
import android.content.SharedPreferences |
|||
|
|||
class TokenManager private constructor(context: Context) { |
|||
|
|||
private val sharedPreferences: SharedPreferences = context.applicationContext.getSharedPreferences( |
|||
PREF_NAME, |
|||
Context.MODE_PRIVATE |
|||
) |
|||
|
|||
fun saveTokens(accessToken: String, refreshToken: String?) { |
|||
sharedPreferences.edit().apply { |
|||
putString(KEY_ACCESS_TOKEN, accessToken) |
|||
putString(KEY_REFRESH_TOKEN, refreshToken) |
|||
apply() |
|||
} |
|||
} |
|||
|
|||
fun getAccessToken(): String? { |
|||
return sharedPreferences.getString(KEY_ACCESS_TOKEN, null) |
|||
} |
|||
|
|||
fun getRefreshToken(): String? { |
|||
return sharedPreferences.getString(KEY_REFRESH_TOKEN, null) |
|||
} |
|||
|
|||
fun clearTokens() { |
|||
sharedPreferences.edit().apply { |
|||
remove(KEY_ACCESS_TOKEN) |
|||
remove(KEY_REFRESH_TOKEN) |
|||
apply() |
|||
} |
|||
} |
|||
|
|||
companion object { |
|||
private const val PREF_NAME = "user_tokens" |
|||
private const val KEY_ACCESS_TOKEN = "access_token" |
|||
private const val KEY_REFRESH_TOKEN = "refresh_token" |
|||
|
|||
@Volatile |
|||
private var INSTANCE: TokenManager? = null |
|||
|
|||
fun getInstance(context: Context): TokenManager { |
|||
return INSTANCE ?: synchronized(this) { |
|||
INSTANCE ?: TokenManager(context.applicationContext).also { INSTANCE = it } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<paths xmlns:android="http://schemas.android.com/apk/res/android"> |
|||
<!-- Grants access to the app's internal cache directory --> |
|||
<cache-path name="cache_files" path="." /> |
|||
|
|||
<!-- Grants access to internal storage app-specific files directory --> |
|||
<files-path name="internal_files" path="." /> |
|||
|
|||
<!-- Grants access to external app storage directory --> |
|||
<external-files-path name="external_files" path="." /> |
|||
</paths> |
|||
Loading…
Reference in new issue