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:
ankitsaraf 2025-12-21 19:59:37 +05:30
parent 3cd8a005c9
commit eb7e19228b
18 changed files with 628 additions and 87 deletions

View File

@ -76,7 +76,8 @@ fun BuyAnimalCard(
.height(257.dp)
) {
ImageCarousel(
imageUrls = product.imageUrl ?: emptyList(),
media = product.media ?: emptyList(),
enableFullscreenPreview = false,
modifier = Modifier.fillMaxSize()
)

View File

@ -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
)
}
}

View File

@ -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

View File

@ -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
}
}
}
)
}

View File

@ -66,7 +66,7 @@ fun SortOverlay(
Box(
modifier = Modifier
.fillMaxHeight(0.85f)
.fillMaxWidth(0.85f)
.fillMaxWidth(0.75f)
.background(Color(0xFFF7F4EE))
.clip(
RoundedCornerShape(

View File

@ -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")

View File

@ -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,

View File

@ -40,3 +40,4 @@ fun FiltersState.isDefault(): Boolean {
pregnancyStatuses.isEmpty() &&
calving.filterSet.not()
}

View File

@ -17,3 +17,9 @@ 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
}

View File

@ -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()}
)
}

View File

@ -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"

View File

@ -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) {}

View File

@ -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 ->

View File

@ -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
)
}
}
}

View File

@ -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

View File

@ -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
)
}
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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