|
|
|
@ -2,7 +2,13 @@ package com.example.dattingapp.homes |
|
|
|
|
|
|
|
import com.example.dattingapp.homes.networkandapi.Location |
|
|
|
import com.example.dattingapp.homes.networkandapi.CreatePostRequest |
|
|
|
import android.Manifest |
|
|
|
import android.content.Context |
|
|
|
import android.content.pm.PackageManager |
|
|
|
import android.location.LocationManager |
|
|
|
import android.net.Uri |
|
|
|
import android.util.Log |
|
|
|
import android.widget.Toast |
|
|
|
import androidx.activity.compose.rememberLauncherForActivityResult |
|
|
|
import androidx.activity.result.PickVisualMediaRequest |
|
|
|
import androidx.activity.result.contract.ActivityResultContracts |
|
|
|
@ -14,6 +20,7 @@ import androidx.compose.foundation.layout.* |
|
|
|
import androidx.compose.foundation.lazy.LazyRow |
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells |
|
|
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid |
|
|
|
import androidx.compose.foundation.lazy.grid.items |
|
|
|
import androidx.compose.foundation.lazy.itemsIndexed |
|
|
|
import androidx.compose.foundation.rememberScrollState |
|
|
|
import androidx.compose.foundation.shape.CircleShape |
|
|
|
@ -33,6 +40,7 @@ import androidx.compose.ui.graphics.vector.ImageVector |
|
|
|
import androidx.compose.ui.layout.ContentScale |
|
|
|
import androidx.compose.ui.platform.LocalContext |
|
|
|
import androidx.compose.ui.text.SpanStyle |
|
|
|
import androidx.compose.ui.text.TextStyle |
|
|
|
import androidx.compose.ui.text.buildAnnotatedString |
|
|
|
import androidx.compose.ui.text.font.FontWeight |
|
|
|
import androidx.compose.ui.text.withStyle |
|
|
|
@ -40,35 +48,23 @@ import androidx.compose.ui.tooling.preview.Preview |
|
|
|
import androidx.compose.ui.unit.dp |
|
|
|
import androidx.compose.ui.unit.sp |
|
|
|
import androidx.compose.ui.viewinterop.AndroidView |
|
|
|
import androidx.core.content.ContextCompat |
|
|
|
import androidx.core.content.FileProvider |
|
|
|
import androidx.lifecycle.viewmodel.compose.viewModel |
|
|
|
import androidx.media3.common.MediaItem |
|
|
|
import androidx.media3.exoplayer.ExoPlayer |
|
|
|
import androidx.media3.ui.PlayerView |
|
|
|
import coil.compose.rememberAsyncImagePainter |
|
|
|
import com.example.dattingapp.homes.networkandapi.CreatePostUiState |
|
|
|
import com.example.dattingapp.homes.networkandapi.CreatePostViewModel |
|
|
|
import com.example.dattingapp.homes.networkandapi.PostMediaViewModel |
|
|
|
import com.example.dattingapp.homes.networkandapi.VibeFeeling |
|
|
|
import com.example.dattingapp.homes.networkandapi.vibeFeelingsList |
|
|
|
import com.example.dattingapp.ui.theme.LightPinkBg |
|
|
|
import com.example.dattingapp.ui.theme.LightPurpleBg |
|
|
|
import com.example.dattingapp.ui.theme.DarkOverlay |
|
|
|
import com.example.dattingapp.ui.theme.LightBlueBg |
|
|
|
import android.Manifest |
|
|
|
import android.content.pm.PackageManager |
|
|
|
import android.location.LocationManager |
|
|
|
import android.widget.Toast |
|
|
|
import androidx.core.content.ContextCompat |
|
|
|
import androidx.compose.foundation.lazy.grid.items |
|
|
|
import androidx.lifecycle.ViewModel |
|
|
|
import androidx.lifecycle.viewmodel.compose.viewModel |
|
|
|
import com.example.dattingapp.homes.networkandapi.CreatePostViewModel |
|
|
|
import com.example.dattingapp.homes.networkandapi.CreatePostUiState |
|
|
|
import android.util.Log |
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context |
|
|
|
import androidx.compose.ui.text.TextStyle |
|
|
|
import com.example.dattingapp.homes.networkandapi.PostMediaViewModel |
|
|
|
import com.example.dattingapp.ui.theme.DattingAPPTheme |
|
|
|
|
|
|
|
import com.example.dattingapp.ui.theme.LightBlueBg |
|
|
|
import com.example.dattingapp.ui.theme.LightPinkBg |
|
|
|
import com.example.dattingapp.ui.theme.LightPurpleBg |
|
|
|
import java.io.File |
|
|
|
|
|
|
|
// Sealed class representing selectable media types (Photo or Video) |
|
|
|
@ -77,21 +73,17 @@ sealed class MediaContentItem(val uri: Uri) { |
|
|
|
class Video(uri: Uri) : MediaContentItem(uri) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class) |
|
|
|
@Composable |
|
|
|
fun CreateVibePostScreen( |
|
|
|
onCloseClick: () -> Unit = {}, |
|
|
|
onShareClick: () -> Unit = {} |
|
|
|
onShareClick: () -> Unit = {} // called ONLY after the post is created |
|
|
|
) { |
|
|
|
val context = LocalContext.current |
|
|
|
var textState by remember { mutableStateOf("") } |
|
|
|
val maxChar = 500 |
|
|
|
|
|
|
|
// Holds selected media items (photos, videos, or both) |
|
|
|
val selectedMediaList = remember { mutableStateListOf<MediaContentItem>() } |
|
|
|
// Determine the post type dynamically based on attached media |
|
|
|
val post_type = remember(selectedMediaList) { |
|
|
|
when { |
|
|
|
selectedMediaList.any { it is MediaContentItem.Video } -> "video" |
|
|
|
@ -107,10 +99,60 @@ fun CreateVibePostScreen( |
|
|
|
var showFeelingSheet by remember { mutableStateOf(false) } |
|
|
|
var selectedFeeling by remember { mutableStateOf<VibeFeeling?>(null) } |
|
|
|
|
|
|
|
// ---------------- ViewModels (moved to the top of the screen) ---------------- |
|
|
|
val createPostViewModel: CreatePostViewModel = viewModel() |
|
|
|
val postMediaViewModel: PostMediaViewModel = viewModel() |
|
|
|
val uiState by createPostViewModel.cretePpostUiState.collectAsState() |
|
|
|
|
|
|
|
// True from the Share tap until the whole flow is done |
|
|
|
val isPosting = uiState is CreatePostUiState.Loading || uiState is CreatePostUiState.Success |
|
|
|
|
|
|
|
// Location states (declared before the effect below, which clears them) |
|
|
|
var latitude by remember { mutableStateOf<Double?>(null) } |
|
|
|
var longitude by remember { mutableStateOf<Double?>(null) } |
|
|
|
var showLocationTag by remember { mutableStateOf(false) } |
|
|
|
|
|
|
|
// React to the create-post result |
|
|
|
LaunchedEffect(uiState) { |
|
|
|
when (val state = uiState) { |
|
|
|
is CreatePostUiState.Success -> { |
|
|
|
try { |
|
|
|
val postId = state.postData.id |
|
|
|
Log.d("CreatePostDebug", "Post created with ID: $postId") |
|
|
|
|
|
|
|
val errors = mutableListOf<String>() |
|
|
|
selectedMediaList.toList().forEachIndexed { index, item -> |
|
|
|
val err = postMediaViewModel.uploadMedia(context, postId, index, item.uri) |
|
|
|
if (err != null) errors.add("File ${index + 1}: $err") |
|
|
|
} |
|
|
|
if (errors.isNotEmpty()) { |
|
|
|
Toast.makeText(context, errors.first(), Toast.LENGTH_LONG).show() |
|
|
|
} |
|
|
|
|
|
|
|
textState = "" |
|
|
|
selectedMediaList.clear() |
|
|
|
selectedFeeling = null |
|
|
|
latitude = null |
|
|
|
longitude = null |
|
|
|
showLocationTag = false |
|
|
|
|
|
|
|
onShareClick() |
|
|
|
} finally { |
|
|
|
// always runs, even if this effect is cancelled, so the button never stays stuck |
|
|
|
createPostViewModel.reset() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
is CreatePostUiState.Error -> { |
|
|
|
Log.e("CreatePostDebug", "Error: ${state.message}") |
|
|
|
Toast.makeText(context, state.message, Toast.LENGTH_LONG).show() |
|
|
|
createPostViewModel.reset() |
|
|
|
} |
|
|
|
|
|
|
|
// Launcher for Multi-Media Gallery Picker (Supports both Photos and Videos simultaneously) |
|
|
|
else -> {} |
|
|
|
} |
|
|
|
} |
|
|
|
// ---------------- Pickers ---------------- |
|
|
|
val galleryLauncher = rememberLauncherForActivityResult( |
|
|
|
contract = ActivityResultContracts.PickMultipleVisualMedia() |
|
|
|
) { uris: List<Uri> -> |
|
|
|
@ -124,20 +166,14 @@ fun CreateVibePostScreen( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Temporary file helper for Camera captures |
|
|
|
fun createTempFileUri(extension: String, prefix: String): Uri { |
|
|
|
val tempFile = File.createTempFile(prefix, extension, context.cacheDir).apply { |
|
|
|
createNewFile() |
|
|
|
deleteOnExit() |
|
|
|
} |
|
|
|
return FileProvider.getUriForFile( |
|
|
|
context, |
|
|
|
"${context.packageName}.provider", |
|
|
|
tempFile |
|
|
|
) |
|
|
|
return FileProvider.getUriForFile(context, "${context.packageName}.provider", tempFile) |
|
|
|
} |
|
|
|
|
|
|
|
// Camera Launchers for taking photos or recording videos |
|
|
|
val cameraPhotoLauncher = rememberLauncherForActivityResult( |
|
|
|
contract = ActivityResultContracts.TakePicture() |
|
|
|
) { success: Boolean -> |
|
|
|
@ -154,25 +190,17 @@ fun CreateVibePostScreen( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Location States |
|
|
|
var latitude by remember { mutableStateOf<Double?>(null) } |
|
|
|
var longitude by remember { mutableStateOf<Double?>(null) } |
|
|
|
var showLocationTag by remember { mutableStateOf(false) } |
|
|
|
|
|
|
|
// Native Location Manager (Works in China without Google Play Services) |
|
|
|
// ---------------- Location ---------------- |
|
|
|
val locationManager = remember { |
|
|
|
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager |
|
|
|
} |
|
|
|
|
|
|
|
// Helper function to get location using native APIs |
|
|
|
fun fetchDeviceLocation() { |
|
|
|
try { |
|
|
|
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) |
|
|
|
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) |
|
|
|
|
|
|
|
var location: android.location.Location? = null |
|
|
|
|
|
|
|
if (isGpsEnabled) { |
|
|
|
location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) |
|
|
|
} |
|
|
|
@ -192,31 +220,23 @@ fun CreateVibePostScreen( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Permission Launcher for the system dialog |
|
|
|
val locationPermissionLauncher = rememberLauncherForActivityResult( |
|
|
|
contract = ActivityResultContracts.RequestPermission() |
|
|
|
) { isGranted -> |
|
|
|
if (isGranted) { |
|
|
|
fetchDeviceLocation() |
|
|
|
} else { |
|
|
|
Toast.makeText(context, "Location permission denied", Toast.LENGTH_SHORT).show() |
|
|
|
} |
|
|
|
if (isGranted) fetchDeviceLocation() |
|
|
|
else Toast.makeText(context, "Location permission denied", Toast.LENGTH_SHORT).show() |
|
|
|
} |
|
|
|
|
|
|
|
// Click handler for the location chip |
|
|
|
val onLocationChipClick: () -> Unit = { |
|
|
|
val hasPermission = ContextCompat.checkSelfPermission( |
|
|
|
context, Manifest.permission.ACCESS_FINE_LOCATION |
|
|
|
) == PackageManager.PERMISSION_GRANTED |
|
|
|
|
|
|
|
if (hasPermission) { |
|
|
|
fetchDeviceLocation() |
|
|
|
} else { |
|
|
|
// Triggers the system "Allow / Deny" dialog |
|
|
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) |
|
|
|
} |
|
|
|
if (hasPermission) fetchDeviceLocation() |
|
|
|
else locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) |
|
|
|
} |
|
|
|
|
|
|
|
// ---------------- UI ---------------- |
|
|
|
Scaffold( |
|
|
|
bottomBar = { |
|
|
|
Row( |
|
|
|
@ -227,7 +247,6 @@ fun CreateVibePostScreen( |
|
|
|
verticalAlignment = Alignment.CenterVertically, |
|
|
|
horizontalArrangement = Arrangement.SpaceBetween |
|
|
|
) { |
|
|
|
// 1. Scrollable option chips on the left |
|
|
|
LazyRow( |
|
|
|
modifier = Modifier.weight(1f), |
|
|
|
horizontalArrangement = Arrangement.spacedBy(10.dp), |
|
|
|
@ -238,7 +257,7 @@ fun CreateVibePostScreen( |
|
|
|
icon = Icons.Outlined.PhotoLibrary, |
|
|
|
backgroundColor = LightPinkBg, |
|
|
|
contentColor = Color(0xFFE91E63), |
|
|
|
onClick = { showImagePickerDialog = true } |
|
|
|
onClick = { if (!isPosting) showImagePickerDialog = true } |
|
|
|
) |
|
|
|
} |
|
|
|
item { |
|
|
|
@ -246,7 +265,7 @@ fun CreateVibePostScreen( |
|
|
|
icon = Icons.Outlined.MyLocation, |
|
|
|
backgroundColor = LightBlueBg, |
|
|
|
contentColor = Color(0xFF1E88E5), |
|
|
|
onClick = { onLocationChipClick() } |
|
|
|
onClick = { if (!isPosting) onLocationChipClick() } |
|
|
|
) |
|
|
|
} |
|
|
|
item { |
|
|
|
@ -254,7 +273,7 @@ fun CreateVibePostScreen( |
|
|
|
icon = Icons.Outlined.Mood, |
|
|
|
backgroundColor = LightPinkBg, |
|
|
|
contentColor = Color(0xFFE91E63), |
|
|
|
onClick = { showFeelingSheet = true } // Opens bottom sheet |
|
|
|
onClick = { if (!isPosting) showFeelingSheet = true } |
|
|
|
) |
|
|
|
} |
|
|
|
item { |
|
|
|
@ -268,97 +287,52 @@ fun CreateVibePostScreen( |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.width(8.dp)) |
|
|
|
|
|
|
|
// 2. React to UI state changes (Handles post success and triggers media uploads) |
|
|
|
// 2. Share button on the right |
|
|
|
|
|
|
|
val context = LocalContext.current |
|
|
|
val createPostViewModel: CreatePostViewModel = viewModel() |
|
|
|
val postMediaViewModel: PostMediaViewModel = viewModel() |
|
|
|
val uiState by createPostViewModel.cretePpostUiState.collectAsState() |
|
|
|
|
|
|
|
LaunchedEffect(uiState) { |
|
|
|
when (val state = uiState) { |
|
|
|
is CreatePostUiState.Success -> { |
|
|
|
val postId = state.postData.id |
|
|
|
Log.d("CreatePostDebug", "Post created successfully with ID: $postId. Starting media uploads...") |
|
|
|
|
|
|
|
if (selectedMediaList.isEmpty()) { |
|
|
|
Log.d("CreatePostDebug", "No media attached. Flow complete.") |
|
|
|
onShareClick() // Navigate away or finish screen |
|
|
|
} else { |
|
|
|
// Loop through and upload media sequentially |
|
|
|
for ((index, item) in selectedMediaList.withIndex()) { |
|
|
|
val uri = when (item) { |
|
|
|
is MediaContentItem.Photo -> item.uri |
|
|
|
is MediaContentItem.Video -> item.uri |
|
|
|
} |
|
|
|
|
|
|
|
val position = index + 1 // Position starts at 1 |
|
|
|
|
|
|
|
if (uri != null && uri.toString().isNotBlank()) { |
|
|
|
Log.d("CreatePostDebug", "Uploading media at position $position...") |
|
|
|
postMediaViewModel.poostMedia(context,postId,index,uri) |
|
|
|
|
|
|
|
// Call your media upload function here |
|
|
|
// val isUploaded = createPostViewModel.uploadPostMediaItem(context, postId, position, uri) |
|
|
|
} |
|
|
|
} |
|
|
|
Log.d("CreatePostDebug", "All media processed.") |
|
|
|
onShareClick() // Navigate away or finish screen after media upload |
|
|
|
} |
|
|
|
} |
|
|
|
is CreatePostUiState.Error -> { |
|
|
|
Log.e("CreatePostDebug", "Error: ${state.message}") |
|
|
|
} |
|
|
|
else -> {} |
|
|
|
} |
|
|
|
} |
|
|
|
Button( |
|
|
|
enabled = !isPosting, // blocks double taps |
|
|
|
onClick = { |
|
|
|
// 1. Build the Location object if the tag is active and coordinates exist |
|
|
|
if (textState.isBlank() && selectedMediaList.isEmpty()) { |
|
|
|
Toast.makeText(context, "Add text or at least one photo/video", Toast.LENGTH_SHORT).show() |
|
|
|
return@Button |
|
|
|
} |
|
|
|
// ... build request and call createPost() |
|
|
|
val postLocation = if (showLocationTag && latitude != null && longitude != null) { |
|
|
|
Location( |
|
|
|
name = "Current Location", // Or your geocoded address string |
|
|
|
name = "Current Location", |
|
|
|
latitude = latitude, |
|
|
|
longitude = longitude |
|
|
|
) |
|
|
|
} else { |
|
|
|
null |
|
|
|
} |
|
|
|
} else null |
|
|
|
|
|
|
|
// 2. Construct the request payload matching your data class |
|
|
|
val createPostRequest = CreatePostRequest( |
|
|
|
content = textState, |
|
|
|
post_type = post_type, // "text", "image", or "video" (dynamically derived) |
|
|
|
content_format = "markdown", // Adjust based on your backend requirement |
|
|
|
feeling = selectedFeeling?.let { "${it.name} ${it.emoji}" }, // Passing the selected feeling name/label |
|
|
|
content = textState.trim().ifBlank { " " }, // ← the fix: " " instead of "" |
|
|
|
post_type = post_type, |
|
|
|
content_format = "plain", // keep markdown, it works |
|
|
|
feeling = selectedFeeling?.let { "${it.name} ${it.emoji}" }, |
|
|
|
location = postLocation |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Simply trigger the ViewModel function inside onClick! |
|
|
|
// Only start the request. The screen is closed by the |
|
|
|
// LaunchedEffect above once the post is created. |
|
|
|
createPostViewModel.createPost(context, createPostRequest) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Pass the payload to your ViewModel or API repository callback |
|
|
|
// onShareClick(requestPayload, selectedMediaList) |
|
|
|
onShareClick() |
|
|
|
}, |
|
|
|
modifier = Modifier |
|
|
|
.wrapContentWidth() |
|
|
|
.height(40.dp), |
|
|
|
shape = RoundedCornerShape(12.dp), |
|
|
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), |
|
|
|
colors = ButtonDefaults.buttonColors( |
|
|
|
containerColor = Color.Transparent, |
|
|
|
disabledContainerColor = Color.Transparent |
|
|
|
), |
|
|
|
contentPadding = PaddingValues(0.dp) |
|
|
|
) { |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.background( |
|
|
|
brush = Brush.horizontalGradient( |
|
|
|
colors = listOf(Color(0xFFC2185B), Color(0xFF7B1FA2)) |
|
|
|
colors = if (isPosting) |
|
|
|
listOf(Color(0xFFC2185B).copy(alpha = 0.5f), Color(0xFF7B1FA2).copy(alpha = 0.5f)) |
|
|
|
else |
|
|
|
listOf(Color(0xFFC2185B), Color(0xFF7B1FA2)) |
|
|
|
), |
|
|
|
shape = RoundedCornerShape(12.dp) |
|
|
|
) |
|
|
|
@ -367,7 +341,7 @@ fun CreateVibePostScreen( |
|
|
|
) { |
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) { |
|
|
|
Text( |
|
|
|
text = "Share", |
|
|
|
text = if (isPosting) "Posting..." else "Share", |
|
|
|
color = Color.White, |
|
|
|
fontWeight = FontWeight.Bold, |
|
|
|
fontSize = 14.sp |
|
|
|
@ -387,71 +361,67 @@ fun CreateVibePostScreen( |
|
|
|
containerColor = Color.White |
|
|
|
) { innerPadding -> |
|
|
|
|
|
|
|
Column( |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxSize() |
|
|
|
.padding(innerPadding) |
|
|
|
.padding(horizontal = 16.dp) |
|
|
|
.verticalScroll(rememberScrollState()) |
|
|
|
|
|
|
|
) { |
|
|
|
// User Profile Header |
|
|
|
Row( |
|
|
|
verticalAlignment = Alignment.CenterVertically, |
|
|
|
modifier = Modifier.fillMaxWidth() |
|
|
|
) { |
|
|
|
Box(modifier = Modifier.size(48.dp)) { |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.size(48.dp) |
|
|
|
.clip(CircleShape) |
|
|
|
.background(Color.Gray) |
|
|
|
) |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.size(12.dp) |
|
|
|
.clip(CircleShape) |
|
|
|
.background(Color(0xFFE91E63)) |
|
|
|
.align(Alignment.BottomEnd) |
|
|
|
.border(2.dp, Color.White, CircleShape) |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.width(12.dp)) |
|
|
|
Column( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxSize() |
|
|
|
.padding(horizontal = 16.dp) |
|
|
|
.verticalScroll(rememberScrollState()) |
|
|
|
) { |
|
|
|
// User Profile Header |
|
|
|
Row( |
|
|
|
verticalAlignment = Alignment.CenterVertically, |
|
|
|
modifier = Modifier.fillMaxWidth() |
|
|
|
) { |
|
|
|
Box(modifier = Modifier.size(48.dp)) { |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.size(48.dp) |
|
|
|
.clip(CircleShape) |
|
|
|
.background(Color.Gray) |
|
|
|
) |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.size(12.dp) |
|
|
|
.clip(CircleShape) |
|
|
|
.background(Color(0xFFE91E63)) |
|
|
|
.align(Alignment.BottomEnd) |
|
|
|
.border(2.dp, Color.White, CircleShape) |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
Column(modifier = Modifier.weight(1f)) { |
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) { |
|
|
|
Text( |
|
|
|
text = buildAnnotatedString { |
|
|
|
// "Alex Rivera " - Bold |
|
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|
|
|
append("Alex Rivera ") |
|
|
|
} |
|
|
|
Spacer(modifier = Modifier.width(12.dp)) |
|
|
|
|
|
|
|
// "- feeling " - Normal weight (also fixed "felling" to "feeling") |
|
|
|
selectedFeeling?.let { feeling -> |
|
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) { |
|
|
|
append(" — feeling ") |
|
|
|
} |
|
|
|
Column(modifier = Modifier.weight(1f)) { |
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) { |
|
|
|
Text( |
|
|
|
text = buildAnnotatedString { |
|
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|
|
|
// Change feeling.name to feeling.label (or feeling.title depending on your data class) |
|
|
|
append("${feeling.name} ${feeling.emoji}") |
|
|
|
append("Alex Rivera ") |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
fontSize = 16.sp |
|
|
|
) |
|
|
|
Spacer(modifier = Modifier.width(4.dp)) |
|
|
|
|
|
|
|
//this is for vibe |
|
|
|
|
|
|
|
} |
|
|
|
selectedFeeling?.let { feeling -> |
|
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Normal)) { |
|
|
|
append(" — feeling ") |
|
|
|
} |
|
|
|
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { |
|
|
|
append("${feeling.name} ${feeling.emoji}") |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
fontSize = 16.sp |
|
|
|
) |
|
|
|
Spacer(modifier = Modifier.width(4.dp)) |
|
|
|
} |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(4.dp)) |
|
|
|
Spacer(modifier = Modifier.height(4.dp)) |
|
|
|
|
|
|
|
Row( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxWidth(), |
|
|
|
Row( |
|
|
|
modifier = Modifier.fillMaxWidth(), |
|
|
|
horizontalArrangement = Arrangement.SpaceBetween |
|
|
|
) { |
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) { |
|
|
|
@ -475,134 +445,134 @@ fun CreateVibePostScreen( |
|
|
|
modifier = Modifier.size(16.dp) |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(16.dp)) |
|
|
|
|
|
|
|
// Text Area |
|
|
|
TextField( |
|
|
|
value = textState, |
|
|
|
onValueChange = { if (it.length <= maxChar) textState = it }, |
|
|
|
textStyle = TextStyle( |
|
|
|
fontSize = 20.sp, |
|
|
|
lineHeight = 28.sp, |
|
|
|
fontWeight = FontWeight.Medium, |
|
|
|
color = Color(0xFF1E1E1E), |
|
|
|
letterSpacing = 0.2.sp |
|
|
|
), |
|
|
|
placeholder = { |
|
|
|
Text( |
|
|
|
"What's your vibe today? Share a moment, date idea, or story...", |
|
|
|
color = Color(0xFFA0A0A8), |
|
|
|
Spacer(modifier = Modifier.height(16.dp)) |
|
|
|
|
|
|
|
// Text Area |
|
|
|
TextField( |
|
|
|
value = textState, |
|
|
|
onValueChange = { if (it.length <= maxChar) textState = it }, |
|
|
|
enabled = !isPosting, |
|
|
|
textStyle = TextStyle( |
|
|
|
fontSize = 20.sp, |
|
|
|
lineHeight = 28.sp |
|
|
|
lineHeight = 28.sp, |
|
|
|
fontWeight = FontWeight.Medium, |
|
|
|
color = Color(0xFF1E1E1E), |
|
|
|
letterSpacing = 0.2.sp |
|
|
|
), |
|
|
|
placeholder = { |
|
|
|
Text( |
|
|
|
"What's your vibe today? Share a moment, date idea, or story...", |
|
|
|
color = Color(0xFFA0A0A8), |
|
|
|
fontSize = 20.sp, |
|
|
|
lineHeight = 28.sp |
|
|
|
) |
|
|
|
}, |
|
|
|
modifier = Modifier.fillMaxWidth(), |
|
|
|
colors = TextFieldDefaults.colors( |
|
|
|
focusedContainerColor = Color.Transparent, |
|
|
|
unfocusedContainerColor = Color.Transparent, |
|
|
|
focusedIndicatorColor = Color.Transparent, |
|
|
|
unfocusedIndicatorColor = Color.Transparent, |
|
|
|
disabledContainerColor = Color.Transparent, |
|
|
|
disabledIndicatorColor = Color.Transparent, |
|
|
|
cursorColor = Color(0xFFE91E63) |
|
|
|
) |
|
|
|
}, |
|
|
|
modifier = Modifier.fillMaxWidth(), |
|
|
|
colors = TextFieldDefaults.colors( |
|
|
|
focusedContainerColor = Color.Transparent, |
|
|
|
unfocusedContainerColor = Color.Transparent, |
|
|
|
focusedIndicatorColor = Color.Transparent, |
|
|
|
unfocusedIndicatorColor = Color.Transparent, |
|
|
|
cursorColor = Color(0xFFE91E63) |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(8.dp)) |
|
|
|
Spacer(modifier = Modifier.height(24.dp)) |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(16.dp)) |
|
|
|
// Horizontal Scrollable Media Row |
|
|
|
if (selectedMediaList.isNotEmpty()) { |
|
|
|
LazyRow( |
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp), |
|
|
|
modifier = Modifier.fillMaxWidth() |
|
|
|
) { |
|
|
|
itemsIndexed(selectedMediaList) { index, item -> |
|
|
|
Card( |
|
|
|
shape = RoundedCornerShape(20.dp), |
|
|
|
modifier = Modifier |
|
|
|
.width(280.dp) |
|
|
|
.height(220.dp) |
|
|
|
) { |
|
|
|
Box(modifier = Modifier.fillMaxSize()) { |
|
|
|
when (item) { |
|
|
|
is MediaContentItem.Photo -> { |
|
|
|
Image( |
|
|
|
painter = rememberAsyncImagePainter(item.uri), |
|
|
|
contentDescription = "Photo Preview", |
|
|
|
contentScale = ContentScale.Crop, |
|
|
|
modifier = Modifier.fillMaxSize() |
|
|
|
) |
|
|
|
} |
|
|
|
is MediaContentItem.Video -> { |
|
|
|
VideoPlayerItem(videoUri = item.uri) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Horizontal Scrollable Media Row (Contains Photos and Videos) |
|
|
|
if (selectedMediaList.isNotEmpty()) { |
|
|
|
LazyRow( |
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp), |
|
|
|
modifier = Modifier.fillMaxWidth() |
|
|
|
) { |
|
|
|
itemsIndexed(selectedMediaList) { index, item -> |
|
|
|
Card( |
|
|
|
shape = RoundedCornerShape(20.dp), |
|
|
|
modifier = Modifier |
|
|
|
.width(280.dp) |
|
|
|
.height(220.dp) |
|
|
|
) { |
|
|
|
Box(modifier = Modifier.fillMaxSize()) { |
|
|
|
when (item) { |
|
|
|
is MediaContentItem.Photo -> { |
|
|
|
Image( |
|
|
|
painter = rememberAsyncImagePainter(item.uri), |
|
|
|
contentDescription = "Photo Preview", |
|
|
|
contentScale = ContentScale.Crop, |
|
|
|
modifier = Modifier.fillMaxSize() |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.align(Alignment.TopEnd) |
|
|
|
.padding(10.dp) |
|
|
|
) { |
|
|
|
OverlayIconButton( |
|
|
|
icon = Icons.Default.Close, |
|
|
|
onClick = { if (!isPosting) selectedMediaList.removeAt(index) } |
|
|
|
) |
|
|
|
} |
|
|
|
is MediaContentItem.Video -> { |
|
|
|
VideoPlayerItem(videoUri = item.uri) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Delete Card Button |
|
|
|
Box( |
|
|
|
modifier = Modifier |
|
|
|
.align(Alignment.TopEnd) |
|
|
|
.padding(10.dp) |
|
|
|
) { |
|
|
|
OverlayIconButton( |
|
|
|
icon = Icons.Default.Close, |
|
|
|
onClick = { selectedMediaList.removeAt(index) } |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// Media Type Badge Overlay (Top Left) |
|
|
|
Surface( |
|
|
|
color = DarkOverlay, |
|
|
|
shape = RoundedCornerShape(8.dp), |
|
|
|
modifier = Modifier |
|
|
|
.align(Alignment.TopStart) |
|
|
|
.padding(10.dp) |
|
|
|
) { |
|
|
|
Row( |
|
|
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), |
|
|
|
verticalAlignment = Alignment.CenterVertically |
|
|
|
Surface( |
|
|
|
color = DarkOverlay, |
|
|
|
shape = RoundedCornerShape(8.dp), |
|
|
|
modifier = Modifier |
|
|
|
.align(Alignment.TopStart) |
|
|
|
.padding(10.dp) |
|
|
|
) { |
|
|
|
Icon( |
|
|
|
imageVector = if (item is MediaContentItem.Video) Icons.Default.Videocam else Icons.Default.Image, |
|
|
|
contentDescription = null, |
|
|
|
tint = Color.White, |
|
|
|
modifier = Modifier.size(14.dp) |
|
|
|
) |
|
|
|
Spacer(modifier = Modifier.width(4.dp)) |
|
|
|
Text( |
|
|
|
text = if (item is MediaContentItem.Video) "Video" else "Photo ${index + 1}", |
|
|
|
color = Color.White, |
|
|
|
fontSize = 11.sp |
|
|
|
) |
|
|
|
Row( |
|
|
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), |
|
|
|
verticalAlignment = Alignment.CenterVertically |
|
|
|
) { |
|
|
|
Icon( |
|
|
|
imageVector = if (item is MediaContentItem.Video) Icons.Default.Videocam else Icons.Default.Image, |
|
|
|
contentDescription = null, |
|
|
|
tint = Color.White, |
|
|
|
modifier = Modifier.size(14.dp) |
|
|
|
) |
|
|
|
Spacer(modifier = Modifier.width(4.dp)) |
|
|
|
Text( |
|
|
|
text = if (item is MediaContentItem.Video) "Video" else "Photo ${index + 1}", |
|
|
|
color = Color.White, |
|
|
|
fontSize = 11.sp |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(16.dp)) |
|
|
|
} |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(16.dp)) |
|
|
|
Spacer(modifier = Modifier.height(24.dp)) |
|
|
|
} |
|
|
|
|
|
|
|
// Location Tag Bar |
|
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(20.dp)) |
|
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(10.dp)) |
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(24.dp)) |
|
|
|
// LOADING BAR: runs left to right along the top while posting |
|
|
|
if (isPosting) { |
|
|
|
LinearProgressIndicator( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxWidth() |
|
|
|
.height(4.dp) |
|
|
|
.align(Alignment.TopCenter), |
|
|
|
color = Color(0xFFC2185B), |
|
|
|
trackColor = Color(0xFFF3D5E3) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Camera / Gallery Choice Bottom Sheet Dialog |
|
|
|
// Camera / Gallery Choice Bottom Sheet |
|
|
|
if (showImagePickerDialog) { |
|
|
|
ModalBottomSheet( |
|
|
|
onDismissRequest = { showImagePickerDialog = false }, |
|
|
|
@ -620,7 +590,6 @@ fun CreateVibePostScreen( |
|
|
|
modifier = Modifier.padding(bottom = 16.dp) |
|
|
|
) |
|
|
|
|
|
|
|
// Pick from Gallery (Photo + Video combined option) |
|
|
|
Row( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxWidth() |
|
|
|
@ -643,7 +612,6 @@ fun CreateVibePostScreen( |
|
|
|
Text(text = "Choose Photos & Videos from Gallery", fontSize = 16.sp) |
|
|
|
} |
|
|
|
|
|
|
|
// Take Photo |
|
|
|
Row( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxWidth() |
|
|
|
@ -666,7 +634,6 @@ fun CreateVibePostScreen( |
|
|
|
Text(text = "Take Photo", fontSize = 16.sp) |
|
|
|
} |
|
|
|
|
|
|
|
// Record Video |
|
|
|
Row( |
|
|
|
modifier = Modifier |
|
|
|
.fillMaxWidth() |
|
|
|
@ -696,9 +663,7 @@ fun CreateVibePostScreen( |
|
|
|
if (showFeelingSheet) { |
|
|
|
FeelingPickerBottomSheet( |
|
|
|
onDismiss = { showFeelingSheet = false }, |
|
|
|
onFeelingSelected = { feeling -> |
|
|
|
selectedFeeling = feeling |
|
|
|
} |
|
|
|
onFeelingSelected = { feeling -> selectedFeeling = feeling } |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -716,9 +681,7 @@ fun VideoPlayerItem(videoUri: Uri) { |
|
|
|
} |
|
|
|
|
|
|
|
DisposableEffect(Unit) { |
|
|
|
onDispose { |
|
|
|
exoPlayer.release() |
|
|
|
} |
|
|
|
onDispose { exoPlayer.release() } |
|
|
|
} |
|
|
|
|
|
|
|
AndroidView( |
|
|
|
@ -775,13 +738,10 @@ fun VibeOptionChip( |
|
|
|
modifier = Modifier.size(18.dp) |
|
|
|
) |
|
|
|
Spacer(modifier = Modifier.width(6.dp)) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class) |
|
|
|
@Composable |
|
|
|
fun FeelingPickerBottomSheet( |
|
|
|
@ -824,10 +784,7 @@ fun FeelingPickerBottomSheet( |
|
|
|
.padding(horizontal = 12.dp, vertical = 10.dp), |
|
|
|
verticalAlignment = Alignment.CenterVertically |
|
|
|
) { |
|
|
|
Text( |
|
|
|
text = feeling.emoji, |
|
|
|
fontSize = 20.sp |
|
|
|
) |
|
|
|
Text(text = feeling.emoji, fontSize = 20.sp) |
|
|
|
Spacer(modifier = Modifier.width(8.dp)) |
|
|
|
Text( |
|
|
|
text = feeling.name, |
|
|
|
@ -842,6 +799,7 @@ fun FeelingPickerBottomSheet( |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@Preview(showBackground = true, showSystemUi = true, backgroundColor = 0xFFFFFFFF) |
|
|
|
@Composable |
|
|
|
fun CreateVibePostScreenPreview() { |
|
|
|
|