diff --git a/app/src/main/java/com/cornellappdev/transit/models/Place.kt b/app/src/main/java/com/cornellappdev/transit/models/Place.kt index 58ebdb2..5d17271 100644 --- a/app/src/main/java/com/cornellappdev/transit/models/Place.kt +++ b/app/src/main/java/com/cornellappdev/transit/models/Place.kt @@ -1,7 +1,6 @@ package com.cornellappdev.transit.models import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import kotlinx.serialization.Serializable @@ -22,7 +21,19 @@ enum class PlaceType { BUS_STOP, @Json(name = "applePlace") - APPLE_PLACE + APPLE_PLACE, + + @Json(name = "eatery") + EATERY, + + @Json(name = "library") + LIBRARY, + + @Json(name = "gym") + GYM, + + @Json(name = "printer") + PRINTER } /** @@ -36,6 +47,7 @@ data class Place( @Json(name = "detail") val detail: String?, @Json(name = "type") var type: PlaceType ) { + //TODO: sublabel for bus stop should be the current distance away val subLabel - get() = if (type == PlaceType.BUS_STOP) "Bus Stop" else detail.toString() + get() = if (type == PlaceType.BUS_STOP) "Bus Stop" else detail.orEmpty() } diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt index c83c4ae..691dd1b 100644 --- a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/Eatery.kt @@ -77,7 +77,7 @@ data class Eatery( longitude = this.longitude ?: 0.0, name = this.name, detail = this.location, - type = PlaceType.APPLE_PLACE + type = PlaceType.EATERY ) } diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/StaticPlaces.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/StaticPlaces.kt index 0600cd1..0a26fc2 100644 --- a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/StaticPlaces.kt +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/StaticPlaces.kt @@ -30,7 +30,7 @@ data class Printer( longitude = this.longitude, name = this.location, detail = this.description, - type = PlaceType.APPLE_PLACE + type = PlaceType.PRINTER ) } @@ -55,6 +55,6 @@ data class Library( longitude = this.longitude, name = this.location, detail = this.address, - type = PlaceType.APPLE_PLACE + type = PlaceType.LIBRARY ) } diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/UpliftGym.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/UpliftGym.kt index e45aa9c..ea75fae 100644 --- a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/UpliftGym.kt +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/UpliftGym.kt @@ -70,7 +70,7 @@ data class UpliftGym( longitude = this.longitude, name = this.name, detail = getGymLocationString(this.name), - type = PlaceType.APPLE_PLACE + type = PlaceType.GYM ) } diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/MenuItem.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/MenuItem.kt index cd463f5..ab5d493 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/MenuItem.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/MenuItem.kt @@ -36,6 +36,7 @@ fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Un .clickable(onClick = onClick), verticalAlignment = Alignment.CenterVertically ) { + //TODO: Add icons for each ecosystem type if (type == PlaceType.APPLE_PLACE) { Image( painterResource(R.drawable.location_pin_gray), @@ -69,8 +70,14 @@ fun MenuItem(type: PlaceType, label: String, sublabel: String, onClick: () -> Un } -@Preview +@Preview(showBackground = true) @Composable -fun PreviewMenuItem() { +private fun PreviewMenuItemBusStop() { MenuItem(PlaceType.BUS_STOP, "Ithaca Commons", "Ithaca, NY", {}) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewMenuItemApplePlace() { + MenuItem(PlaceType.APPLE_PLACE, "Apple Place", "Ithaca, NY", {}) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/BottomSheetLocationCard.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/BottomSheetLocationCard.kt index ba663b6..ff5cccc 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/BottomSheetLocationCard.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/BottomSheetLocationCard.kt @@ -2,20 +2,22 @@ package com.cornellappdev.transit.ui.components.home import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.PlatformTextStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.ui.theme.PrimaryText +import com.cornellappdev.transit.ui.theme.SecondaryText import com.cornellappdev.transit.ui.theme.Style /** @@ -25,41 +27,45 @@ import com.cornellappdev.transit.ui.theme.Style fun BottomSheetLocationCard( title: String, subtitle1: String, - subtitle2: String = "", + isFavorite: Boolean, + onFavoriteClick: () -> Unit, onClick: () -> Unit ) { Column( modifier = Modifier - .clickable { - onClick() - } + .fillMaxWidth() + .background(Color.White, shape = RoundedCornerShape(12.dp)) + .clickable(onClick = onClick) ) { - Column( + Box( modifier = Modifier .fillMaxWidth() - .height(90.dp) - .background(color = Color.White, shape = RoundedCornerShape(12.dp)) .padding(16.dp) ) { - Text( - text = title, - style = Style.cardH1, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = subtitle1, - style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = subtitle2, - style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Column( + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically) + ) { + Text( + text = title, + style = Style.cardH1, + color = PrimaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 32.dp) + ) + Text( + text = subtitle1, + style = Style.heading3, + color = SecondaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 32.dp) + ) + } + + FavoritesStar(onFavoriteClick = onFavoriteClick, isFavorite = isFavorite) } + } } @@ -68,7 +74,8 @@ fun BottomSheetLocationCard( private fun PreviewBottomSheetLocationCard() { BottomSheetLocationCard( title = "Uris Hall", - subtitle1 = "Cornell University", - subtitle2 = "Open until 10:00 PM" + subtitle1 = "Bus Stop", + isFavorite = true, + onFavoriteClick = {} ) { } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt index 9f7bb90..6df22ea 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt @@ -7,12 +7,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomEnd -import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt index 5a443f5..f8564c4 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.transit.R import com.cornellappdev.transit.models.ecosystem.Eatery import com.cornellappdev.transit.ui.theme.DividerGray diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt index 7f1277b..7c636d3 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EcosystemBottomSheetContent.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString @@ -27,19 +28,24 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.cornellappdev.transit.R import com.cornellappdev.transit.models.Place +import com.cornellappdev.transit.models.PlaceType import com.cornellappdev.transit.models.ecosystem.DayOperatingHours import com.cornellappdev.transit.models.ecosystem.DetailedEcosystemPlace import com.cornellappdev.transit.models.ecosystem.Eatery +import com.cornellappdev.transit.models.ecosystem.Library +import com.cornellappdev.transit.models.ecosystem.Printer import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.models.ecosystem.UpliftCapacity import com.cornellappdev.transit.models.ecosystem.UpliftGym import com.cornellappdev.transit.networking.ApiResponse import com.cornellappdev.transit.ui.theme.FavoritesDividerGray import com.cornellappdev.transit.ui.theme.robotoFamily +import com.cornellappdev.transit.ui.viewmodels.EcosystemFavoritesUiState import com.cornellappdev.transit.ui.viewmodels.FavoritesFilterSheetState import com.cornellappdev.transit.ui.viewmodels.FilterState import com.cornellappdev.transit.util.TimeUtils.isOpenAnnotatedStringFromOperatingHours import com.cornellappdev.transit.util.ecosystem.capacityPercentAnnotatedString +import com.cornellappdev.transit.ui.viewmodels.PrinterCardUiState import kotlin.collections.isNotEmpty import com.cornellappdev.transit.util.getGymLocationString @@ -60,6 +66,7 @@ fun EcosystemBottomSheetContent( onFilterClick: (FilterState) -> Unit, staticPlaces: StaticPlaces, favorites: Set, + favoritesUiState: EcosystemFavoritesUiState, modifier: Modifier = Modifier, navigateToPlace: (Place) -> Unit, onDetailsClick: (DetailedEcosystemPlace) -> Unit, @@ -113,6 +120,7 @@ fun EcosystemBottomSheetContent( currentFilter = activeFilter, staticPlaces = staticPlaces, favorites = favorites, + favoritesUiState = favoritesUiState, navigateToPlace = navigateToPlace, onDetailsClick = onDetailsClick, onFavoriteStarClick = onFavoriteStarClick, @@ -146,6 +154,7 @@ private fun BottomSheetFilteredContent( currentFilter: FilterState, staticPlaces: StaticPlaces, favorites: Set, + favoritesUiState: EcosystemFavoritesUiState, navigateToPlace: (Place) -> Unit, onDetailsClick: (DetailedEcosystemPlace) -> Unit, onFavoriteStarClick: (Place) -> Unit, @@ -174,60 +183,79 @@ private fun BottomSheetFilteredContent( } } val isFilterBarHidden = currentFilter == FilterState.FAVORITES && appliedFilters.isEmpty() - LazyColumn( - contentPadding = PaddingValues( - start = 12.dp, - end = 12.dp, - top = if (isFilterBarHidden) 0.dp else 8.dp, - bottom = 120.dp // Makes bottom content visible with padding at the end - ), - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - when (currentFilter) { - FilterState.FAVORITES -> { - favoriteList( - favorites, - navigateToPlace, - onAddFavoritesClick - ) - } + key(currentFilter, appliedFilters) { + LazyColumn( + contentPadding = PaddingValues( + start = 12.dp, + end = 12.dp, + top = if (isFilterBarHidden) 0.dp else 8.dp, + bottom = 120.dp // Makes bottom content visible with padding at the end + ), + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + when (currentFilter) { + FilterState.FAVORITES -> { + favoriteList( + favorites = favorites, + filteredFavorites = favoritesUiState.filteredSortedFavorites, + eateryByPlace = favoritesUiState.eateryByPlace, + libraryByPlace = favoritesUiState.libraryByPlace, + gymByPlace = favoritesUiState.gymByPlace, + printerByPlace = favoritesUiState.printerByPlace, + navigateToPlace = navigateToPlace, + onAddFavoritesClick = onAddFavoritesClick, + onFavoriteStarClick = onFavoriteStarClick, + onDetailsClick = onDetailsClick, + operatingHoursToString = operatingHoursToString, + capacityToString = ::capacityPercentAnnotatedString, + distanceStringToPlace = distanceStringToPlace + ) + } - FilterState.PRINTERS -> { - printerList(staticPlaces, navigateToPlace, onFavoriteStarClick, favorites) - } + FilterState.PRINTERS -> { + printerList( + staticPlaces = staticPlaces, + navigateToPlace = navigateToPlace, + favorites = favorites, + onFavoriteStarClick = onFavoriteStarClick, + distanceStringToPlace = distanceStringToPlace + ) + } - FilterState.GYMS -> { - gymList( - gymsApiResponse = staticPlaces.gyms, - onDetailsClick = onDetailsClick, - favorites = favorites, - onFavoriteStarClick = onFavoriteStarClick, - operatingHoursToString = ::isOpenAnnotatedStringFromOperatingHours, - capacityToString = ::capacityPercentAnnotatedString, - distanceStringToPlace = distanceStringToPlace - ) - } + FilterState.GYMS -> { + gymList( + gymsApiResponse = staticPlaces.gyms, + onDetailsClick = onDetailsClick, + favorites = favorites, + onFavoriteStarClick = onFavoriteStarClick, + operatingHoursToString = ::isOpenAnnotatedStringFromOperatingHours, + capacityToString = ::capacityPercentAnnotatedString, + distanceStringToPlace = distanceStringToPlace + ) + } - FilterState.EATERIES -> { - eateryList( - eateriesApiResponse = staticPlaces.eateries, - onDetailsClick = onDetailsClick, - favorites = favorites, - onFavoriteStarClick = onFavoriteStarClick, - operatingHoursToString = operatingHoursToString, - distanceStringToPlace = distanceStringToPlace - ) - } + FilterState.EATERIES -> { + eateryList( + eateriesApiResponse = staticPlaces.eateries, + onDetailsClick = onDetailsClick, + favorites = favorites, + onFavoriteStarClick = onFavoriteStarClick, + operatingHoursToString = operatingHoursToString, + distanceStringToPlace = distanceStringToPlace + ) + } - FilterState.LIBRARIES -> { - libraryList( - staticPlaces, - navigateToPlace, - onDetailsClick, - favorites, - onFavoriteStarClick, - ) + FilterState.LIBRARIES -> { + libraryList( + staticPlaces, + navigateToPlace, + onDetailsClick, + favorites, + onFavoriteStarClick, + distanceStringToPlace, + ) + } } } } @@ -239,19 +267,151 @@ private fun BottomSheetFilteredContent( */ private fun LazyListScope.favoriteList( favorites: Set, + filteredFavorites: List, + eateryByPlace: Map, + libraryByPlace: Map, + gymByPlace: Map, + printerByPlace: Map, navigateToPlace: (Place) -> Unit, - onAddFavoritesClick: () -> Unit + onAddFavoritesClick: () -> Unit, + onFavoriteStarClick: (Place) -> Unit, + onDetailsClick: (DetailedEcosystemPlace) -> Unit, + operatingHoursToString: (List) -> AnnotatedString, + capacityToString: (UpliftCapacity?) -> AnnotatedString, + distanceStringToPlace: (Double?, Double?) -> String ) { item { Spacer(modifier = Modifier.height(8.dp)) AddFavoritesButton(onAddFavoritesClick = onAddFavoritesClick) } - items(favorites.toList()) { - BottomSheetLocationCard( - title = it.name, - subtitle1 = it.subLabel - ) { - //TODO: Eatery + + items( + items = filteredFavorites, + key = { place -> "${place.type}:${place.name}:${place.latitude}:${place.longitude}" } + ) { place -> + when (place.type) { + PlaceType.EATERY -> { + val matchingEatery = eateryByPlace[place] + if (matchingEatery != null) { + RoundedImagePlaceCard( + title = matchingEatery.name, + subtitle = (matchingEatery.location + ?: "") + distanceStringToPlace( + matchingEatery.latitude, + matchingEatery.longitude + ), + isFavorite = true, + onFavoriteClick = { onFavoriteStarClick(place) }, + leftAnnotatedString = operatingHoursToString( + matchingEatery.operatingHours() + ) + ) { + onDetailsClick(matchingEatery) + } + } else { + StandardCard( + place = place, + onFavoriteStarClick = onFavoriteStarClick, + navigateToPlace = navigateToPlace, + distanceStringToPlace = distanceStringToPlace + ) + } + } + + PlaceType.LIBRARY -> { + val matchingLibrary = libraryByPlace[place] + if (matchingLibrary != null) { + RoundedImagePlaceCard( + title = matchingLibrary.location, + subtitle = matchingLibrary.address + distanceStringToPlace( + matchingLibrary.latitude, + matchingLibrary.longitude + ), + isFavorite = true, + onFavoriteClick = { onFavoriteStarClick(place) } + ) { + onDetailsClick(matchingLibrary) + } + } else { + StandardCard( + place = place, + onFavoriteStarClick = onFavoriteStarClick, + navigateToPlace = navigateToPlace, + distanceStringToPlace = distanceStringToPlace + ) + } + } + + PlaceType.GYM -> { + val matchingGym = gymByPlace[place] + if (matchingGym != null) { + RoundedImagePlaceCard( + title = matchingGym.name, + subtitle = getGymLocationString(matchingGym.name) + distanceStringToPlace( + matchingGym.latitude, + matchingGym.longitude + ), + isFavorite = true, + onFavoriteClick = { + onFavoriteStarClick(place) + }, + leftAnnotatedString = operatingHoursToString( + matchingGym.operatingHours() + ), + rightAnnotatedString = capacityToString( + matchingGym.upliftCapacity + ), + ) { + onDetailsClick(matchingGym) + } + } else { + StandardCard( + place = place, + onFavoriteStarClick = onFavoriteStarClick, + navigateToPlace = navigateToPlace, + distanceStringToPlace = distanceStringToPlace + ) + } + } + + PlaceType.PRINTER -> { + val matchingPrinter = printerByPlace[place] + if (matchingPrinter != null) { + PrinterCard( + title = matchingPrinter.title, + subtitle = matchingPrinter.subtitle + distanceStringToPlace( + place.latitude, + place.longitude + ), + inColor = matchingPrinter.inColor, + hasCopy = matchingPrinter.hasCopy, + hasScan = matchingPrinter.hasScan, + alertMessage = matchingPrinter.alertMessage, + isFavorite = place in favorites, + onFavoriteClick = { + onFavoriteStarClick(place) + } + ) { + navigateToPlace(place) + } + } else { + StandardCard( + place = place, + onFavoriteStarClick = onFavoriteStarClick, + navigateToPlace = navigateToPlace, + distanceStringToPlace = distanceStringToPlace + ) + } + } + + PlaceType.BUS_STOP, PlaceType.APPLE_PLACE -> { + StandardCard( + place = place, + onFavoriteStarClick = onFavoriteStarClick, + navigateToPlace = navigateToPlace, + distanceStringToPlace = distanceStringToPlace + ) + } } } } @@ -302,7 +462,6 @@ private fun LazyListScope.gymList( onDetailsClick(it) } } - } } } @@ -313,8 +472,9 @@ private fun LazyListScope.gymList( private fun LazyListScope.printerList( staticPlaces: StaticPlaces, navigateToPlace: (Place) -> Unit, - onFavoriteStarClick: (Place) -> Unit, favorites: Set, + onFavoriteStarClick: (Place) -> Unit, + distanceStringToPlace: (Double?, Double?) -> String, ) { when (staticPlaces.printers) { is ApiResponse.Error -> { @@ -334,7 +494,10 @@ private fun LazyListScope.printerList( PrinterCard( title = it.location.substringBefore("*").trim(), - subtitle = it.description.substringAfter("-").trim(), + subtitle = it.description.substringAfter("-").trim() + distanceStringToPlace( + it.latitude, + it.longitude + ), inColor = it.description.contains("Color", ignoreCase = true), hasCopy = it.description.contains("Copy", ignoreCase = true), hasScan = it.description.contains("Scan", ignoreCase = true), @@ -405,7 +568,8 @@ private fun LazyListScope.libraryList( navigateToPlace: (Place) -> Unit, navigateToDetails: (DetailedEcosystemPlace) -> Unit, favorites: Set, - onFavoriteStarClick: (Place) -> Unit + onFavoriteStarClick: (Place) -> Unit, + distanceStringToPlace: (Double?, Double?) -> String, ) { when (staticPlaces.libraries) { is ApiResponse.Error -> { @@ -419,7 +583,7 @@ private fun LazyListScope.libraryList( RoundedImagePlaceCard( placeholderRes = R.drawable.olin_library, title = it.location, - subtitle = it.address, + subtitle = it.address + distanceStringToPlace(it.latitude, it.longitude), isFavorite = it.toPlace() in favorites, onFavoriteClick = { onFavoriteStarClick(it.toPlace()) @@ -432,6 +596,26 @@ private fun LazyListScope.libraryList( } } +@Composable +private fun StandardCard( + place: Place, + onFavoriteStarClick: (Place) -> Unit, + navigateToPlace: (Place) -> Unit, + distanceStringToPlace: (Double?, Double?) -> String, +) { + val distance = distanceStringToPlace(place.latitude, place.longitude) + val subtitle = if (distance.isBlank()) place.subLabel else "${place.subLabel}$distance" + + BottomSheetLocationCard( + title = place.name, + subtitle1 = subtitle, + isFavorite = true, + onFavoriteClick = { onFavoriteStarClick(place) } + ) { + navigateToPlace(place) + } +} + @Preview(showBackground = true) @Composable private fun PreviewEcosystemBottomSheet() { @@ -452,6 +636,7 @@ private fun PreviewEcosystemBottomSheet() { ApiResponse.Pending ), favorites = emptySet(), + favoritesUiState = EcosystemFavoritesUiState(), modifier = Modifier, navigateToPlace = {}, onDetailsClick = {}, @@ -475,4 +660,168 @@ private fun PreviewEcosystemBottomSheet() { operatingHoursToString = { _ -> AnnotatedString("") }, distanceStringToPlace = { _, _ -> "" } ) -} \ No newline at end of file +} + +@Preview(showBackground = true, name = "Favorites with Applied Filters") +@Composable +private fun PreviewBottomSheetFilteredContentFavorites() { + val mockEatery = Eatery( + id = 1, + name = "Trillium", + menuSummary = "Coffee, pastries, sandwiches", + imageUrl = null, + location = "Kennedy Hall", + campusArea = "Central Campus", + onlineOrderUrl = null, + latitude = 42.4488, + longitude = -76.4813, + paymentAcceptsMealSwipes = true, + paymentAcceptsBrbs = true, + paymentAcceptsCash = true, + events = null + ) + + val mockLibrary = Library( + id = 1, + location = "Olin Library", + address = "161 Ho Plaza", + latitude = 42.4534, + longitude = -76.4735 + ) + + val mockGym = UpliftGym( + name = "Noyes Community Recreation Center", + id = "noyes-rec", + facilityId = "1", + hours = listOf(null, null, null, null, null, null, null), + imageUrl = null, + upliftCapacity = null, + latitude = 42.4480, + longitude = -76.4840 + ) + + val mockPrinter = Printer( + id = 1, + location = "Mann Library", + description = "1st Floor, near entrance", + latitude = 42.4479, + longitude = -76.4764 + ) + + BottomSheetFilteredContent( + currentFilter = FilterState.FAVORITES, + staticPlaces = StaticPlaces( + printers = ApiResponse.Success(listOf(mockPrinter)), + libraries = ApiResponse.Success(listOf(mockLibrary)), + eateries = ApiResponse.Success(listOf(mockEatery)), + gyms = ApiResponse.Success(listOf(mockGym)) + ), + favorites = setOf( + Place( + latitude = 42.4488, + longitude = -76.4813, + name = "Trillium", + detail = "Kennedy Hall", + type = PlaceType.EATERY + ), + Place( + latitude = 42.4534, + longitude = -76.4735, + name = "Olin Library", + detail = "161 Ho Plaza", + type = PlaceType.LIBRARY + ), + Place( + latitude = 42.4480, + longitude = -76.4840, + name = "Noyes Community Recreation Center", + detail = "North Campus", + type = PlaceType.GYM + ), + Place( + latitude = 42.4479, + longitude = -76.4764, + name = "Mann Library", + detail = "1st Floor, near entrance", + type = PlaceType.PRINTER + ), + Place( + latitude = 42.4440, + longitude = -76.4825, + name = "Seneca St & Fall Creek Dr", + detail = "Bus Stop", + type = PlaceType.BUS_STOP + ) + ), + favoritesUiState = EcosystemFavoritesUiState( + filteredSortedFavorites = listOf( + Place( + latitude = 42.4488, + longitude = -76.4813, + name = "Trillium", + detail = "Kennedy Hall", + type = PlaceType.EATERY + ), + Place( + latitude = 42.4534, + longitude = -76.4735, + name = "Olin Library", + detail = "161 Ho Plaza", + type = PlaceType.LIBRARY + ), + Place( + latitude = 42.4480, + longitude = -76.4840, + name = "Noyes Community Recreation Center", + detail = "North Campus", + type = PlaceType.GYM + ), + Place( + latitude = 42.4479, + longitude = -76.4764, + name = "Mann Library", + detail = "1st Floor, near entrance", + type = PlaceType.PRINTER + ), + Place( + latitude = 42.4440, + longitude = -76.4825, + name = "Seneca St & Fall Creek Dr", + detail = "Bus Stop", + type = PlaceType.BUS_STOP + ) + ), + eateryByPlace = listOf(mockEatery).associateBy { it.toPlace() }, + libraryByPlace = listOf(mockLibrary).associateBy { it.toPlace() }, + gymByPlace = listOf(mockGym).associateBy { it.toPlace() }, + printerByPlace = mapOf( + mockPrinter.toPlace() to PrinterCardUiState( + title = "Mann Library", + subtitle = "near entrance", + inColor = false, + hasCopy = false, + hasScan = false, + alertMessage = "" + ) + ) + ), + navigateToPlace = {}, + onDetailsClick = {}, + onFavoriteStarClick = {}, + onAddFavoritesClick = {}, + onFilterButtonClick = {}, + appliedFilters = setOf( + FavoritesFilterSheetState.EATERIES, + FavoritesFilterSheetState.LIBRARIES, + FavoritesFilterSheetState.GYMS, + FavoritesFilterSheetState.PRINTERS, + FavoritesFilterSheetState.OTHER + ), + onRemoveAppliedFilter = {}, + operatingHoursToString = { _ -> AnnotatedString("Open • 10am - 4pm") }, + distanceStringToPlace = { _, _ -> "distance" } + ) +} + + + diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt index 7718dc9..1f03f28 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.transit.R import com.cornellappdev.transit.models.ecosystem.UpliftGym import com.cornellappdev.transit.ui.theme.DividerGray @@ -25,7 +24,6 @@ import com.cornellappdev.transit.ui.theme.PrimaryText import com.cornellappdev.transit.ui.theme.SecondaryText import com.cornellappdev.transit.ui.theme.Style import com.cornellappdev.transit.ui.theme.TransitBlue -import com.cornellappdev.transit.ui.viewmodels.HomeViewModel import com.cornellappdev.transit.util.StringUtils.createDeepLink import com.cornellappdev.transit.util.TimeUtils.getOpenStatus import com.cornellappdev.transit.util.TimeUtils.isOpenAnnotatedStringFromOperatingHours diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt index fc657a3..f4cddb6 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt @@ -19,24 +19,30 @@ import com.cornellappdev.transit.ui.theme.MetadataGray * Rounded image from a network request, fallback to a drawable */ @Composable -fun PlaceCardImage(imageUrl: String?, @DrawableRes placeholderRes: Int, shouldClipBottom: Boolean = false) { +fun PlaceCardImage( + imageUrl: String?, + @DrawableRes placeholderRes: Int? = null, + shouldClipBottom: Boolean = false +) { val imageModifier = Modifier .then( - if(shouldClipBottom) Modifier.clip(RoundedCornerShape(12.dp)) - else Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))) + if (shouldClipBottom) Modifier.clip(RoundedCornerShape(12.dp)) + else Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + ) .fillMaxWidth() .height(112.dp) .background(MetadataGray) - if (imageUrl.isNullOrBlank()) { + // Images with no placeholders will simply not show + if (imageUrl.isNullOrBlank() && placeholderRes != null) { Image( painter = painterResource(id = placeholderRes), contentDescription = null, contentScale = ContentScale.Crop, modifier = imageModifier ) - } else { + } else if (!imageUrl.isNullOrBlank() && placeholderRes != null) { AsyncImage( model = imageUrl, contentDescription = null, diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt index d659ba7..292bb22 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/RoundedImagePlaceCard.kt @@ -38,7 +38,7 @@ fun RoundedImagePlaceCard( onFavoriteClick: () -> Unit, leftAnnotatedString: AnnotatedString? = null, rightAnnotatedString: AnnotatedString? = null, - @DrawableRes placeholderRes: Int, + @DrawableRes placeholderRes: Int? = null, onClick: () -> Unit, ) { Column( @@ -108,7 +108,7 @@ fun RoundedImagePlaceCard( @Preview @Composable -fun RoundedImagePlaceCardPreview() { +private fun RoundedImagePlaceCardPreview() { RoundedImagePlaceCard( placeholderRes = R.drawable.olin_library, title = "Olin Library", diff --git a/app/src/main/java/com/cornellappdev/transit/ui/screens/HomeScreen.kt b/app/src/main/java/com/cornellappdev/transit/ui/screens/HomeScreen.kt index bf0a1e2..f3aa71d 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/screens/HomeScreen.kt @@ -184,6 +184,8 @@ fun HomeScreen( val filterStateValue = homeViewModel.filterState.collectAsStateWithLifecycle().value val staticPlaces = homeViewModel.staticPlacesFlow.collectAsStateWithLifecycle().value + val ecosystemFavoritesUiState = + homeViewModel.ecosystemFavoritesUiState.collectAsStateWithLifecycle().value // Main search bar active/inactive var searchActive by remember { mutableStateOf(false) } @@ -335,6 +337,7 @@ fun HomeScreen( modifier = Modifier.onTapDisableSearch(), staticPlaces = staticPlaces, favorites = favorites, + favoritesUiState = ecosystemFavoritesUiState, navigateToPlace = { homeViewModel.beginRouteOptions(it) navController.navigate("route") diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/FavoritesViewModel.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/FavoritesViewModel.kt index 5503132..3977faa 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/FavoritesViewModel.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/FavoritesViewModel.kt @@ -6,8 +6,6 @@ import com.cornellappdev.transit.models.RouteRepository import com.cornellappdev.transit.models.Place import com.cornellappdev.transit.models.UserPreferenceRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt index 7073369..00ba1f6 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt @@ -5,12 +5,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.cornellappdev.transit.models.LocationRepository import com.cornellappdev.transit.models.Place +import com.cornellappdev.transit.models.PlaceType import com.cornellappdev.transit.models.RouteRepository import com.cornellappdev.transit.models.SelectedRouteRepository import com.cornellappdev.transit.models.ecosystem.StaticPlaces import com.cornellappdev.transit.models.UserPreferenceRepository +import com.cornellappdev.transit.models.ecosystem.Eatery import com.cornellappdev.transit.models.ecosystem.EateryRepository import com.cornellappdev.transit.models.ecosystem.GymRepository +import com.cornellappdev.transit.models.ecosystem.Library +import com.cornellappdev.transit.models.ecosystem.Printer +import com.cornellappdev.transit.models.ecosystem.UpliftGym import com.cornellappdev.transit.networking.ApiResponse import com.cornellappdev.transit.util.StringUtils.fromMetersToMiles import com.cornellappdev.transit.util.calculateDistance @@ -132,6 +137,39 @@ class HomeViewModel @Inject constructor( val appliedFavoritesFilters: StateFlow> = _appliedFavoritesFilters.asStateFlow() + val ecosystemFavoritesUiState: StateFlow = combine( + userPreferenceRepository.favoritesFlow, + staticPlacesFlow, + appliedFavoritesFilters + ) { favorites, staticPlaces, appliedFilters -> + val allowedTypes = appliedFilters.toAllowedPlaceTypes() + + val filteredSortedFavorites = favorites.asSequence() + .filter { allowedTypes.isEmpty() || it.type in allowedTypes } + .sortedWith(compareBy({ it.type.ordinal }, { it.name })) + .toList() + + val eateries = (staticPlaces.eateries as? ApiResponse.Success)?.data.orEmpty() + val libraries = (staticPlaces.libraries as? ApiResponse.Success)?.data.orEmpty() + val gyms = (staticPlaces.gyms as? ApiResponse.Success)?.data.orEmpty() + val printers = (staticPlaces.printers as? ApiResponse.Success)?.data.orEmpty() + + EcosystemFavoritesUiState( + filteredSortedFavorites = filteredSortedFavorites, + eateryByPlace = eateries.associateBy { it.toPlace() }, + libraryByPlace = libraries.associateBy { it.toPlace() }, + gymByPlace = gyms.associateBy { it.toPlace() }, + printerByPlace = printers.associate { printer -> + val place = printer.toPlace() + place to printer.toPrinterCardUiState() + } + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = EcosystemFavoritesUiState() + ) + fun toggleFavoritesFilter(filter: FavoritesFilterSheetState) { _selectedFavoritesFilters.value = if (filter in _selectedFavoritesFilters.value) { _selectedFavoritesFilters.value - filter @@ -356,4 +394,50 @@ class HomeViewModel @Inject constructor( } return "" } -} \ No newline at end of file +} + +/** + * Derived favorites content for the ecosystem tabs, computed in ViewModel. + */ +data class EcosystemFavoritesUiState( + val filteredSortedFavorites: List = emptyList(), + val eateryByPlace: Map = emptyMap(), + val libraryByPlace: Map = emptyMap(), + val gymByPlace: Map = emptyMap(), + val printerByPlace: Map = emptyMap() +) + +/** + * UI-ready printer fields so composables don't parse backend strings. + */ +data class PrinterCardUiState( + val title: String, + val subtitle: String, + val inColor: Boolean, + val hasCopy: Boolean, + val hasScan: Boolean, + val alertMessage: String +) + +private fun Printer.toPrinterCardUiState(): PrinterCardUiState { + val alertMessage = location.substringAfter("*", "").trim('*').trim() + return PrinterCardUiState( + title = location.substringBefore("*").trim(), + subtitle = description.substringAfter("-", description).trim(), + inColor = description.contains("Color", ignoreCase = true), + hasCopy = description.contains("Copy", ignoreCase = true), + hasScan = description.contains("Scan", ignoreCase = true), + alertMessage = alertMessage + ) +} + +private fun Set.toAllowedPlaceTypes(): Set = buildSet { + if (FavoritesFilterSheetState.EATERIES in this@toAllowedPlaceTypes) add(PlaceType.EATERY) + if (FavoritesFilterSheetState.LIBRARIES in this@toAllowedPlaceTypes) add(PlaceType.LIBRARY) + if (FavoritesFilterSheetState.GYMS in this@toAllowedPlaceTypes) add(PlaceType.GYM) + if (FavoritesFilterSheetState.PRINTERS in this@toAllowedPlaceTypes) add(PlaceType.PRINTER) + if (FavoritesFilterSheetState.OTHER in this@toAllowedPlaceTypes) { + add(PlaceType.APPLE_PLACE) + add(PlaceType.BUS_STOP) + } +} diff --git a/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt b/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt index 1f47111..b5d63a0 100644 --- a/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt +++ b/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt @@ -1,7 +1,5 @@ package com.cornellappdev.transit.util -import android.adservices.adid.AdId - /** * Temporary mapping for about content diff --git a/app/src/main/java/com/cornellappdev/transit/util/DistanceUtils.kt b/app/src/main/java/com/cornellappdev/transit/util/DistanceUtils.kt index 2bda8a6..8feb931 100644 --- a/app/src/main/java/com/cornellappdev/transit/util/DistanceUtils.kt +++ b/app/src/main/java/com/cornellappdev/transit/util/DistanceUtils.kt @@ -1,7 +1,6 @@ package com.cornellappdev.transit.util import android.location.Location -import com.cornellappdev.transit.util.StringUtils.fromMetersToMiles import com.google.android.gms.maps.model.LatLng /**