Improvements to image carousel.
-Added video support in the carousel. -Added option to expand media to the whole screen for inspection. Added prototype wishlist feature.
This commit is contained in:
parent
3cd8a005c9
commit
eb7e19228b
|
|
@ -76,7 +76,8 @@ fun BuyAnimalCard(
|
|||
.height(257.dp)
|
||||
) {
|
||||
ImageCarousel(
|
||||
imageUrls = product.imageUrl ?: emptyList(),
|
||||
media = product.media ?: emptyList(),
|
||||
enableFullscreenPreview = false,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,13 +26,25 @@ import com.example.livingai_lg.R
|
|||
|
||||
@Composable
|
||||
fun FilterButton(
|
||||
onClick: () -> Unit
|
||||
hasActiveFilters: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val backgroundColor =
|
||||
if (hasActiveFilters) Color(0xFF0A0A0A) else Color.White
|
||||
|
||||
val contentColor =
|
||||
if (hasActiveFilters) Color.White else Color(0xFF0A0A0A)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.border(1.078.dp, Color(0xFF000000).copy(alpha = 0.1f), RoundedCornerShape(8.dp))
|
||||
.background(Color.White, RoundedCornerShape(8.dp))
|
||||
.border(
|
||||
1.078.dp,
|
||||
if (hasActiveFilters) Color.Transparent
|
||||
else Color(0xFF000000).copy(alpha = 0.1f),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(backgroundColor, RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
|
|
@ -42,17 +54,31 @@ fun FilterButton(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_filter),
|
||||
contentDescription = "Filter",
|
||||
tint = Color(0xFF0A0A0A),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
// Icon + dot indicator
|
||||
Row {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_filter),
|
||||
contentDescription = "Filter",
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
if (hasActiveFilters) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp)
|
||||
.size(6.dp)
|
||||
.background(Color.Red, RoundedCornerShape(50))
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Filter",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF0A0A0A)
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.example.livingai_lg.ui.components
|
|||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
|
|
@ -17,51 +18,134 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.MediaItem
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ImageCarousel(
|
||||
imageUrls: List<String>,
|
||||
modifier: Modifier = Modifier
|
||||
media: List<MediaItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
enableFullscreenPreview: Boolean = false,
|
||||
onMediaClick: (startIndex: Int) -> Unit = {},
|
||||
) {
|
||||
val pagerState = rememberPagerState { media.size }
|
||||
var startIndex by remember { mutableStateOf(0) }
|
||||
|
||||
when {
|
||||
imageUrls.isEmpty() -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(Color.LightGray),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("No images", color = Color.White)
|
||||
media.isEmpty() -> {
|
||||
NoImagePlaceholder(modifier = modifier)
|
||||
}
|
||||
|
||||
media.size == 1 -> {
|
||||
when (val item = media.first()) {
|
||||
is MediaItem.Image -> {
|
||||
AsyncImage(
|
||||
model = item.url,
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(0)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
is MediaItem.Video -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(0)
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = item.thumbnailUrl ?: item.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play video",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.align(Alignment.Center)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
CircleShape
|
||||
)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageUrls.size == 1 -> {
|
||||
AsyncImage(
|
||||
model = imageUrls.first(),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val pagerState = rememberPagerState { imageUrls.size }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) { page ->
|
||||
AsyncImage(
|
||||
model = imageUrls[page],
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
when (val item = media[page]) {
|
||||
is MediaItem.Image -> {
|
||||
AsyncImage(
|
||||
model = item.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(page)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
is MediaItem.Video -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(enabled = enableFullscreenPreview) {
|
||||
onMediaClick(page)
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = item.thumbnailUrl ?: item.url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
// Play icon overlay
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Play video",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.align(Alignment.Center)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
CircleShape
|
||||
)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page Indicator (inside image)
|
||||
|
|
@ -71,7 +155,7 @@ fun ImageCarousel(
|
|||
.padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
repeat(imageUrls.size) { index ->
|
||||
repeat(pagerState.pageCount) { index ->
|
||||
val isSelected = pagerState.currentPage == index
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
package com.example.livingai_lg.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.PlayerView
|
||||
import coil.compose.AsyncImage
|
||||
import com.example.livingai_lg.ui.models.MediaItem
|
||||
import androidx.media3.common.MediaItem as ExoMediaItem
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MediaFullscreenOverlay(
|
||||
media: List<MediaItem>,
|
||||
startIndex: Int,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = startIndex,
|
||||
pageCount = { media.size }
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(0.85f))
|
||||
.zIndex(100f)
|
||||
) {
|
||||
|
||||
var isZoomed by remember { mutableStateOf(false) }
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
userScrollEnabled = !isZoomed
|
||||
) { page ->
|
||||
when (val item = media[page]) {
|
||||
is MediaItem.Image -> {
|
||||
ZoomableImage(
|
||||
url = item.url,
|
||||
onZoomChanged = { isZoomed = it }
|
||||
)
|
||||
}
|
||||
is MediaItem.Video -> {
|
||||
VideoPlayer(item.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page Indicator (inside image)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 36.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
repeat(pagerState.pageCount) { index ->
|
||||
val isSelected = pagerState.currentPage == index
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(6.dp)
|
||||
.width(if (isSelected) 18.dp else 6.dp)
|
||||
.background(
|
||||
Color.White,
|
||||
RoundedCornerShape(50)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close button
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(16.dp)
|
||||
.size(32.dp)
|
||||
.clickable { onClose() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZoomableImage(
|
||||
url: String,
|
||||
onZoomChanged: (Boolean) -> Unit
|
||||
) {
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var offsetX by remember { mutableStateOf(0f) }
|
||||
var offsetY by remember { mutableStateOf(0f) }
|
||||
|
||||
val transformState = rememberTransformableState { zoomChange, panChange, _ ->
|
||||
val newScale = (scale * zoomChange).coerceIn(1f, 4f)
|
||||
scale = newScale
|
||||
|
||||
if (newScale > 1f) {
|
||||
offsetX += panChange.x
|
||||
offsetY += panChange.y
|
||||
} else {
|
||||
offsetX = 0f
|
||||
offsetY = 0f
|
||||
}
|
||||
|
||||
onZoomChanged(newScale > 1f)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
translationX = offsetX
|
||||
translationY = offsetY
|
||||
}
|
||||
// 👇 double tap handler
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onDoubleTap = {
|
||||
if (scale > 1f) {
|
||||
scale = 1f
|
||||
offsetX = 0f
|
||||
offsetY = 0f
|
||||
onZoomChanged(false)
|
||||
} else {
|
||||
scale = 2.5f
|
||||
onZoomChanged(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// 👇 only consume gestures when zoomed
|
||||
.then(
|
||||
if (scale > 1f) Modifier.transformable(transformState)
|
||||
else Modifier
|
||||
),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun VideoPlayer(url: String) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context).build().apply {
|
||||
setMediaItem(ExoMediaItem.fromUri(url))
|
||||
prepare()
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
exoPlayer.release()
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = {
|
||||
PlayerView(context).apply {
|
||||
player = exoPlayer
|
||||
useController = true
|
||||
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING)
|
||||
|
||||
// 👇 Enable fullscreen button if available
|
||||
setFullscreenButtonClickListener { isFullscreen ->
|
||||
// PlayerView handles system UI automatically
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ fun SortOverlay(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(0.85f)
|
||||
.fillMaxWidth(0.85f)
|
||||
.fillMaxWidth(0.75f)
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -24,6 +26,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.Alignment
|
||||
|
||||
|
||||
@Composable
|
||||
fun WishlistNameOverlay(
|
||||
|
|
@ -32,18 +36,28 @@ fun WishlistNameOverlay(
|
|||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically { -it },
|
||||
exit = slideOutVertically { -it }
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White)
|
||||
.padding(16.dp)
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically { -it },
|
||||
exit = slideOutVertically { -it }
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = 20.dp,
|
||||
bottomEnd = 20.dp
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Save Filters",
|
||||
fontSize = 18.sp,
|
||||
|
|
@ -54,12 +68,13 @@ fun WishlistNameOverlay(
|
|||
value = name,
|
||||
onValueChange = { name = it },
|
||||
placeholder = { Text("Wishlist name") },
|
||||
singleLine = true
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ data class Animal(
|
|||
val breedInfo: String? = null,
|
||||
val price: Long? = null,
|
||||
val isFairPrice: Boolean? = null,
|
||||
val imageUrl: List<String>? = null,
|
||||
val media: List<MediaItem>? = null,
|
||||
val location: String? = null,
|
||||
val displayLocation: String? = null,
|
||||
val distance: Long? = null,
|
||||
|
|
@ -32,7 +32,9 @@ val sampleAnimals = listOf(
|
|||
breedInfo = "The best in India",
|
||||
location = "Punjab",
|
||||
distance = 12000,
|
||||
imageUrl = listOf("https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2F4.bp.blogspot.com%2F_tecSnxaePMo%2FTLLVknW8dOI%2FAAAAAAAAACo%2F_kd1ZNBXU1o%2Fs1600%2FGIR%2CGujrat.jpg&f=1&nofb=1&ipt=da6ba1d040c396b64d3f08cc99998f66200dcd6c001e4a56def143ab3d1a87ea","https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcpimg.tistatic.com%2F4478702%2Fb%2F4%2Fgir-cow.jpg&f=1&nofb=1&ipt=19bf391461480585c786d01433d863a383c60048ac2ce063ce91f173e215205d"),
|
||||
media = listOf(MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2F4.bp.blogspot.com%2F_tecSnxaePMo%2FTLLVknW8dOI%2FAAAAAAAAACo%2F_kd1ZNBXU1o%2Fs1600%2FGIR%2CGujrat.jpg&f=1&nofb=1&ipt=da6ba1d040c396b64d3f08cc99998f66200dcd6c001e4a56def143ab3d1a87ea"),MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcpimg.tistatic.com%2F4478702%2Fb%2F4%2Fgir-cow.jpg&f=1&nofb=1&ipt=19bf391461480585c786d01433d863a383c60048ac2ce063ce91f173e215205d"),
|
||||
//MediaItem.Video("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
|
||||
),
|
||||
views = 9001,
|
||||
aiScore = 0.80f,
|
||||
price = 120000,
|
||||
|
|
@ -54,7 +56,7 @@ val sampleAnimals = listOf(
|
|||
breedInfo = "The 2nd best in India",
|
||||
location = "Punjab",
|
||||
isFairPrice = true,
|
||||
imageUrl = listOf("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdnbbsr.s3waas.gov.in%2Fs3a5a61717dddc3501cfdf7a4e22d7dbaa%2Fuploads%2F2020%2F09%2F2020091812-1024x680.jpg&f=1&nofb=1&ipt=bb426406b3747e54151e4812472e203f33922fa3b4e11c4feef9aa59a5733146"),
|
||||
media = listOf(MediaItem.Image("https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdnbbsr.s3waas.gov.in%2Fs3a5a61717dddc3501cfdf7a4e22d7dbaa%2Fuploads%2F2020%2F09%2F2020091812-1024x680.jpg&f=1&nofb=1&ipt=bb426406b3747e54151e4812472e203f33922fa3b4e11c4feef9aa59a5733146")),
|
||||
distance = 0L,
|
||||
views = 100,
|
||||
sellerId = "1",
|
||||
|
|
@ -73,7 +75,7 @@ val sampleAnimals = listOf(
|
|||
breedInfo = "Not Indian",
|
||||
location = "Punjab",
|
||||
distance = 12000,
|
||||
imageUrl = listOf("https://api.builder.io/api/v1/image/assets/TEMP/885e24e34ede6a39f708df13dabc4c1683c3e976?width=786"),
|
||||
media = listOf(MediaItem.Image("https://api.builder.io/api/v1/image/assets/TEMP/885e24e34ede6a39f708df13dabc4c1683c3e976?width=786")),
|
||||
views = 94,
|
||||
aiScore = 0.80f,
|
||||
price = 80000,
|
||||
|
|
|
|||
|
|
@ -40,3 +40,4 @@ fun FiltersState.isDefault(): Boolean {
|
|||
pregnancyStatuses.isEmpty() &&
|
||||
calving.filterSet.not()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,4 +16,10 @@ class MediaUpload(
|
|||
|
||||
enum class MediaType {
|
||||
PHOTO, VIDEO
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MediaItem {
|
||||
data class Image(val url: String) : MediaItem()
|
||||
data class Video(val url: String, val thumbnailUrl: String? = null) : MediaItem()
|
||||
companion object
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.example.livingai_lg.ui.screens.SaleArchiveScreen
|
|||
import com.example.livingai_lg.ui.screens.SavedListingsScreen
|
||||
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||
import com.example.livingai_lg.ui.screens.SortScreen
|
||||
import com.example.livingai_lg.ui.screens.WishlistScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||
|
|
@ -278,6 +279,18 @@ fun AppNavigation(
|
|||
AppScreen.sellerProfile(sellerId)
|
||||
)
|
||||
},
|
||||
onWishlistClick = {
|
||||
navController.navigate(AppScreen.WISHLIST)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppScreen.WISHLIST) {
|
||||
WishlistScreen(
|
||||
onApply = {
|
||||
navController.navigate(AppScreen.BUY_ANIMALS)
|
||||
},
|
||||
onBack = {navController.popBackStack()}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ object AppScreen {
|
|||
const val BUY_ANIMALS_FILTERS = "buy_animals_filters"
|
||||
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||
|
||||
const val WISHLIST = "wishlist"
|
||||
|
||||
const val SELLER_PROFILE = "seller_profile"
|
||||
fun sellerProfile(sellerId: String) =
|
||||
"$SELLER_PROFILE/$sellerId"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
|
||||
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
|
|
@ -53,6 +54,7 @@ import com.example.livingai_lg.ui.models.sampleAnimals
|
|||
import com.example.livingai_lg.ui.utils.formatAge
|
||||
import com.example.livingai_lg.R
|
||||
import com.example.livingai_lg.ui.components.ActionPopup
|
||||
import com.example.livingai_lg.ui.components.MediaFullscreenOverlay
|
||||
import com.example.livingai_lg.ui.components.RatingStars
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
|
@ -67,12 +69,14 @@ fun AnimalProfileScreen(
|
|||
onNavClick: (route: String) -> Unit = {}
|
||||
) {
|
||||
var showSavedPopup by remember { mutableStateOf(false) }
|
||||
var showMediaOverlay by remember { mutableStateOf(false) }
|
||||
var mediaOverlayStartIndex by remember { mutableStateOf(0) }
|
||||
val animal = sampleAnimals.find { animal -> animal.id == animalId } ?: Animal(id = "null")
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -90,8 +94,13 @@ fun AnimalProfileScreen(
|
|||
// Main image
|
||||
val product = null
|
||||
ImageCarousel(
|
||||
imageUrls = animal.imageUrl ?: emptyList(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
media = animal.media ?: emptyList(),
|
||||
enableFullscreenPreview = true,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onMediaClick = { startIndex ->
|
||||
showMediaOverlay = true
|
||||
mediaOverlayStartIndex = startIndex
|
||||
}
|
||||
)
|
||||
|
||||
// Gradient overlay at bottom
|
||||
|
|
@ -110,11 +119,38 @@ fun AnimalProfileScreen(
|
|||
)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBackIos,
|
||||
contentDescription = "Back",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(5.dp)
|
||||
.size(36.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = CircleShape,
|
||||
ambientColor = Color.Black.copy(alpha = 0.4f),
|
||||
spotColor = Color.Black.copy(alpha = 0.4f)
|
||||
)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.35f),
|
||||
shape = CircleShape
|
||||
)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onBackClick()
|
||||
}
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
// Views indicator (top left)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(start = 5.dp, top = 5.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 5.dp, top = 5.dp)
|
||||
.shadow(
|
||||
elevation = 6.dp,
|
||||
shape = RoundedCornerShape(50),
|
||||
|
|
@ -226,7 +262,7 @@ fun AnimalProfileScreen(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(horizontal = 36.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
|
|
@ -348,7 +384,8 @@ fun AnimalProfileScreen(
|
|||
FloatingActionBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 10.dp)
|
||||
.padding(horizontal = 16.dp,
|
||||
16.dp)
|
||||
.offset(y = (-10).dp)
|
||||
.zIndex(10f), // 👈 ensure it floats above everything
|
||||
onChatClick = { /* TODO */ },
|
||||
|
|
@ -375,6 +412,14 @@ fun AnimalProfileScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (showMediaOverlay) {
|
||||
MediaFullscreenOverlay(
|
||||
media = animal.media?: emptyList(),
|
||||
startIndex = mediaOverlayStartIndex,
|
||||
onClose = { showMediaOverlay = false }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Modifier.Companion.align(bottomEnd: Alignment) {}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -43,21 +44,23 @@ import com.example.livingai_lg.ui.components.NotificationsOverlay
|
|||
import com.example.livingai_lg.ui.components.SortOverlay
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.models.TextFilter
|
||||
import com.example.livingai_lg.ui.models.isDefault
|
||||
import com.example.livingai_lg.ui.models.sampleNotifications
|
||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||
import com.example.livingai_lg.ui.state.FilterStore
|
||||
|
||||
@Composable
|
||||
fun BuyScreen(
|
||||
initialFilters: FiltersState = FiltersState(),
|
||||
onProductClick: (productId: String) -> Unit = {},
|
||||
onBackClick: () -> Unit = {},
|
||||
onNavClick: (route: String) -> Unit = {},
|
||||
onFilterClick: () -> Unit = {},
|
||||
onSortClick: () -> Unit = {},
|
||||
onSellerClick: (sellerId: String) -> Unit = {},
|
||||
onWishlistClick: () -> Unit = {}
|
||||
) {
|
||||
var activeFilters by remember {
|
||||
mutableStateOf(initialFilters)
|
||||
mutableStateOf(FilterStore.filters.value)
|
||||
}
|
||||
val isSaved = remember { mutableStateOf(false) }
|
||||
var showAddressSelector by remember { mutableStateOf(false) }
|
||||
|
|
@ -110,17 +113,36 @@ fun BuyScreen(
|
|||
}
|
||||
)
|
||||
|
||||
// Right-side actions (notifications, etc.)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_notification_unread),
|
||||
contentDescription = "Notifications",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){ showNotifications = true }
|
||||
)
|
||||
|
||||
Row{
|
||||
// Right-side actions (notifications, etc.)
|
||||
Icon(
|
||||
imageVector = Icons.Default.FavoriteBorder,
|
||||
contentDescription = "Wishlist",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){ onWishlistClick() }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Icon(
|
||||
|
||||
painter = painterResource(R.drawable.ic_notification_unread),
|
||||
contentDescription = "Notifications",
|
||||
tint = Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
.clickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
){ showNotifications = true }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
|
|
@ -176,7 +198,7 @@ fun BuyScreen(
|
|||
SortButton(
|
||||
onClick = { showSortOverlay.value = true }
|
||||
)
|
||||
FilterButton(onClick = { showFilterOverlay.value = true })
|
||||
FilterButton(onClick = { showFilterOverlay.value = true }, hasActiveFilters = !activeFilters.isDefault())
|
||||
}
|
||||
|
||||
sampleAnimals.forEach { animal ->
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -16,8 +12,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowForwardIos
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.*
|
||||
|
|
@ -35,8 +29,8 @@ import com.example.livingai_lg.ui.components.WishlistNameOverlay
|
|||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.models.RangeFilterState
|
||||
import com.example.livingai_lg.ui.models.TextFilter
|
||||
import com.example.livingai_lg.ui.models.WishlistEntry
|
||||
import com.example.livingai_lg.ui.models.WishlistStore
|
||||
import com.example.livingai_lg.ui.state.WishlistEntry
|
||||
import com.example.livingai_lg.ui.state.WishlistStore
|
||||
import com.example.livingai_lg.ui.models.isDefault
|
||||
import com.example.livingai_lg.ui.theme.AppTypography
|
||||
|
||||
|
|
@ -118,6 +112,7 @@ fun FilterScreen(
|
|||
|
||||
if(!wishlistEditMode){
|
||||
IconButton(
|
||||
|
||||
onClick = {
|
||||
if (!filters.isDefault()) {
|
||||
showWishlistOverlay = true
|
||||
|
|
@ -126,8 +121,10 @@ fun FilterScreen(
|
|||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FavoriteBorder,
|
||||
contentDescription = "Add to Wishlist"
|
||||
contentDescription = "Add to Wishlist",
|
||||
tint = if(!filters.isDefault()) Color.Black else Color.Gray
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -101,6 +102,7 @@ fun SortScreen(
|
|||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
|
||||
) {
|
||||
|
||||
// Header
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package com.example.livingai_lg.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import com.example.livingai_lg.ui.state.FilterStore
|
||||
import com.example.livingai_lg.ui.state.WishlistStore
|
||||
|
||||
@Composable
|
||||
fun WishlistScreen(
|
||||
onApply: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
|
||||
val wishlist by WishlistStore.wishlist.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF7F4EE))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Saved Filters",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (wishlist.isEmpty()) {
|
||||
Text("No saved filters yet")
|
||||
} else {
|
||||
wishlist.forEach { entry ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.clickable {
|
||||
onApply()
|
||||
FilterStore.set(entry.filters)
|
||||
}
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = entry.name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "Tap to apply",
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.example.livingai_lg.ui.state
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
|
||||
object FilterStore {
|
||||
|
||||
private val _filters = MutableStateFlow(FiltersState())
|
||||
val filters: StateFlow<FiltersState> = _filters
|
||||
|
||||
fun update(block: (FiltersState) -> FiltersState) {
|
||||
_filters.update(block)
|
||||
}
|
||||
|
||||
fun set(newState: FiltersState) {
|
||||
_filters.value = newState
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_filters.value = FiltersState()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.livingai_lg.ui.models
|
||||
package com.example.livingai_lg.ui.state
|
||||
|
||||
import com.example.livingai_lg.ui.models.FiltersState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
Loading…
Reference in New Issue