diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/BuyAnimalCard.kt b/app/src/main/java/com/example/livingai_lg/ui/components/BuyAnimalCard.kt index 67f9db9..64bb863 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/BuyAnimalCard.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/BuyAnimalCard.kt @@ -76,7 +76,8 @@ fun BuyAnimalCard( .height(257.dp) ) { ImageCarousel( - imageUrls = product.imageUrl ?: emptyList(), + media = product.media ?: emptyList(), + enableFullscreenPreview = false, modifier = Modifier.fillMaxSize() ) diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/FilterButton.kt b/app/src/main/java/com/example/livingai_lg/ui/components/FilterButton.kt index 76697bf..9c9a7b2 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/FilterButton.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/FilterButton.kt @@ -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 ) } } diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/ImageCarousel.kt b/app/src/main/java/com/example/livingai_lg/ui/components/ImageCarousel.kt index f0b27d3..83d9e53 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/ImageCarousel.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/ImageCarousel.kt @@ -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, - modifier: Modifier = Modifier + media: List, + 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 diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/MediaFullscreenOverlay.kt b/app/src/main/java/com/example/livingai_lg/ui/components/MediaFullscreenOverlay.kt new file mode 100644 index 0000000..6d8aba4 --- /dev/null +++ b/app/src/main/java/com/example/livingai_lg/ui/components/MediaFullscreenOverlay.kt @@ -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, + 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 + } + + } + } + ) +} diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt b/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt index 9b596bd..d732d67 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/SortOverlay.kt @@ -66,7 +66,7 @@ fun SortOverlay( Box( modifier = Modifier .fillMaxHeight(0.85f) - .fillMaxWidth(0.85f) + .fillMaxWidth(0.75f) .background(Color(0xFFF7F4EE)) .clip( RoundedCornerShape( diff --git a/app/src/main/java/com/example/livingai_lg/ui/components/wishlistNameOverlay.kt b/app/src/main/java/com/example/livingai_lg/ui/components/wishlistNameOverlay.kt index c14edc4..9250aa4 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/components/wishlistNameOverlay.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/components/wishlistNameOverlay.kt @@ -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") diff --git a/app/src/main/java/com/example/livingai_lg/ui/models/Animal.kt b/app/src/main/java/com/example/livingai_lg/ui/models/Animal.kt index 7437977..bd10b01 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/models/Animal.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/models/Animal.kt @@ -8,7 +8,7 @@ data class Animal( val breedInfo: String? = null, val price: Long? = null, val isFairPrice: Boolean? = null, - val imageUrl: List? = null, + val media: List? = 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, diff --git a/app/src/main/java/com/example/livingai_lg/ui/models/FilterState.kt b/app/src/main/java/com/example/livingai_lg/ui/models/FilterState.kt index 9013331..a1f4c30 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/models/FilterState.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/models/FilterState.kt @@ -40,3 +40,4 @@ fun FiltersState.isDefault(): Boolean { pregnancyStatuses.isEmpty() && calving.filterSet.not() } + diff --git a/app/src/main/java/com/example/livingai_lg/ui/models/mediaType.kt b/app/src/main/java/com/example/livingai_lg/ui/models/mediaType.kt index 7719bd8..52c3462 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/models/mediaType.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/models/mediaType.kt @@ -16,4 +16,10 @@ class MediaUpload( enum class MediaType { PHOTO, VIDEO -} \ No newline at end of file +} + +sealed class MediaItem { + data class Image(val url: String) : MediaItem() + data class Video(val url: String, val thumbnailUrl: String? = null) : MediaItem() + companion object +} diff --git a/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt b/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt index c8c9921..3992f11 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/navigation/AppNavigation.kt @@ -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()} ) } diff --git a/app/src/main/java/com/example/livingai_lg/ui/navigation/AppScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/navigation/AppScreen.kt index bfa2960..0c12da4 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/navigation/AppScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/navigation/AppScreen.kt @@ -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" diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/AnimalProfileScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/AnimalProfileScreen.kt index 892d625..82d14d9 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/AnimalProfileScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/AnimalProfileScreen.kt @@ -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) {} diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt index 7eb094e..c6161d4 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/BuyScreen.kt @@ -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 -> diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt index 0132e03..524d167 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/FilterScreen.kt @@ -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 ) + } } } diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt index c3ff298..554c7dc 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/SortScreen.kt @@ -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 diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/WishlistScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/WishlistScreen.kt new file mode 100644 index 0000000..d88c433 --- /dev/null +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/WishlistScreen.kt @@ -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 + ) + } + } + } + } + } +} + diff --git a/app/src/main/java/com/example/livingai_lg/ui/state/FilterStore.kt b/app/src/main/java/com/example/livingai_lg/ui/state/FilterStore.kt new file mode 100644 index 0000000..289cf53 --- /dev/null +++ b/app/src/main/java/com/example/livingai_lg/ui/state/FilterStore.kt @@ -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 = _filters + + fun update(block: (FiltersState) -> FiltersState) { + _filters.update(block) + } + + fun set(newState: FiltersState) { + _filters.value = newState + } + + fun reset() { + _filters.value = FiltersState() + } +} diff --git a/app/src/main/java/com/example/livingai_lg/ui/models/Wishlist.kt b/app/src/main/java/com/example/livingai_lg/ui/state/WishlistStore.kt similarity index 88% rename from app/src/main/java/com/example/livingai_lg/ui/models/Wishlist.kt rename to app/src/main/java/com/example/livingai_lg/ui/state/WishlistStore.kt index c8b48a5..cb32fe7 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/models/Wishlist.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/state/WishlistStore.kt @@ -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