diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9b8d9e..69f0bd7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,6 +99,11 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + // Images + implementation("io.coil-kt.coil3:coil-compose:3.0.4") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4") + implementation("com.valentinilk.shimmer:compose-shimmer:1.3.1") + //Maps implementation("com.google.maps.android:maps-compose:4.0.0") implementation("com.google.android.gms:play-services-maps:19.0.0") diff --git a/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt b/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt index 98e081a..4b7f5b5 100644 --- a/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt +++ b/app/src/main/java/com/cornellappdev/transit/TransitApplication.kt @@ -1,7 +1,26 @@ package com.cornellappdev.transit import android.app.Application +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.memory.MemoryCache +import coil3.request.crossfade import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class TransitApplication : Application() \ No newline at end of file +class TransitApplication : Application(), SingletonImageLoader.Factory { + override fun newImageLoader(context: PlatformContext): ImageLoader { + // Default image loading procedure for network request images + return ImageLoader.Builder(context) + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context, 0.25) + .build() + } + .crossfade(true) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt new file mode 100644 index 0000000..8d96a94 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/models/ecosystem/DayOperatingHours.kt @@ -0,0 +1,9 @@ +package com.cornellappdev.transit.models.ecosystem + +/** + * Mapping of a day of the week to the hours for that day + */ +data class DayOperatingHours( + val dayOfWeek: String, + val hours: List +) 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 104e0bf..6367f8e 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 @@ -1,10 +1,16 @@ package com.cornellappdev.transit.models.ecosystem +import com.cornellappdev.transit.util.TimeUtils.toPascalCaseString import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.time.DayOfWeek +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter /** * Data class representing an eatery */ +@JsonClass(generateAdapter = true) data class Eatery( @Json(name = "id") var id: Int, @Json(name = "name") var name: String, @@ -18,4 +24,79 @@ data class Eatery( @Json(name = "payment_accepts_meal_swipes") var paymentAcceptsMealSwipes: Boolean?, @Json(name = "payment_accepts_brbs") var paymentAcceptsBrbs: Boolean?, @Json(name = "payment_accepts_cash") var paymentAcceptsCash: Boolean?, - ) : DetailedEcosystemPlace \ No newline at end of file + @Json(name = "events") val events: List? +) : DetailedEcosystemPlace { + + /** + * Value to represent the custom order of days in a week (with Sunday as + * the first day due to a particular design choice). Used for sorting purposes + */ + private val dayOrder = mapOf( + "Sunday" to 1, + "Monday" to 2, + "Tuesday" to 3, + "Wednesday" to 4, + "Thursday" to 5, + "Friday" to 6, + "Saturday" to 7 + ) + + /** + * @Return a list of pairs representing each day of the week + * and the corresponding times that an eatery is open. The list is sorted + * by day with the custom dayOrder (Sunday first). + */ + fun formatOperatingHours(): List { + val dailyHours = operatingHours() + + // Convert map to list and sort by custom day order + return dailyHours.entries + .sortedBy { entry -> + val dayName = + entry.key.toPascalCaseString() + dayOrder[dayName] ?: Int.MAX_VALUE + } + .map { entry -> + val dayName = + entry.key.toPascalCaseString() + DayOperatingHours(dayName, entry.value) + } + } + + /** + * @Return a map of each day of the week to its list of operating hours + */ + private fun operatingHours(): Map> { + val dailyHours = mutableMapOf>() + + events?.forEach { event -> + val dayOfWeek = event.startTime?.dayOfWeek + val openTime = event.startTime?.format(DateTimeFormatter.ofPattern("h:mm a")) + val closeTime = event.endTime?.format(DateTimeFormatter.ofPattern("h:mm a")) + + val timeString = "$openTime - $closeTime" + + if (dayOfWeek != null && dailyHours[dayOfWeek]?.none { it.contains(timeString) } != false) { + dailyHours.computeIfAbsent(dayOfWeek) { mutableListOf() }.add(timeString) + } + } + + DayOfWeek.entries.forEach { dayOfWeek -> + dailyHours.computeIfAbsent(dayOfWeek) { mutableListOf("Closed") } + } + + return dailyHours + } +} + + +@JsonClass(generateAdapter = true) +data class Event( + @Json(name = "id") val id: Int? = null, + /** + * Descriptions tend to be "Lunch", "Dinner", etc.. + */ + @Json(name = "event_description") val description: String? = null, + @Json(name = "start") val startTime: LocalDateTime? = null, + @Json(name = "end") val endTime: LocalDateTime? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt b/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt index a64af5c..77c7b0b 100644 --- a/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt +++ b/app/src/main/java/com/cornellappdev/transit/networking/EateryNetworkApi.kt @@ -4,7 +4,7 @@ import com.cornellappdev.transit.models.ecosystem.Eatery import retrofit2.http.GET interface EateryNetworkApi { - @GET("/eatery/simple") + @GET("/eatery/") suspend fun getEateries(): List } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt b/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt index 639bafa..0a988e4 100644 --- a/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt +++ b/app/src/main/java/com/cornellappdev/transit/networking/MoshiAdapters.kt @@ -3,6 +3,12 @@ package com.cornellappdev.transit.networking import com.google.android.gms.maps.model.LatLng import com.squareup.moshi.FromJson import com.squareup.moshi.Json +import com.squareup.moshi.ToJson +import java.text.ParseException +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter class LatLngAdapter { @@ -21,4 +27,22 @@ class LatLngAdapter { fun fromJson(coord: Coordinate): LatLng { return LatLng(coord.latitude, coord.longitude) } +} + +class DateTimeAdapter { + @ToJson + fun toJson(dateTime: LocalDateTime): String { + return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")) + } + + @FromJson + fun fromJson(dateTime: Long): LocalDateTime { + try { + val instant = Instant.ofEpochSecond(dateTime) + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()) + } catch (e: ParseException) { + e.printStackTrace() + } + return LocalDateTime.MIN + } } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt b/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt index fc371d9..eae3f94 100644 --- a/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt +++ b/app/src/main/java/com/cornellappdev/transit/networking/NetworkModule.kt @@ -55,6 +55,7 @@ object NetworkModule { fun provideMoshi(): Moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .add(LatLngAdapter()) + .add(DateTimeAdapter()) .build() @Provides diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt new file mode 100644 index 0000000..6b0bc3e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/CenteredSpinningIndicator.kt @@ -0,0 +1,26 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.ui.theme.MetadataGray + +@Composable +fun CenteredSpinningIndicator() { + Row( + modifier = Modifier + .fillMaxWidth() + .height(138.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator(color = MetadataGray, modifier = Modifier.size(40.dp)) + } +} \ 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 new file mode 100644 index 0000000..2232832 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceHeaderSection.kt @@ -0,0 +1,94 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +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 + +/** + * Text area of detailed place header with favorites star + */ +@Composable +fun DetailedPlaceHeaderSection( + title: String, + subtitle: String?, + leftAnnotatedString: AnnotatedString? = null, + rightAnnotatedString: AnnotatedString? = null, + onFavoriteClick: () -> Unit, + isFavorite: Boolean +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + ) { + Column( + modifier = Modifier + .align(Alignment.CenterStart), + ) { + Text( + text = title, + style = Style.detailHeading, + color = PrimaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 32.dp, bottom = 12.dp) + ) + subtitle?.let { + Text( + text = subtitle, + style = Style.cardSubtitle, + color = SecondaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(end = 32.dp, bottom = 8.dp) + + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + leftAnnotatedString?.let { + Text( + text = leftAnnotatedString, + style = Style.cardSubtitle + ) + } + Spacer(modifier = Modifier.weight(1f)) + rightAnnotatedString?.let { + Text( + text = rightAnnotatedString, + style = Style.cardSubtitle + ) + } + } + } + + FavoritesStar(onFavoriteClick, isFavorite) + } +} + +@Preview(showBackground = true) +@Composable +private fun DetailedPlaceHeaderSectionPreview() { + DetailedPlaceHeaderSection( + title = "Atrium Cafe", + subtitle = "Sage Hall", + onFavoriteClick = {}, + isFavorite = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt index f1f8ae2..6fca4bc 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt @@ -90,8 +90,13 @@ fun DetailedPlaceSheetContent( // Main Content when (ecosystemPlace) { is Eatery -> { - //TODO - Text(ecosystemPlace.name) + EateryDetailsContent( + eatery = ecosystemPlace, + isFavorite = ecosystemPlace.toPlace() in favorites, + onFavoriteClick = { + onFavoriteStarClick(ecosystemPlace.toPlace()) + } + ) } is Library -> { @@ -131,7 +136,9 @@ fun DetailedPlaceSheetContent( .clickable { when (ecosystemPlace) { is Eatery -> { - //TODO + navigateToPlace( + ecosystemPlace.toPlace() + ) } is Library -> { 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 new file mode 100644 index 0000000..64da8a8 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt @@ -0,0 +1,130 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +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 +import com.cornellappdev.transit.ui.theme.Gray05 +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.getAboutContent + +@Composable +fun EateryDetailsContent( + homeViewModel: HomeViewModel = hiltViewModel(), + eatery: Eatery, + isFavorite: Boolean, + onFavoriteClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 20.dp, + end = 20.dp, + ) + ) { + PlaceCardImage( + imageUrl = eatery.imageUrl, + placeholderRes = R.drawable.olin_library, + shouldClipBottom = true + ) + + DetailedPlaceHeaderSection( + eatery.name, + eatery.campusArea, + leftAnnotatedString = homeViewModel.isOpenAnnotatedStringFromOperatingHours( + eatery.formatOperatingHours() + ), + onFavoriteClick = onFavoriteClick, + isFavorite = isFavorite + ) + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider(thickness = 1.dp, color = DividerGray) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "About", + style = Style.detailSubtitle, + color = PrimaryText, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Text( + text = getAboutContent(eatery.name), + style = Style.detailBody, + color = SecondaryText, + modifier = Modifier.padding(bottom = 15.dp) + ) + + Text( + text = stringResource(R.string.view_menu), + style = Style.heading2, + color = TransitBlue + ) + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider(thickness = 1.dp, color = DividerGray) + + Spacer(modifier = Modifier.height(24.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(R.drawable.location_pin_gray), + contentDescription = null, + modifier = Modifier + .size(20.dp), + tint = Gray05 + ) + Text( + text = eatery.location ?: "", + style = Style.detailBody, + color = SecondaryText, + modifier = Modifier.padding(start = 15.dp) + ) + + } + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider(thickness = 1.dp, color = DividerGray) + + ExpandableOperatingHoursList( + homeViewModel.isOpenAnnotatedStringFromOperatingHours(eatery.formatOperatingHours()), + homeViewModel.rotateOperatingHours(eatery.formatOperatingHours()) + ) + + } +} \ No newline at end of file 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 7f86542..d1eae12 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 @@ -14,18 +14,22 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel 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.StaticPlaces import com.cornellappdev.transit.networking.ApiResponse import com.cornellappdev.transit.ui.theme.robotoFamily import com.cornellappdev.transit.ui.viewmodels.FilterState +import com.cornellappdev.transit.ui.viewmodels.HomeViewModel import com.cornellappdev.transit.util.ecosystem.toPlace @@ -94,6 +98,7 @@ fun EcosystemBottomSheetContent( @Composable private fun BottomSheetFilteredContent( + homeViewModel: HomeViewModel = hiltViewModel(), currentFilter: FilterState, staticPlaces: StaticPlaces, favorites: Set, @@ -119,7 +124,13 @@ private fun BottomSheetFilteredContent( } FilterState.EATERIES -> { - eateryList(staticPlaces, navigateToPlace) + eateryList( + eateriesApiResponse = staticPlaces.eateries, + onDetailsClick = onDetailsClick, + favorites = favorites, + onFavoriteStarClick = onFavoriteStarClick, + operatingHoursToString = homeViewModel::isOpenAnnotatedStringFromOperatingHours + ) } FilterState.LIBRARIES -> { @@ -212,23 +223,38 @@ private fun LazyListScope.printerList( * LazyList scoped enumeration of eateries for bottom sheet */ private fun LazyListScope.eateryList( - staticPlaces: StaticPlaces, - navigateToPlace: (Place) -> Unit + eateriesApiResponse: ApiResponse>, + onDetailsClick: (DetailedEcosystemPlace) -> Unit, + favorites: Set, + onFavoriteStarClick: (Place) -> Unit, + operatingHoursToString: (List) -> AnnotatedString ) { - when (staticPlaces.eateries) { + when (eateriesApiResponse) { is ApiResponse.Error -> { } is ApiResponse.Pending -> { + item { + CenteredSpinningIndicator() + } } is ApiResponse.Success -> { - items(staticPlaces.eateries.data) { - BottomSheetLocationCard( + items(eateriesApiResponse.data) { + RoundedImagePlaceCard( + imageUrl = it.imageUrl, title = it.name, - subtitle1 = it.location.orEmpty() + subtitle = it.location ?: "", + isFavorite = it.toPlace() in favorites, + onFavoriteClick = { + onFavoriteStarClick(it.toPlace()) + }, + placeholderRes = R.drawable.olin_library, + leftAnnotatedString = operatingHoursToString( + it.formatOperatingHours() + ) ) { - //TODO: Eatery + onDetailsClick(it) } } } @@ -255,7 +281,7 @@ private fun LazyListScope.libraryList( is ApiResponse.Success -> { items(staticPlaces.libraries.data) { RoundedImagePlaceCard( - imageRes = R.drawable.olin_library, + placeholderRes = R.drawable.olin_library, title = it.location, subtitle = it.address, isFavorite = it.toPlace() in favorites, diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt new file mode 100644 index 0000000..d355140 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/ExpandableOperatingHoursList.kt @@ -0,0 +1,96 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.R +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours +import com.cornellappdev.transit.ui.theme.DividerGray +import com.cornellappdev.transit.ui.theme.Gray05 +import com.cornellappdev.transit.ui.theme.Style +import com.cornellappdev.transit.util.sampleHours + +/** + * + */ +@Composable +fun ExpandableOperatingHoursList( + annotatedString: AnnotatedString, + operatingHours: List +) { + + var isExpanded by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { + isExpanded = !isExpanded + }) { + Icon( + painterResource(R.drawable.clock), + contentDescription = null, + modifier = Modifier + .size(20.dp), + tint = Gray05 + ) + Text( + text = annotatedString, + style = Style.detailBody, + modifier = Modifier.padding(start = 15.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + + if (isExpanded) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.baseline_expand_less), + contentDescription = "Expand less", + modifier = Modifier.height(79.dp) + ) + } else { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.baseline_expand_more), + contentDescription = "Expand more", + modifier = Modifier.height(79.dp) + ) + } + } + + if (isExpanded) { + OperatingHoursList(operatingHours) + } + HorizontalDivider(thickness = 1.dp, color = DividerGray) + } + +} + +@Preview(showBackground = true) +@Composable +private fun ExpandableOperatingHoursListPreview() { + ExpandableOperatingHoursList( + annotatedString = buildAnnotatedString { + append("Open") + }, + operatingHours = sampleHours + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt new file mode 100644 index 0000000..4afd729 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/OperatingHoursList.kt @@ -0,0 +1,112 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours +import com.cornellappdev.transit.ui.theme.PrimaryText +import com.cornellappdev.transit.ui.theme.SecondaryText +import com.cornellappdev.transit.ui.theme.Style.heading3 +import com.cornellappdev.transit.ui.theme.Style.heading3Semibold +import com.cornellappdev.transit.util.sampleHours + +/** + * Composable that displays operating hours for an eatery + * + * @param operatingHours List of pairs where first is the day name and second is list of time ranges + * @param modifier Modifier for the component + */ +@Composable +fun OperatingHoursList( + operatingHours: List, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + operatingHours.forEachIndexed { index, (day, hours) -> + OperatingHoursRow( + day = day, + hours = hours, + index == 0 + ) + } + } +} + +/** + * Single row displaying a day and its operating hours + */ +@Composable +private fun OperatingHoursRow( + day: String, + hours: List, + isHighlighted: Boolean = false +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Row( + horizontalArrangement = Arrangement.Start, + modifier = Modifier.weight(0.6f) + ) { + if (isHighlighted) { + VerticalDivider( + color = PrimaryText, + thickness = 2.dp, + modifier = Modifier.height(16.dp) + ) + Spacer(Modifier.width(12.dp)) + } else { + Spacer(Modifier.width(14.dp)) + } + + Text( + text = day, + style = if (isHighlighted) heading3Semibold else heading3, + color = if (isHighlighted) PrimaryText else SecondaryText, + ) + } + + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.weight(0.4f) + ) { + hours.forEach { timeRange -> + Text( + text = timeRange, + style = heading3, + color = SecondaryText, + modifier = Modifier.padding(bottom = 8.dp), + textAlign = TextAlign.Left + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun OperatingHoursPreview() { + + OperatingHoursList(operatingHours = sampleHours) +} \ No newline at end of file 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 new file mode 100644 index 0000000..814205e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/PlaceCardImage.kt @@ -0,0 +1,49 @@ +package com.cornellappdev.transit.ui.components.home + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +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) { + + val imageModifier = Modifier + .then( + if(shouldClipBottom) Modifier.clip(RoundedCornerShape(12.dp)) + else Modifier.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))) + .fillMaxWidth() + .height(112.dp) + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .background(MetadataGray) + + if (imageUrl.isNullOrBlank()) { + Image( + painter = painterResource(id = placeholderRes), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + } else { + AsyncImage( + model = imageUrl, + contentDescription = null, + error = painterResource(id = placeholderRes), + contentScale = ContentScale.Crop, + modifier = imageModifier + ) + } +} \ No newline at end of file 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 c58f95c..9fa4ab0 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 @@ -1,7 +1,6 @@ package com.cornellappdev.transit.ui.components.home import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -10,47 +9,37 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.outlined.Star -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.PlatformTextStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.transit.R -import com.cornellappdev.transit.ui.theme.FavoritesYellow import com.cornellappdev.transit.ui.theme.PrimaryText import com.cornellappdev.transit.ui.theme.SecondaryText import com.cornellappdev.transit.ui.theme.Style +/** + * Card for a place with a rounded image on top + */ @Composable fun RoundedImagePlaceCard( - @DrawableRes imageRes: Int, + imageUrl: String? = null, title: String, subtitle: String, isFavorite: Boolean, onFavoriteClick: () -> Unit, leftAnnotatedString: AnnotatedString? = null, rightAnnotatedString: AnnotatedString? = null, - onClick: () -> Unit + @DrawableRes placeholderRes: Int, + onClick: () -> Unit, ) { Column( modifier = Modifier @@ -62,15 +51,7 @@ fun RoundedImagePlaceCard( .fillMaxWidth() .background(Color.White, shape = RoundedCornerShape(12.dp)) ) { - Image( - painter = painterResource(id = imageRes), - contentDescription = title, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(112.dp) - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - ) + PlaceCardImage(imageUrl, placeholderRes) Box( modifier = Modifier @@ -105,13 +86,15 @@ fun RoundedImagePlaceCard( ) { leftAnnotatedString?.let { Text( - text = leftAnnotatedString + text = leftAnnotatedString, + style = Style.heading3 ) } Spacer(modifier = Modifier.weight(1f)) rightAnnotatedString?.let { Text( - text = rightAnnotatedString + text = rightAnnotatedString, + style = Style.heading3 ) } } @@ -128,7 +111,7 @@ fun RoundedImagePlaceCard( @Composable fun RoundedImagePlaceCardPreview() { RoundedImagePlaceCard( - imageRes = R.drawable.olin_library, + placeholderRes = R.drawable.olin_library, title = "Olin Library", subtitle = "Ho Plaza", isFavorite = true, diff --git a/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt b/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt index 5ca2412..f7c0b77 100644 --- a/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt +++ b/app/src/main/java/com/cornellappdev/transit/ui/theme/Style.kt @@ -55,6 +55,15 @@ object Style { ), ) + val cardSubtitle = TextStyle( + fontSize = 16.sp, + fontFamily = robotoFamily, + lineHeight = 16.sp, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + ) + val heading3Emphasized = TextStyle( fontSize = 14.sp, fontFamily = robotoFamily, @@ -65,6 +74,16 @@ object Style { fontWeight = FontWeight(500) ) + val heading3Semibold = TextStyle( + fontSize = 14.sp, + fontFamily = robotoFamily, + lineHeight = 16.sp, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + fontWeight = FontWeight(600) + ) + /** * TextStyle for headers of route types */ 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 bd7c24d..53f373c 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 @@ -1,6 +1,11 @@ package com.cornellappdev.transit.ui.viewmodels import android.content.Context +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.cornellappdev.transit.models.LocationRepository @@ -9,9 +14,15 @@ 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.DayOperatingHours import com.cornellappdev.transit.models.ecosystem.EateryRepository import com.cornellappdev.transit.models.ecosystem.GymRepository import com.cornellappdev.transit.networking.ApiResponse +import com.cornellappdev.transit.ui.theme.LateRed +import com.cornellappdev.transit.ui.theme.LiveGreen +import com.cornellappdev.transit.ui.theme.SecondaryText +import com.cornellappdev.transit.ui.theme.Style.cardSubtitle +import com.cornellappdev.transit.util.TimeUtils.toPascalCaseString import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -27,6 +38,11 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -274,4 +290,158 @@ class HomeViewModel @Inject constructor( this.filterState.value = filterState } + /** + * Rotate operating hours such that first value is today's date + * + * @param operatingHours A list of pairs mapping the first value day string to second value list of hours open + */ + fun rotateOperatingHours( + operatingHours: List, + currentDate: LocalDate = LocalDate.now() + ): List { + val today = currentDate.dayOfWeek.toPascalCaseString() + + val todayIndex = operatingHours.indexOfFirst { + it.dayOfWeek.equals(today, ignoreCase = true) + } + + // Defensive programming only if [operatingHours] is missing a day + if (todayIndex == -1) return operatingHours + + return operatingHours.drop(todayIndex) + operatingHours.take(todayIndex) + } + + /** + * Find the next time a place is open if it is closed for the day + */ + private fun findOpenNextDay(operatingHours: List): OpenStatus { + // Check day after + val dayAfter = operatingHours[1].hours + if (!dayAfter.any { it.equals("Closed", ignoreCase = true) }) { + val firstOpenTime = parseTimeRange(dayAfter[0])?.first + if (firstOpenTime != null) { + return OpenStatus( + false, + "until ${formatTime(firstOpenTime)}" + ) + } + } + // Find next open day + for (i in 2 until operatingHours.size) { + val currDay = operatingHours[i].hours + if (!currDay.any { it.equals("Closed", ignoreCase = true) }) { + val dayName = operatingHours[i].dayOfWeek + return OpenStatus( + false, + "until $dayName" + ) + } + } + return OpenStatus(false, "Closed today") + } + + /** + * Given operating hours rotated for today's date, return whether it is open and when it is open until + * or when it will next open + * + * @param operatingHours A list of pairs mapping the first value day string to second value list of hours open + */ + private fun getOpenStatus( + operatingHours: List, + currentDateTime: LocalDateTime = LocalDateTime.now() + ): OpenStatus { + + val currentTime = currentDateTime.toLocalTime() + val todaySchedule = operatingHours[0].hours // First day should be today after rotation + + // Check if closed today + if (todaySchedule.any { it.equals("Closed", ignoreCase = true) }) { + return findOpenNextDay(operatingHours) + } + + val timeRanges = todaySchedule.mapNotNull { parseTimeRange(it) } + + // Check if currently open + for (range in timeRanges) { + if (currentTime >= range.first && currentTime < range.second) { + return OpenStatus(true, "until ${formatTime(range.second)}") + } + } + + // Check if opens later today + for (range in timeRanges) { + if (currentTime < range.first) { + return OpenStatus(false, "until ${formatTime(range.first)}") + } + } + + // Closed for today, find next open day + return findOpenNextDay(operatingHours) + } + + /** + * Return annotated string for open times + */ + private fun getOpenStatusAnnotatedString(openStatus: OpenStatus): AnnotatedString { + return buildAnnotatedString { + if (openStatus.isOpen) { + withStyle( + style = SpanStyle( + color = LiveGreen, + ) + ) { + append("Open") + } + } else { + withStyle( + style = SpanStyle( + color = LateRed + ) + ) { + append("Closed") + } + } + withStyle( + style = SpanStyle( + color = SecondaryText + ) + ) { + append(" - ") + append(openStatus.nextChangeTime) + } + } + } + + private fun parseTimeRange(timeString: String): Pair? { + if (timeString.equals("Closed", ignoreCase = true)) return null + + val parts = timeString.split("-").map { it.trim() } + if (parts.size != 2) return null + + return try { + val formatter = DateTimeFormatter.ofPattern("h:mm a") + val start = LocalTime.parse(parts[0], formatter) + val end = LocalTime.parse(parts[1], formatter) + start to end + } catch (e: Exception) { + null + } + } + + private fun formatTime(time: LocalTime): String { + val formatter = DateTimeFormatter.ofPattern("h:mm a") + return time.format(formatter) + } + + /** + * Rotate operating hours to current day, then determine if place is open, then format string + */ + fun isOpenAnnotatedStringFromOperatingHours(operatingHours: List): AnnotatedString { + return getOpenStatusAnnotatedString( + getOpenStatus( + rotateOperatingHours(operatingHours) + ) + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt new file mode 100644 index 0000000..93171c9 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/OpenStatus.kt @@ -0,0 +1,3 @@ +package com.cornellappdev.transit.ui.viewmodels + +data class OpenStatus(val isOpen: Boolean, val nextChangeTime: String) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt b/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt new file mode 100644 index 0000000..d43f21d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/util/ContentConstants.kt @@ -0,0 +1,22 @@ +package com.cornellappdev.transit.util + + +/** + * Temporary mapping for about content + */ +fun getAboutContent(key: String): String { + val aboutContent = buildMap { + put("104West!", "Cornell's kosher and multicultural dining room is STAR-K and STAR-D certified.") + put("Becker House Dining Room", "Dining room located in Carl Becker House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Cook House Dining Room", "Dining room located in Alice Cook House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Jansen's Dining Room at Bethe House", "Dining room located in Hans Bethe House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Keeton House Dining Room", "Dining room located in William Keeton House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + put("Morrison Dining", "Choose your own culinary adventure at Cornell's newest dining room.") + put("North Star Dining Room", "Dining room located in Appel Commons on North Campus.") + put("Okenshields", "Dining room located in Willard Straight Hall on Central Campus.") + put("Risley Dining Room", "Risley is our gluten-free, tree nut free and peanut free dining room under the AllerCheckā„¢\uFE0F approved by MenuTrinfoĀ®\uFE0F program, in Risley Residential College on North Campus.") + put("Rose House Dining Room", "Dining room located in Flora Rose House on West Campus. Open only to residents from 6-7pm Wednesdays for House Dinners.") + } + + return aboutContent.getOrDefault(key, "") +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt b/app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt new file mode 100644 index 0000000..3f7c01f --- /dev/null +++ b/app/src/main/java/com/cornellappdev/transit/util/PreviewConstants.kt @@ -0,0 +1,24 @@ +package com.cornellappdev.transit.util + +import com.cornellappdev.transit.models.ecosystem.DayOperatingHours + +val sampleHours = listOf( + DayOperatingHours("Tuesday", listOf("10:00 AM - 10:00 PM")), + DayOperatingHours("Wednesday", listOf("10:00 AM - 5:00 PM")), + DayOperatingHours("Thursday", listOf("Closed")), + DayOperatingHours("Friday", listOf("10:00 AM - 5:00 PM")), + DayOperatingHours( + "Saturday", listOf( + "8:00 AM - 9:30 AM", + "10:30 AM - 2:00 PM", + "5:00 PM - 8:00 PM" + ) + ), + DayOperatingHours( + "Sunday", listOf( + "10:00 AM - 2:00 PM", + "5:00 PM - 8:30 PM" + ) + ), + DayOperatingHours("Monday", listOf("10:00 AM - 10:00 PM")), +) \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt b/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt index 8ea5e3b..34642b0 100644 --- a/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt +++ b/app/src/main/java/com/cornellappdev/transit/util/TimeUtils.kt @@ -1,6 +1,7 @@ package com.cornellappdev.transit.util import android.icu.text.SimpleDateFormat +import java.time.DayOfWeek import java.time.Duration import java.time.Instant import java.time.ZoneId @@ -93,4 +94,11 @@ object TimeUtils { return Instant.parse(isoString).plusSeconds(seconds.toLong()).toString() } + /** + * Day of week with first letter capitalized and all else lowercase + */ + fun DayOfWeek.toPascalCaseString(): String = this.name.lowercase() + .replaceFirstChar { it.uppercase() } + + } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt b/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt index da842a1..79a75e2 100644 --- a/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt +++ b/app/src/main/java/com/cornellappdev/transit/util/ecosystem/PlaceUtils.kt @@ -2,6 +2,7 @@ package com.cornellappdev.transit.util.ecosystem import com.cornellappdev.transit.models.Place import com.cornellappdev.transit.models.PlaceType +import com.cornellappdev.transit.models.ecosystem.Eatery import com.cornellappdev.transit.models.ecosystem.Library import com.cornellappdev.transit.models.ecosystem.Printer @@ -27,3 +28,14 @@ fun Printer.toPlace(): Place = Place( detail = this.description, type = PlaceType.APPLE_PLACE ) + +/** + * Predefined mapping from eatery to generic place. Nullable latitudes and longitudes default to 0 + */ +fun Eatery.toPlace(): Place = Place( + latitude = this.latitude ?: 0.0, + longitude = this.longitude ?: 0.0, + name = this.name, + detail = this.location, + type = PlaceType.APPLE_PLACE +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4ce9d0..c6488bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Help us improve Navi by letting us know what\'s wrong Covers humanities and social sciences with circulation and reference services. Reserve a room + View Menu on Eatery \ No newline at end of file