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)
|
.height(257.dp)
|
||||||
) {
|
) {
|
||||||
ImageCarousel(
|
ImageCarousel(
|
||||||
imageUrls = product.imageUrl ?: emptyList(),
|
media = product.media ?: emptyList(),
|
||||||
|
enableFullscreenPreview = false,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,25 @@ import com.example.livingai_lg.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FilterButton(
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(36.dp)
|
.height(36.dp)
|
||||||
.border(1.078.dp, Color(0xFF000000).copy(alpha = 0.1f), RoundedCornerShape(8.dp))
|
.border(
|
||||||
.background(Color.White, RoundedCornerShape(8.dp))
|
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)
|
.padding(horizontal = 8.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = LocalIndication.current,
|
indication = LocalIndication.current,
|
||||||
|
|
@ -42,17 +54,31 @@ fun FilterButton(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_filter),
|
// Icon + dot indicator
|
||||||
contentDescription = "Filter",
|
Row {
|
||||||
tint = Color(0xFF0A0A0A),
|
Icon(
|
||||||
modifier = Modifier.size(16.dp)
|
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(
|
||||||
text = "Filter",
|
text = "Filter",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
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.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.livingai_lg.ui.models.MediaItem
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageCarousel(
|
fun ImageCarousel(
|
||||||
imageUrls: List<String>,
|
media: List<MediaItem>,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
enableFullscreenPreview: Boolean = false,
|
||||||
|
onMediaClick: (startIndex: Int) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val pagerState = rememberPagerState { media.size }
|
||||||
|
var startIndex by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
when {
|
when {
|
||||||
imageUrls.isEmpty() -> {
|
media.isEmpty() -> {
|
||||||
Box(
|
NoImagePlaceholder(modifier = modifier)
|
||||||
modifier = modifier
|
}
|
||||||
.background(Color.LightGray),
|
|
||||||
contentAlignment = Alignment.Center
|
media.size == 1 -> {
|
||||||
) {
|
when (val item = media.first()) {
|
||||||
Text("No images", color = Color.White)
|
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 -> {
|
else -> {
|
||||||
val pagerState = rememberPagerState { imageUrls.size }
|
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) { page ->
|
) { page ->
|
||||||
AsyncImage(
|
when (val item = media[page]) {
|
||||||
model = imageUrls[page],
|
is MediaItem.Image -> {
|
||||||
contentDescription = null,
|
AsyncImage(
|
||||||
modifier = Modifier.fillMaxSize(),
|
model = item.url,
|
||||||
contentScale = ContentScale.Crop
|
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)
|
// Page Indicator (inside image)
|
||||||
|
|
@ -71,7 +155,7 @@ fun ImageCarousel(
|
||||||
.padding(bottom = 8.dp),
|
.padding(bottom = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
repeat(imageUrls.size) { index ->
|
repeat(pagerState.pageCount) { index ->
|
||||||
val isSelected = pagerState.currentPage == index
|
val isSelected = pagerState.currentPage == index
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight(0.85f)
|
.fillMaxHeight(0.85f)
|
||||||
.fillMaxWidth(0.85f)
|
.fillMaxWidth(0.75f)
|
||||||
.background(Color(0xFFF7F4EE))
|
.background(Color(0xFFF7F4EE))
|
||||||
.clip(
|
.clip(
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WishlistNameOverlay(
|
fun WishlistNameOverlay(
|
||||||
|
|
@ -32,18 +36,28 @@ fun WishlistNameOverlay(
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
|
|
||||||
AnimatedVisibility(
|
Box(
|
||||||
visible = true,
|
modifier = Modifier.fillMaxSize(),
|
||||||
enter = slideInVertically { -it },
|
contentAlignment = Alignment.TopCenter
|
||||||
exit = slideOutVertically { -it }
|
|
||||||
) {
|
) {
|
||||||
Box(
|
AnimatedVisibility(
|
||||||
Modifier
|
visible = true,
|
||||||
.fillMaxWidth()
|
enter = slideInVertically { -it },
|
||||||
.background(Color.White)
|
exit = slideOutVertically { -it }
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
) {
|
||||||
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(
|
||||||
text = "Save Filters",
|
text = "Save Filters",
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
|
|
@ -54,12 +68,13 @@ fun WishlistNameOverlay(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
placeholder = { Text("Wishlist name") },
|
placeholder = { Text("Wishlist name") },
|
||||||
singleLine = true
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.End,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth()
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ data class Animal(
|
||||||
val breedInfo: String? = null,
|
val breedInfo: String? = null,
|
||||||
val price: Long? = null,
|
val price: Long? = null,
|
||||||
val isFairPrice: Boolean? = null,
|
val isFairPrice: Boolean? = null,
|
||||||
val imageUrl: List<String>? = null,
|
val media: List<MediaItem>? = null,
|
||||||
val location: String? = null,
|
val location: String? = null,
|
||||||
val displayLocation: String? = null,
|
val displayLocation: String? = null,
|
||||||
val distance: Long? = null,
|
val distance: Long? = null,
|
||||||
|
|
@ -32,7 +32,9 @@ val sampleAnimals = listOf(
|
||||||
breedInfo = "The best in India",
|
breedInfo = "The best in India",
|
||||||
location = "Punjab",
|
location = "Punjab",
|
||||||
distance = 12000,
|
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,
|
views = 9001,
|
||||||
aiScore = 0.80f,
|
aiScore = 0.80f,
|
||||||
price = 120000,
|
price = 120000,
|
||||||
|
|
@ -54,7 +56,7 @@ val sampleAnimals = listOf(
|
||||||
breedInfo = "The 2nd best in India",
|
breedInfo = "The 2nd best in India",
|
||||||
location = "Punjab",
|
location = "Punjab",
|
||||||
isFairPrice = true,
|
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,
|
distance = 0L,
|
||||||
views = 100,
|
views = 100,
|
||||||
sellerId = "1",
|
sellerId = "1",
|
||||||
|
|
@ -73,7 +75,7 @@ val sampleAnimals = listOf(
|
||||||
breedInfo = "Not Indian",
|
breedInfo = "Not Indian",
|
||||||
location = "Punjab",
|
location = "Punjab",
|
||||||
distance = 12000,
|
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,
|
views = 94,
|
||||||
aiScore = 0.80f,
|
aiScore = 0.80f,
|
||||||
price = 80000,
|
price = 80000,
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,4 @@ fun FiltersState.isDefault(): Boolean {
|
||||||
pregnancyStatuses.isEmpty() &&
|
pregnancyStatuses.isEmpty() &&
|
||||||
calving.filterSet.not()
|
calving.filterSet.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,10 @@ class MediaUpload(
|
||||||
|
|
||||||
enum class MediaType {
|
enum class MediaType {
|
||||||
PHOTO, VIDEO
|
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.SavedListingsScreen
|
||||||
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
import com.example.livingai_lg.ui.screens.SellerProfileScreen
|
||||||
import com.example.livingai_lg.ui.screens.SortScreen
|
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.LandingScreen
|
||||||
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||||
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
import com.example.livingai_lg.ui.screens.auth.SignInScreen
|
||||||
|
|
@ -278,6 +279,18 @@ fun AppNavigation(
|
||||||
AppScreen.sellerProfile(sellerId)
|
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_FILTERS = "buy_animals_filters"
|
||||||
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
const val BUY_ANIMALS_SORT = "buy_animals_sort"
|
||||||
|
|
||||||
|
const val WISHLIST = "wishlist"
|
||||||
|
|
||||||
const val SELLER_PROFILE = "seller_profile"
|
const val SELLER_PROFILE = "seller_profile"
|
||||||
fun sellerProfile(sellerId: String) =
|
fun sellerProfile(sellerId: String) =
|
||||||
"$SELLER_PROFILE/$sellerId"
|
"$SELLER_PROFILE/$sellerId"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.automirrored.filled.StarHalf
|
||||||
import androidx.compose.material.icons.filled.Bookmark
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
import androidx.compose.material.icons.filled.Star
|
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.ui.utils.formatAge
|
||||||
import com.example.livingai_lg.R
|
import com.example.livingai_lg.R
|
||||||
import com.example.livingai_lg.ui.components.ActionPopup
|
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.components.RatingStars
|
||||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
import com.example.livingai_lg.ui.theme.AppTypography
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
@ -67,12 +69,14 @@ fun AnimalProfileScreen(
|
||||||
onNavClick: (route: String) -> Unit = {}
|
onNavClick: (route: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var showSavedPopup by remember { mutableStateOf(false) }
|
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")
|
val animal = sampleAnimals.find { animal -> animal.id == animalId } ?: Animal(id = "null")
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color(0xFFF7F4EE))
|
.background(Color(0xFFF7F4EE))
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -90,8 +94,13 @@ fun AnimalProfileScreen(
|
||||||
// Main image
|
// Main image
|
||||||
val product = null
|
val product = null
|
||||||
ImageCarousel(
|
ImageCarousel(
|
||||||
imageUrls = animal.imageUrl ?: emptyList(),
|
media = animal.media ?: emptyList(),
|
||||||
modifier = Modifier.fillMaxSize()
|
enableFullscreenPreview = true,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onMediaClick = { startIndex ->
|
||||||
|
showMediaOverlay = true
|
||||||
|
mediaOverlayStartIndex = startIndex
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gradient overlay at bottom
|
// 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)
|
// Views indicator (top left)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopEnd)
|
||||||
.padding(start = 5.dp, top = 5.dp)
|
.padding(end = 5.dp, top = 5.dp)
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = 6.dp,
|
elevation = 6.dp,
|
||||||
shape = RoundedCornerShape(50),
|
shape = RoundedCornerShape(50),
|
||||||
|
|
@ -226,7 +262,7 @@ fun AnimalProfileScreen(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 36.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
|
@ -348,7 +384,8 @@ fun AnimalProfileScreen(
|
||||||
FloatingActionBar(
|
FloatingActionBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.padding(bottom = 10.dp)
|
.padding(horizontal = 16.dp,
|
||||||
|
16.dp)
|
||||||
.offset(y = (-10).dp)
|
.offset(y = (-10).dp)
|
||||||
.zIndex(10f), // 👈 ensure it floats above everything
|
.zIndex(10f), // 👈 ensure it floats above everything
|
||||||
onChatClick = { /* TODO */ },
|
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) {}
|
fun Modifier.Companion.align(bottomEnd: Alignment) {}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Bookmark
|
import androidx.compose.material.icons.filled.Bookmark
|
||||||
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
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.components.SortOverlay
|
||||||
import com.example.livingai_lg.ui.models.FiltersState
|
import com.example.livingai_lg.ui.models.FiltersState
|
||||||
import com.example.livingai_lg.ui.models.TextFilter
|
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.models.sampleNotifications
|
||||||
import com.example.livingai_lg.ui.navigation.AppScreen
|
import com.example.livingai_lg.ui.navigation.AppScreen
|
||||||
|
import com.example.livingai_lg.ui.state.FilterStore
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BuyScreen(
|
fun BuyScreen(
|
||||||
initialFilters: FiltersState = FiltersState(),
|
|
||||||
onProductClick: (productId: String) -> Unit = {},
|
onProductClick: (productId: String) -> Unit = {},
|
||||||
onBackClick: () -> Unit = {},
|
onBackClick: () -> Unit = {},
|
||||||
onNavClick: (route: String) -> Unit = {},
|
onNavClick: (route: String) -> Unit = {},
|
||||||
onFilterClick: () -> Unit = {},
|
onFilterClick: () -> Unit = {},
|
||||||
onSortClick: () -> Unit = {},
|
onSortClick: () -> Unit = {},
|
||||||
onSellerClick: (sellerId: String) -> Unit = {},
|
onSellerClick: (sellerId: String) -> Unit = {},
|
||||||
|
onWishlistClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var activeFilters by remember {
|
var activeFilters by remember {
|
||||||
mutableStateOf(initialFilters)
|
mutableStateOf(FilterStore.filters.value)
|
||||||
}
|
}
|
||||||
val isSaved = remember { mutableStateOf(false) }
|
val isSaved = remember { mutableStateOf(false) }
|
||||||
var showAddressSelector by remember { mutableStateOf(false) }
|
var showAddressSelector by remember { mutableStateOf(false) }
|
||||||
|
|
@ -110,17 +113,36 @@ fun BuyScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Right-side actions (notifications, etc.)
|
|
||||||
Icon(
|
Row{
|
||||||
painter = painterResource(R.drawable.ic_notification_unread),
|
// Right-side actions (notifications, etc.)
|
||||||
contentDescription = "Notifications",
|
Icon(
|
||||||
tint = Color.Black,
|
imageVector = Icons.Default.FavoriteBorder,
|
||||||
modifier = Modifier.size(24.dp)
|
contentDescription = "Wishlist",
|
||||||
.clickable(
|
tint = Color.Black,
|
||||||
indication = LocalIndication.current,
|
modifier = Modifier.size(24.dp)
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
.clickable(
|
||||||
){ showNotifications = true }
|
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(
|
// Row(
|
||||||
// modifier = Modifier
|
// modifier = Modifier
|
||||||
|
|
@ -176,7 +198,7 @@ fun BuyScreen(
|
||||||
SortButton(
|
SortButton(
|
||||||
onClick = { showSortOverlay.value = true }
|
onClick = { showSortOverlay.value = true }
|
||||||
)
|
)
|
||||||
FilterButton(onClick = { showFilterOverlay.value = true })
|
FilterButton(onClick = { showFilterOverlay.value = true }, hasActiveFilters = !activeFilters.isDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleAnimals.forEach { animal ->
|
sampleAnimals.forEach { animal ->
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
package com.example.livingai_lg.ui.screens
|
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.BorderStroke
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -16,8 +12,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
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.Check
|
||||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||||
import androidx.compose.material3.*
|
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.FiltersState
|
||||||
import com.example.livingai_lg.ui.models.RangeFilterState
|
import com.example.livingai_lg.ui.models.RangeFilterState
|
||||||
import com.example.livingai_lg.ui.models.TextFilter
|
import com.example.livingai_lg.ui.models.TextFilter
|
||||||
import com.example.livingai_lg.ui.models.WishlistEntry
|
import com.example.livingai_lg.ui.state.WishlistEntry
|
||||||
import com.example.livingai_lg.ui.models.WishlistStore
|
import com.example.livingai_lg.ui.state.WishlistStore
|
||||||
import com.example.livingai_lg.ui.models.isDefault
|
import com.example.livingai_lg.ui.models.isDefault
|
||||||
import com.example.livingai_lg.ui.theme.AppTypography
|
import com.example.livingai_lg.ui.theme.AppTypography
|
||||||
|
|
||||||
|
|
@ -118,6 +112,7 @@ fun FilterScreen(
|
||||||
|
|
||||||
if(!wishlistEditMode){
|
if(!wishlistEditMode){
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!filters.isDefault()) {
|
if (!filters.isDefault()) {
|
||||||
showWishlistOverlay = true
|
showWishlistOverlay = true
|
||||||
|
|
@ -126,8 +121,10 @@ fun FilterScreen(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.FavoriteBorder,
|
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.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -101,6 +102,7 @@ fun SortScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color(0xFFF7F4EE))
|
.background(Color(0xFFF7F4EE))
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Header
|
// 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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
Loading…
Reference in New Issue