From bc0070c3f795ae5c93c04705349590e23f9f72e3 Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Wed, 26 Mar 2025 16:28:40 -0400 Subject: [PATCH 01/11] Fix sheetHeightFromBottom calculation --- .../resell/android/ui/screens/pdp/PostDetailPage.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt index e4add505..77653ba9 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt @@ -327,16 +327,14 @@ private fun BottomSheetContent( .defaultHorizontalPadding() .onGloballyPositioned { layoutCoordinates -> val textPosition = layoutCoordinates.positionInRoot().y - val textHeight = layoutCoordinates.size.height - val screenHeightPx = with(density) { screenHeight.toPx() } // Calculate distance from bottom in px and convert to dp - val distanceFromBottomPx = screenHeightPx - (textPosition + textHeight) + val distanceFromBottomPx = screenHeightPx - textPosition val textDistanceFromBottom = with(density) { distanceFromBottomPx.toDp() } // Tell the parent that the height has changed. - onHeightChanged(textDistanceFromBottom + 170.dp) + onHeightChanged(textDistanceFromBottom + 115.dp) }, verticalAlignment = Alignment.CenterVertically ) { From 14cded47ac5c37b7a59789028d89f12620191035 Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Tue, 8 Apr 2025 15:31:39 -0400 Subject: [PATCH 02/11] Increase distance from bottom --- .../resell/android/ui/screens/pdp/PostDetailPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt index 77653ba9..3fd0d17e 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt @@ -334,7 +334,7 @@ private fun BottomSheetContent( val textDistanceFromBottom = with(density) { distanceFromBottomPx.toDp() } // Tell the parent that the height has changed. - onHeightChanged(textDistanceFromBottom + 115.dp) + onHeightChanged(textDistanceFromBottom + 150.dp) }, verticalAlignment = Alignment.CenterVertically ) { From 00f4c9bd7ff1945a70011fa11adeae50a05a6e5a Mon Sep 17 00:00:00 2001 From: Justin Guo Date: Wed, 9 Apr 2025 15:45:49 -0400 Subject: [PATCH 03/11] Add `Recent` pagination --- .../android/model/api/PostsApiService.kt | 6 ++- .../model/posts/ResellPostRepository.kt | 34 ++++++++--------- .../components/global/ResellListingScroll.kt | 26 +++++++++++++ .../android/ui/screens/main/HomeScreen.kt | 27 +++++++++++++- .../android/viewmodel/main/HomeViewModel.kt | 37 +++++++++++++++++-- 5 files changed, 106 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt index 1220037f..d9d453fd 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt @@ -10,6 +10,7 @@ import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -17,7 +18,10 @@ import java.util.TimeZone interface PostsApiService { @GET("post") - suspend fun getPosts(): PostsResponse + suspend fun getPosts( + @Query("page") page: Int = 1, + @Query("limit") size: Int = 10 + ): PostsResponse @GET("post/id/{id}") suspend fun getPost(@Path("id") id: String): Post diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt index 8330f767..ef6747a7 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt @@ -6,6 +6,7 @@ import com.cornellappdev.resell.android.model.api.NewPostBody import com.cornellappdev.resell.android.model.api.Post import com.cornellappdev.resell.android.model.api.PostResponse import com.cornellappdev.resell.android.model.api.RetrofitInstance +import com.cornellappdev.resell.android.model.classes.Listing import com.cornellappdev.resell.android.model.classes.ResellApiResponse import com.cornellappdev.resell.android.util.toNetworkingString import kotlinx.coroutines.CoroutineScope @@ -29,20 +30,8 @@ class ResellPostRepository @Inject constructor( MutableStateFlow>>(ResellApiResponse.Pending) val allPostsFlow = _allPostsFlow.asStateFlow() - private val _savedPostsIds = - MutableStateFlow>>(ResellApiResponse.Pending) - - val savedPosts = _savedPostsIds.combine(allPostsFlow) { savedIds, posts -> - savedIds.combine(posts).map { (saved, list) -> - list.filter { - saved.contains(it.id) - } - } - }.stateIn( - scope = CoroutineScope(Dispatchers.IO), - started = SharingStarted.Eagerly, - initialValue = ResellApiResponse.Pending - ) + val _savedPosts = MutableStateFlow>>(ResellApiResponse.Pending) + val savedPosts = _savedPosts.asStateFlow() private var recentBitmaps: List? = null @@ -50,13 +39,16 @@ class ResellPostRepository @Inject constructor( * Asynchronously fetches the list of posts from the API. Once finished, will send down * `allPostsFlow` to be observed. */ - fun fetchPosts() { + fun fetchPosts(page: Int = 1) { _allPostsFlow.value = ResellApiResponse.Pending CoroutineScope(Dispatchers.IO).launch { try { _allPostsFlow.value = ResellApiResponse.Success( - retrofitInstance.postsApi.getPosts().posts + retrofitInstance.postsApi.getPosts( + page = page + ) + .posts .sortedByDescending { it.createdDate }) @@ -67,6 +59,10 @@ class ResellPostRepository @Inject constructor( } } + suspend fun getPostsByPage(page: Int): List { + return retrofitInstance.postsApi.getPosts(page = page).posts + } + suspend fun uploadPost( title: String, description: String, @@ -127,15 +123,15 @@ class ResellPostRepository @Inject constructor( * Sometime after calling, [savedPosts] will be updated. */ fun fetchSavedPosts() { - _savedPostsIds.value = ResellApiResponse.Pending + _savedPosts.value = ResellApiResponse.Pending CoroutineScope(Dispatchers.IO).launch { try { val saved = retrofitInstance.postsApi.getSavedPosts().posts - _savedPostsIds.value = ResellApiResponse.Success(saved.map { it.id }) + _savedPosts.value = ResellApiResponse.Success(saved) } catch (e: Exception) { Log.e("ResellPostRepository", "Error fetching saved posts: ", e) - _savedPostsIds.value = ResellApiResponse.Error + _savedPosts.value = ResellApiResponse.Error } } } diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt index 2c06e8e4..d7379862 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt @@ -10,11 +10,15 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.resell.android.model.classes.Listing import com.cornellappdev.resell.android.ui.theme.Padding +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map @Composable @@ -26,12 +30,30 @@ fun ResellListingsScroll( paddedTop: Dp = 0.dp, emptyState: @Composable () -> Unit = { }, header: @Composable () -> Unit = {}, + onScrollBottom: () -> Unit = {}, + footer: @Composable () -> Unit = {}, ) { if (listings.isEmpty()) { emptyState() return } + // Check if last visible item is the last in the list + LaunchedEffect(listState) { + snapshotFlow { listState.layoutInfo } + .map { layoutInfo -> + // -1 to account for the header and footer + val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index?.minus(2) + lastVisibleItemIndex == listings.lastIndex + } + .distinctUntilChanged() + .collect { isAtBottom -> + if (isAtBottom) { + onScrollBottom() + } + } + } + LazyVerticalStaggeredGrid( state = listState, columns = StaggeredGridCells.Fixed(2), @@ -56,5 +78,9 @@ fun ResellListingsScroll( onListingPressed(item) } } + + item(span = StaggeredGridItemSpan.FullLine) { + footer() + } } } diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt index 09df2e89..510cfa05 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt @@ -1,5 +1,6 @@ package com.cornellappdev.resell.android.ui.screens.main +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -10,12 +11,14 @@ import androidx.compose.foundation.layout.WindowInsets 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.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -33,6 +36,7 @@ import com.cornellappdev.resell.android.ui.components.global.ResellLoadingListin import com.cornellappdev.resell.android.ui.components.global.ResellTag import com.cornellappdev.resell.android.ui.theme.Padding import com.cornellappdev.resell.android.ui.theme.Primary +import com.cornellappdev.resell.android.ui.theme.ResellPurple import com.cornellappdev.resell.android.ui.theme.Style import com.cornellappdev.resell.android.util.clickableNoIndication import com.cornellappdev.resell.android.util.defaultHorizontalPadding @@ -49,7 +53,8 @@ fun HomeScreen( Column( modifier = Modifier - .fillMaxSize() + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally ) { HomeHeader( activeFilter = homeUiState.activeFilter, @@ -70,6 +75,24 @@ fun HomeScreen( homeViewModel.onListingPressed(it) }, listState = listState, + onScrollBottom = { + homeViewModel.onHitBottom() + }, + footer = { + AnimatedVisibility(visible = homeUiState.bottomLoading) { + Row( + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + color = ResellPurple, + modifier = Modifier + .padding(16.dp) + .size(40.dp) + ) + } + } + } ) } @@ -79,6 +102,8 @@ fun HomeScreen( is ResellApiState.Error -> {} } + + } } diff --git a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt index 283f3dbc..1e07c874 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt @@ -1,5 +1,6 @@ package com.cornellappdev.resell.android.viewmodel.main +import androidx.lifecycle.viewModelScope import com.cornellappdev.resell.android.model.classes.Listing import com.cornellappdev.resell.android.model.classes.ResellApiResponse import com.cornellappdev.resell.android.model.classes.ResellApiState @@ -9,6 +10,7 @@ import com.cornellappdev.resell.android.ui.screens.root.ResellRootRoute import com.cornellappdev.resell.android.viewmodel.ResellViewModel import com.cornellappdev.resell.android.viewmodel.navigation.RootNavigationRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -20,16 +22,20 @@ class HomeViewModel @Inject constructor( initialUiState = HomeUiState( listings = listOf(), activeFilter = HomeFilter.RECENT, - loadedState = ResellApiState.Loading + loadedState = ResellApiState.Loading, + page = 1, + bottomLoading = false ) ) { data class HomeUiState( val loadedState: ResellApiState, - private val listings: List, + val listings: List, val activeFilter: HomeFilter, + val page: Int, + val bottomLoading: Boolean ) { - // TODO This should change to an endpoint, but backend is simple. + // TODO Need to use endpoint now val filteredListings: List get() = listings.filter { activeFilter == HomeFilter.RECENT || @@ -90,4 +96,29 @@ class HomeViewModel @Inject constructor( fun onSearchPressed() { rootNavigationRepository.navigate(ResellRootRoute.SEARCH) } + + fun onHitBottom() { + if (stateValue().bottomLoading) { + return + } + + viewModelScope.launch { + applyMutation { + copy( + page = page + 1, + bottomLoading = true + ) + } + + val newPage = resellPostRepository.getPostsByPage(stateValue().page).map { + it.toListing() + } + applyMutation { + copy( + listings = stateValue().listings + newPage, + bottomLoading = false + ) + } + } + } } From 3fb72235c33e74de5efe413f246d0cd2f1c65453 Mon Sep 17 00:00:00 2001 From: Justin Guo Date: Wed, 9 Apr 2025 15:59:05 -0400 Subject: [PATCH 04/11] Use filter endpoint instead of local filtering --- .../android/model/api/PostsApiService.kt | 2 +- .../model/posts/ResellPostRepository.kt | 9 +- .../android/ui/screens/main/HomeScreen.kt | 7 +- .../android/viewmodel/main/HomeViewModel.kt | 91 ++++++++++++------- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt index d9d453fd..6ca05903 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt @@ -68,7 +68,7 @@ data class SearchRequest( ) data class CategoryRequest( - val category: String + val categories: List ) diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt index ef6747a7..48281489 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt @@ -2,20 +2,17 @@ package com.cornellappdev.resell.android.model.posts import android.util.Log import androidx.compose.ui.graphics.ImageBitmap +import com.cornellappdev.resell.android.model.api.CategoryRequest import com.cornellappdev.resell.android.model.api.NewPostBody import com.cornellappdev.resell.android.model.api.Post import com.cornellappdev.resell.android.model.api.PostResponse import com.cornellappdev.resell.android.model.api.RetrofitInstance -import com.cornellappdev.resell.android.model.classes.Listing import com.cornellappdev.resell.android.model.classes.ResellApiResponse import com.cornellappdev.resell.android.util.toNetworkingString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @@ -63,6 +60,10 @@ class ResellPostRepository @Inject constructor( return retrofitInstance.postsApi.getPosts(page = page).posts } + suspend fun getPostByFilter(category: String): List { + return retrofitInstance.postsApi.getFilteredPosts(CategoryRequest(listOf(category))).posts + } + suspend fun uploadPost( title: String, description: String, diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt index 510cfa05..242a7a9a 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt @@ -70,7 +70,7 @@ fun HomeScreen( when (homeUiState.loadedState) { is ResellApiState.Success -> { ResellListingsScroll( - listings = homeUiState.filteredListings, + listings = homeUiState.listings, onListingPressed = { homeViewModel.onListingPressed(it) }, @@ -79,7 +79,10 @@ fun HomeScreen( homeViewModel.onHitBottom() }, footer = { - AnimatedVisibility(visible = homeUiState.bottomLoading) { + AnimatedVisibility( + visible = homeUiState.bottomLoading, + modifier = Modifier.padding(bottom = 50.dp) + ) { Row( horizontalArrangement = Arrangement.Center ) { diff --git a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt index 1e07c874..e6953399 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt @@ -1,10 +1,9 @@ package com.cornellappdev.resell.android.viewmodel.main +import android.util.Log import androidx.lifecycle.viewModelScope import com.cornellappdev.resell.android.model.classes.Listing -import com.cornellappdev.resell.android.model.classes.ResellApiResponse import com.cornellappdev.resell.android.model.classes.ResellApiState -import com.cornellappdev.resell.android.model.classes.toResellApiState import com.cornellappdev.resell.android.model.posts.ResellPostRepository import com.cornellappdev.resell.android.ui.screens.root.ResellRootRoute import com.cornellappdev.resell.android.viewmodel.ResellViewModel @@ -34,36 +33,10 @@ class HomeViewModel @Inject constructor( val activeFilter: HomeFilter, val page: Int, val bottomLoading: Boolean - ) { - // TODO Need to use endpoint now - val filteredListings: List- get() = listings.filter { - activeFilter == HomeFilter.RECENT || - it.categories.map { it.lowercase() }.any { - it.contains(activeFilter.name.lowercase()) - } - } - } + ) init { - asyncCollect(resellPostRepository.allPostsFlow) { response -> - val posts = when (response) { - is ResellApiResponse.Success -> { - response.data - } - - else -> { - listOf() - } - } - - applyMutation { - copy( - listings = posts.map { it.toListing() }, - loadedState = response.toResellApiState() - ) - } - } + onRecentPressed() } enum class HomeFilter { @@ -87,9 +60,63 @@ class HomeViewModel @Inject constructor( ) } + fun onRecentPressed() { + applyMutation { + copy( + activeFilter = HomeFilter.RECENT, + page = 1, + loadedState = ResellApiState.Loading + ) + } + viewModelScope.launch { + try { + val posts = resellPostRepository.getPostsByPage(1) + applyMutation { + copy( + listings = posts.map { it.toListing() }, + loadedState = ResellApiState.Success + ) + } + } catch (e: Exception) { + Log.e("HomeViewModel", "Error fetching posts: ", e) + applyMutation { + copy( + loadedState = ResellApiState.Error + ) + } + } + } + } + fun onToggleFilter(filter: HomeFilter) { + if (filter == HomeFilter.RECENT) { + onRecentPressed() + return + } + applyMutation { - copy(activeFilter = filter) + copy( + activeFilter = filter, + loadedState = ResellApiState.Loading, + ) + } + viewModelScope.launch { + try { + val posts = resellPostRepository.getPostByFilter(filter.name) + applyMutation { + copy( + listings = posts.map { it.toListing() }, + loadedState = ResellApiState.Success + ) + } + } catch (e: Exception) { + Log.e("HomeViewModel", "Error fetching posts: ", e) + applyMutation { + copy( + loadedState = ResellApiState.Error + ) + } + } } } @@ -98,7 +125,7 @@ class HomeViewModel @Inject constructor( } fun onHitBottom() { - if (stateValue().bottomLoading) { + if (stateValue().bottomLoading || stateValue().activeFilter != HomeFilter.RECENT) { return } From 555232c714bd3e064ab3aeb95d00a8d831b7511e Mon Sep 17 00:00:00 2001 From: Justin Guo Date: Wed, 9 Apr 2025 17:17:29 -0400 Subject: [PATCH 05/11] Make recent tab function private --- .../resell/android/viewmodel/main/HomeViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt index e6953399..ce9e825e 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt @@ -60,7 +60,7 @@ class HomeViewModel @Inject constructor( ) } - fun onRecentPressed() { + private fun onRecentPressed() { applyMutation { copy( activeFilter = HomeFilter.RECENT, From 1adf34f39c4063ac6021c883f94720275e0e0c3b Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Sun, 13 Apr 2025 10:40:26 -0400 Subject: [PATCH 06/11] final fix --- .../resell/android/ui/screens/pdp/PostDetailPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt index 50d8bece..c6ae9dad 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt @@ -334,7 +334,7 @@ private fun BottomSheetContent( val textDistanceFromBottom = with(density) { distanceFromBottomPx.toDp() } // Tell the parent that the height has changed. - onHeightChanged(textDistanceFromBottom + 150.dp) + onHeightChanged(textDistanceFromBottom + 120.dp) }, verticalAlignment = Alignment.CenterVertically ) { From e9ad35313638980552a91cc67a9cf335a3c540a4 Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Tue, 15 Apr 2025 11:31:11 -0400 Subject: [PATCH 07/11] Fix height --- .../resell/android/ui/screens/pdp/PostDetailPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt index 3fd0d17e..4a5ba809 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt @@ -334,7 +334,7 @@ private fun BottomSheetContent( val textDistanceFromBottom = with(density) { distanceFromBottomPx.toDp() } // Tell the parent that the height has changed. - onHeightChanged(textDistanceFromBottom + 150.dp) + onHeightChanged(textDistanceFromBottom + 120.dp) }, verticalAlignment = Alignment.CenterVertically ) { From 90ea826b0dda5961b31f4832494211c789cbba62 Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Tue, 15 Apr 2025 21:50:57 -0400 Subject: [PATCH 08/11] Manually revert files to main versions --- .../android/model/api/PostsApiService.kt | 10 +- .../model/posts/ResellPostRepository.kt | 43 +++--- .../components/global/ResellListingScroll.kt | 28 +--- .../android/ui/screens/main/HomeScreen.kt | 34 +---- .../android/viewmodel/main/HomeViewModel.kt | 126 +++++------------- 5 files changed, 64 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt index 6ca05903..06d0666b 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt @@ -10,7 +10,6 @@ import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path -import retrofit2.http.Query import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -18,10 +17,7 @@ import java.util.TimeZone interface PostsApiService { @GET("post") - suspend fun getPosts( - @Query("page") page: Int = 1, - @Query("limit") size: Int = 10 - ): PostsResponse + suspend fun getPosts(): PostsResponse @GET("post/id/{id}") suspend fun getPost(@Path("id") id: String): Post @@ -68,7 +64,7 @@ data class SearchRequest( ) data class CategoryRequest( - val categories: List + val category: String ) @@ -136,4 +132,4 @@ data class NewPostBody( data class IsSavedResponse( val isSaved: Boolean -) +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt index 48281489..704c35da 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt @@ -2,7 +2,6 @@ package com.cornellappdev.resell.android.model.posts import android.util.Log import androidx.compose.ui.graphics.ImageBitmap -import com.cornellappdev.resell.android.model.api.CategoryRequest import com.cornellappdev.resell.android.model.api.NewPostBody import com.cornellappdev.resell.android.model.api.Post import com.cornellappdev.resell.android.model.api.PostResponse @@ -12,7 +11,10 @@ import com.cornellappdev.resell.android.util.toNetworkingString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject @@ -27,8 +29,20 @@ class ResellPostRepository @Inject constructor( MutableStateFlow>>(ResellApiResponse.Pending) val allPostsFlow = _allPostsFlow.asStateFlow() - val _savedPosts = MutableStateFlow>>(ResellApiResponse.Pending) - val savedPosts = _savedPosts.asStateFlow() + private val _savedPostsIds = + MutableStateFlow>>(ResellApiResponse.Pending) + + val savedPosts = _savedPostsIds.combine(allPostsFlow) { savedIds, posts -> + savedIds.combine(posts).map { (saved, list) -> + list.filter { + saved.contains(it.id) + } + } + }.stateIn( + scope = CoroutineScope(Dispatchers.IO), + started = SharingStarted.Eagerly, + initialValue = ResellApiResponse.Pending + ) private var recentBitmaps: List? = null @@ -36,16 +50,13 @@ class ResellPostRepository @Inject constructor( * Asynchronously fetches the list of posts from the API. Once finished, will send down * `allPostsFlow` to be observed. */ - fun fetchPosts(page: Int = 1) { + fun fetchPosts() { _allPostsFlow.value = ResellApiResponse.Pending CoroutineScope(Dispatchers.IO).launch { try { _allPostsFlow.value = ResellApiResponse.Success( - retrofitInstance.postsApi.getPosts( - page = page - ) - .posts + retrofitInstance.postsApi.getPosts().posts .sortedByDescending { it.createdDate }) @@ -56,14 +67,6 @@ class ResellPostRepository @Inject constructor( } } - suspend fun getPostsByPage(page: Int): List { - return retrofitInstance.postsApi.getPosts(page = page).posts - } - - suspend fun getPostByFilter(category: String): List { - return retrofitInstance.postsApi.getFilteredPosts(CategoryRequest(listOf(category))).posts - } - suspend fun uploadPost( title: String, description: String, @@ -124,15 +127,15 @@ class ResellPostRepository @Inject constructor( * Sometime after calling, [savedPosts] will be updated. */ fun fetchSavedPosts() { - _savedPosts.value = ResellApiResponse.Pending + _savedPostsIds.value = ResellApiResponse.Pending CoroutineScope(Dispatchers.IO).launch { try { val saved = retrofitInstance.postsApi.getSavedPosts().posts - _savedPosts.value = ResellApiResponse.Success(saved) + _savedPostsIds.value = ResellApiResponse.Success(saved.map { it.id }) } catch (e: Exception) { Log.e("ResellPostRepository", "Error fetching saved posts: ", e) - _savedPosts.value = ResellApiResponse.Error + _savedPostsIds.value = ResellApiResponse.Error } } } @@ -144,4 +147,4 @@ class ResellPostRepository @Inject constructor( suspend fun getPostById(id: String): Post { return retrofitInstance.postsApi.getPost(id) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt index d7379862..079c1a3c 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt @@ -10,15 +10,11 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.resell.android.model.classes.Listing import com.cornellappdev.resell.android.ui.theme.Padding -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map @Composable @@ -30,30 +26,12 @@ fun ResellListingsScroll( paddedTop: Dp = 0.dp, emptyState: @Composable () -> Unit = { }, header: @Composable () -> Unit = {}, - onScrollBottom: () -> Unit = {}, - footer: @Composable () -> Unit = {}, ) { if (listings.isEmpty()) { emptyState() return } - // Check if last visible item is the last in the list - LaunchedEffect(listState) { - snapshotFlow { listState.layoutInfo } - .map { layoutInfo -> - // -1 to account for the header and footer - val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index?.minus(2) - lastVisibleItemIndex == listings.lastIndex - } - .distinctUntilChanged() - .collect { isAtBottom -> - if (isAtBottom) { - onScrollBottom() - } - } - } - LazyVerticalStaggeredGrid( state = listState, columns = StaggeredGridCells.Fixed(2), @@ -78,9 +56,5 @@ fun ResellListingsScroll( onListingPressed(item) } } - - item(span = StaggeredGridItemSpan.FullLine) { - footer() - } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt index 242a7a9a..679a78be 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt @@ -1,6 +1,5 @@ package com.cornellappdev.resell.android.ui.screens.main -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -11,14 +10,12 @@ import androidx.compose.foundation.layout.WindowInsets 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.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,7 +33,6 @@ import com.cornellappdev.resell.android.ui.components.global.ResellLoadingListin import com.cornellappdev.resell.android.ui.components.global.ResellTag import com.cornellappdev.resell.android.ui.theme.Padding import com.cornellappdev.resell.android.ui.theme.Primary -import com.cornellappdev.resell.android.ui.theme.ResellPurple import com.cornellappdev.resell.android.ui.theme.Style import com.cornellappdev.resell.android.util.clickableNoIndication import com.cornellappdev.resell.android.util.defaultHorizontalPadding @@ -53,8 +49,7 @@ fun HomeScreen( Column( modifier = Modifier - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally + .fillMaxSize() ) { HomeHeader( activeFilter = homeUiState.activeFilter, @@ -70,32 +65,11 @@ fun HomeScreen( when (homeUiState.loadedState) { is ResellApiState.Success -> { ResellListingsScroll( - listings = homeUiState.listings, + listings = homeUiState.filteredListings, onListingPressed = { homeViewModel.onListingPressed(it) }, listState = listState, - onScrollBottom = { - homeViewModel.onHitBottom() - }, - footer = { - AnimatedVisibility( - visible = homeUiState.bottomLoading, - modifier = Modifier.padding(bottom = 50.dp) - ) { - Row( - horizontalArrangement = Arrangement.Center - ) { - CircularProgressIndicator( - strokeWidth = 2.dp, - color = ResellPurple, - modifier = Modifier - .padding(16.dp) - .size(40.dp) - ) - } - } - } ) } @@ -105,8 +79,6 @@ fun HomeScreen( is ResellApiState.Error -> {} } - - } } @@ -173,4 +145,4 @@ private fun HomeHeader( Spacer(modifier = Modifier.height(Padding.medium)) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt index ce9e825e..ab54278a 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt @@ -1,15 +1,14 @@ package com.cornellappdev.resell.android.viewmodel.main -import android.util.Log -import androidx.lifecycle.viewModelScope import com.cornellappdev.resell.android.model.classes.Listing +import com.cornellappdev.resell.android.model.classes.ResellApiResponse import com.cornellappdev.resell.android.model.classes.ResellApiState +import com.cornellappdev.resell.android.model.classes.toResellApiState import com.cornellappdev.resell.android.model.posts.ResellPostRepository import com.cornellappdev.resell.android.ui.screens.root.ResellRootRoute import com.cornellappdev.resell.android.viewmodel.ResellViewModel import com.cornellappdev.resell.android.viewmodel.navigation.RootNavigationRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -21,22 +20,44 @@ class HomeViewModel @Inject constructor( initialUiState = HomeUiState( listings = listOf(), activeFilter = HomeFilter.RECENT, - loadedState = ResellApiState.Loading, - page = 1, - bottomLoading = false + loadedState = ResellApiState.Loading ) ) { data class HomeUiState( val loadedState: ResellApiState, - val listings: List, + private val listings: List, val activeFilter: HomeFilter, - val page: Int, - val bottomLoading: Boolean - ) + ) { + // TODO This should change to an endpoint, but backend is simple. + val filteredListings: List+ get() = listings.filter { + activeFilter == HomeFilter.RECENT || + it.categories.map { it.lowercase() }.any { + it.contains(activeFilter.name.lowercase()) + } + } + } init { - onRecentPressed() + asyncCollect(resellPostRepository.allPostsFlow) { response -> + val posts = when (response) { + is ResellApiResponse.Success -> { + response.data + } + + else -> { + listOf() + } + } + + applyMutation { + copy( + listings = posts.map { it.toListing() }, + loadedState = response.toResellApiState() + ) + } + } } enum class HomeFilter { @@ -60,92 +81,13 @@ class HomeViewModel @Inject constructor( ) } - private fun onRecentPressed() { - applyMutation { - copy( - activeFilter = HomeFilter.RECENT, - page = 1, - loadedState = ResellApiState.Loading - ) - } - viewModelScope.launch { - try { - val posts = resellPostRepository.getPostsByPage(1) - applyMutation { - copy( - listings = posts.map { it.toListing() }, - loadedState = ResellApiState.Success - ) - } - } catch (e: Exception) { - Log.e("HomeViewModel", "Error fetching posts: ", e) - applyMutation { - copy( - loadedState = ResellApiState.Error - ) - } - } - } - } - fun onToggleFilter(filter: HomeFilter) { - if (filter == HomeFilter.RECENT) { - onRecentPressed() - return - } - applyMutation { - copy( - activeFilter = filter, - loadedState = ResellApiState.Loading, - ) - } - viewModelScope.launch { - try { - val posts = resellPostRepository.getPostByFilter(filter.name) - applyMutation { - copy( - listings = posts.map { it.toListing() }, - loadedState = ResellApiState.Success - ) - } - } catch (e: Exception) { - Log.e("HomeViewModel", "Error fetching posts: ", e) - applyMutation { - copy( - loadedState = ResellApiState.Error - ) - } - } + copy(activeFilter = filter) } } fun onSearchPressed() { rootNavigationRepository.navigate(ResellRootRoute.SEARCH) } - - fun onHitBottom() { - if (stateValue().bottomLoading || stateValue().activeFilter != HomeFilter.RECENT) { - return - } - - viewModelScope.launch { - applyMutation { - copy( - page = page + 1, - bottomLoading = true - ) - } - - val newPage = resellPostRepository.getPostsByPage(stateValue().page).map { - it.toListing() - } - applyMutation { - copy( - listings = stateValue().listings + newPage, - bottomLoading = false - ) - } - } - } -} +} \ No newline at end of file From 1954b083a12d5471cd8b95d7af744122a0a1f38a Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Tue, 15 Apr 2025 21:54:32 -0400 Subject: [PATCH 09/11] Just add one space to some files --- .../cornellappdev/resell/android/model/api/PostsApiService.kt | 2 +- .../resell/android/model/posts/ResellPostRepository.kt | 2 +- .../resell/android/ui/components/global/ResellListingScroll.kt | 2 +- .../cornellappdev/resell/android/ui/screens/main/HomeScreen.kt | 2 +- .../resell/android/viewmodel/main/HomeViewModel.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt index 06d0666b..1220037f 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/api/PostsApiService.kt @@ -132,4 +132,4 @@ data class NewPostBody( data class IsSavedResponse( val isSaved: Boolean -) \ No newline at end of file +) diff --git a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt index 704c35da..8330f767 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/model/posts/ResellPostRepository.kt @@ -147,4 +147,4 @@ class ResellPostRepository @Inject constructor( suspend fun getPostById(id: String): Post { return retrofitInstance.postsApi.getPost(id) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt index 079c1a3c..2c06e8e4 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/components/global/ResellListingScroll.kt @@ -57,4 +57,4 @@ fun ResellListingsScroll( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt index 679a78be..09df2e89 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/HomeScreen.kt @@ -145,4 +145,4 @@ private fun HomeHeader( Spacer(modifier = Modifier.height(Padding.medium)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt index ab54278a..283f3dbc 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/HomeViewModel.kt @@ -90,4 +90,4 @@ class HomeViewModel @Inject constructor( fun onSearchPressed() { rootNavigationRepository.navigate(ResellRootRoute.SEARCH) } -} \ No newline at end of file +} From cf9a909279dc45703747a38e44e37673529b84cb Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Wed, 16 Apr 2025 01:48:37 -0400 Subject: [PATCH 10/11] Clean up --- .../android/ui/screens/pdp/PostDetailPage.kt | 91 +++++++++---------- 1 file changed, 42 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt index c6ae9dad..f0fa7726 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt @@ -20,16 +20,13 @@ 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.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -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.draw.BlurredEdgeTreatment.Companion.Rectangle @@ -39,10 +36,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -64,7 +58,6 @@ import com.cornellappdev.resell.android.util.clickableNoIndication import com.cornellappdev.resell.android.util.defaultHorizontalPadding import com.cornellappdev.resell.android.viewmodel.pdp.PostDetailViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostDetailPage( postDetailViewModel: PostDetailViewModel = hiltViewModel(), @@ -137,37 +130,66 @@ private fun Content( onUserClick: () -> Unit = {}, showContact: Boolean = false, ) { - var sheetHeightFromBottom by remember { mutableStateOf(0.dp) } val pagerState = rememberPagerState(pageCount = { images.size }) // Derive peekHeight as screen height minus image height: val screenHeight = LocalConfiguration.current.screenHeightDp.dp // TODO the plus at the end seems wrong. Test on other devices. - val peekHeight = screenHeight - imageHeight + 96.dp + val peekHeight = screenHeight - imageHeight + 180.dp Box( modifier = Modifier.fillMaxWidth() ) { BottomSheetScaffold( + sheetContainerColor = Color.Transparent, + sheetShadowElevation = 0.dp, + sheetDragHandle = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + ) { + WhichPage( + pagerState = pagerState, + modifier = Modifier + .align(Alignment.BottomCenter) + ) + + BookmarkFAB( + selected = bookmarked, + onClick = onBookmarkClick, + modifier = Modifier + .align(Alignment.BottomStart) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .background(color = Color.White), + horizontalArrangement = Arrangement.Center, + ) { + BottomSheetDefaults.DragHandle() + } + } + }, sheetContent = { BottomSheetContent( - profilePictureUrl = userPfp, - username = username, title = title, price = price, description = description, - onHeightChanged = { - sheetHeightFromBottom = it - }, - onSimilarClick = onSimilarClick, + profilePictureUrl = userPfp, + username = username, similarImageUrls = similarImageUrls, + onSimilarClick = onSimilarClick, onUserClick = onUserClick ) }, sheetPeekHeight = peekHeight, - sheetContainerColor = Color.White, - sheetShadowElevation = 12.dp, containerColor = Color.White, modifier = Modifier .fillMaxSize() @@ -177,7 +199,7 @@ private fun Content( state = pagerState, modifier = Modifier .fillMaxSize() - .background(IconInactive), + .background(IconInactive) ) { Column(modifier = Modifier.fillMaxHeight()) { PdpImageBlurredBackground( @@ -213,22 +235,6 @@ private fun Content( state = contactButtonState ) } - - WhichPage( - pagerState = pagerState, - modifier = Modifier - .padding(bottom = sheetHeightFromBottom) - .align(Alignment.BottomCenter) - ) - - BookmarkFAB( - selected = bookmarked, - onClick = onBookmarkClick, - modifier = Modifier - .align(Alignment.BottomStart) - .defaultHorizontalPadding() - .padding(bottom = sheetHeightFromBottom) - ) } } @@ -303,14 +309,12 @@ private fun BottomSheetContent( username: String, paddingTop: Dp = 116.dp, similarImageUrls: ResellApiResponse>, - onHeightChanged: (Dp) -> Unit, onSimilarClick: (Int) -> Unit, onUserClick: () -> Unit, ) { // Get screen height val screenHeight = LocalConfiguration.current.screenHeightDp.dp - val density = LocalDensity.current // Calculate maximum height for the sheet content based on padding from top val maxSheetHeight = screenHeight - paddingTop @@ -324,18 +328,7 @@ private fun BottomSheetContent( Row( modifier = Modifier .fillMaxWidth() - .defaultHorizontalPadding() - .onGloballyPositioned { layoutCoordinates -> - val textPosition = layoutCoordinates.positionInRoot().y - val screenHeightPx = with(density) { screenHeight.toPx() } - - // Calculate distance from bottom in px and convert to dp - val distanceFromBottomPx = screenHeightPx - textPosition - val textDistanceFromBottom = with(density) { distanceFromBottomPx.toDp() } - - // Tell the parent that the height has changed. - onHeightChanged(textDistanceFromBottom + 120.dp) - }, + .defaultHorizontalPadding(), verticalAlignment = Alignment.CenterVertically ) { Text( From 8bd0da434d4d18da3a564e9b9fc0f81cfff887a0 Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Wed, 16 Apr 2025 02:00:42 -0400 Subject: [PATCH 11/11] Disable gestures on the bookmark row --- .../android/ui/screens/pdp/PostDetailPage.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt index f0fa7726..7b24736b 100644 --- a/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt +++ b/app/src/main/java/com/cornellappdev/resell/android/ui/screens/pdp/PostDetailPage.kt @@ -35,6 +35,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource @@ -152,6 +155,7 @@ private fun Content( Box( modifier = Modifier .fillMaxWidth() + .gesturesDisabled() ) { WhichPage( pagerState = pagerState, @@ -238,6 +242,18 @@ private fun Content( } } +fun Modifier.gesturesDisabled() = + pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .forEach(PointerInputChange::consume) + } + } + } + + @Composable private fun PdpImageBlurredBackground( imageHeight: Dp,