diff --git a/PennMobile/build.gradle b/PennMobile/build.gradle index 0139198f1..9564bd1f3 100644 --- a/PennMobile/build.gradle +++ b/PennMobile/build.gradle @@ -61,6 +61,7 @@ dependencies { // If you want the foundation layout, use the bundle or the specific activity compose you have defined implementation libs.androidx.activity.compose implementation libs.androidx.material3.android + implementation libs.places // implementation libs.androidx.foundation.layout androidTestImplementation libs.androidx.espresso.core diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/adapters/MenuAdapter.java b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/adapters/MenuAdapter.java deleted file mode 100644 index f995bee09..000000000 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/adapters/MenuAdapter.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.pennapps.labs.pennmobile.dining.adapters; - -import android.content.Context; -import android.graphics.Typeface; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import com.pennapps.labs.pennmobile.R; - -import java.util.HashMap; -import java.util.List; - -/** - * Created by Lily on 2/8/2016. - */ -public class MenuAdapter extends BaseExpandableListAdapter { - private final Context context; - private final List headers; - // child data in format of header title, child title - private final HashMap> children; - - public MenuAdapter(Context context, List headers, - HashMap> children) { - this.context = context; - this.headers = headers; - this.children = children; - } - @Override - public Object getChild(int groupPosition, int childPosititon) { - return this.children.get(this.headers.get(groupPosition)) - .get(childPosititon); - } - @Override - public long getChildId(int groupPosition, int childPosition) { - return childPosition; - } - - @Override - public int getGroupCount() { - return this.headers.size(); - } - - @Override - public int getChildrenCount(int groupPosition) { - return this.children.get(this.headers.get(groupPosition)) - .size(); - } - - @Override - public Object getGroup(int groupPosition) { - return this.headers.get(groupPosition); - } - - @Override - public long getGroupId(int groupPosition) { - return groupPosition; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { - String headerTitle = (String) getGroup(groupPosition); - if (convertView == null) { - LayoutInflater infalInflater = (LayoutInflater) this.context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = infalInflater.inflate(R.layout.menu_station, null); - } - convertView.setPadding(-10, 0, 0, 0); - TextView listHeader = convertView - .findViewById(R.id.station_name); - listHeader.setTypeface(null, Typeface.BOLD); - listHeader.setText(headerTitle); - - ImageView expandArrow = convertView.findViewById(R.id.station_expand); - ImageView collapseArrow = convertView.findViewById(R.id.station_collapse); - - if (isExpanded){ - collapseArrow.setVisibility(View.VISIBLE); - expandArrow.setVisibility(View.INVISIBLE); - listHeader.setTextColor(context.getResources().getColor(R.color.color_primary)); - } - else{ - collapseArrow.setVisibility(View.INVISIBLE); - expandArrow.setVisibility(View.VISIBLE); - listHeader.setTextColor(context.getResources().getColor(R.color.black)); - } - - return convertView; - } - - @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { - final String childText = (String) getChild(groupPosition, childPosition); - - if (convertView == null) { - LayoutInflater infalInflater = (LayoutInflater) this.context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = infalInflater.inflate(R.layout.menu_food_item, null); - } - - TextView txtListChild = convertView - .findViewById(R.id.menu_food_description); - - txtListChild.setText(childText); - return convertView; - } - - @Override - public boolean isChildSelectable(int groupPosition, int childPosition) { - return true; - } -} diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/classes/DiningHall.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/classes/DiningHall.kt index 0bc474962..b2321038f 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/classes/DiningHall.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/classes/DiningHall.kt @@ -22,6 +22,11 @@ open class DiningHall : Parcelable { var image: Int private set + // key date string "yyyy-MM-dd" + // populated by DiningFragment.getMenusForWeek() for today + 6 days. + @Transient + val menusByDate: MutableMap> = HashMap() + @SerializedName("tblDayPart") var menus: MutableList = ArrayList() @@ -55,6 +60,16 @@ open class DiningHall : Parcelable { this.menus.sortedWith(comparator) } + // Sort and store menus for a specific date in menusByDate. + fun sortMealsForDate( + date: String, + menus: MutableList, + ) { + val mealOrder = listOf("Breakfast", "Brunch", "Lunch", "Dinner", "Express") + val sorted = menus.sortedBy { mealOrder.indexOf(it.name) } + menusByDate[date] = sorted.toMutableList() + } + override fun describeContents(): Int = 0 override fun writeToParcel( diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt index 93fc205e2..309b7f1b6 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt @@ -70,6 +70,7 @@ import dagger.hilt.android.AndroidEntryPoint import rx.schedulers.Schedulers import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import kotlin.text.get @AndroidEntryPoint class DiningFragment : Fragment() { @@ -293,36 +294,60 @@ class DiningFragment : Fragment() { } companion object { - // Gets the dining hall menus - fun getMenus(venues: MutableList) { - try { - val idVenueMap = mutableMapOf() - venues.forEach { idVenueMap[it.id] = it } - val current = LocalDateTime.now() - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val formatted = current.format(formatter) - val studentLife = MainActivity.studentLifeInstance - studentLife - .getMenus(formatted) - .subscribeOn(Schedulers.io()) - .subscribe({ menus -> - menus?.filterNotNull()?.forEach { menu -> - menu.venue?.let { venue -> - idVenueMap[venue.venueId]?.let { diningHall -> - val diningHallMenus = diningHall.menus - diningHallMenus.add(menu) - diningHall.sortMeals(diningHallMenus) + private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + // Fetch menus for today + next 6 days for all venues (closed and open) + fun getMenusForWeek(venues: MutableList) { + val idVenueMap = mutableMapOf() + venues.forEach { idVenueMap[it.id] = it } + + val today = LocalDateTime.now() + for (offset in 0..6) { + val date = today.plusDays(offset.toLong()) + val formatted = date.format(dateFormatter) + + try { + val studentLife = MainActivity.studentLifeInstance + studentLife + .getMenus(formatted) + .subscribeOn(Schedulers.io()) + .subscribe({ menus -> + menus?.filterNotNull()?.forEach { menu -> + menu.venue?.let { venue -> + idVenueMap[venue.venueId]?.let { diningHall -> + synchronized(diningHall.menusByDate) { + val dayMenus = + diningHall.menusByDate + .getOrPut(formatted) { ArrayList() } + (dayMenus as java.util.ArrayList).add(menu) + diningHall.sortMealsForDate(formatted, dayMenus) + } + // Keep today's menus in the legacy .menus field so existing code that reads it still works + if (offset == 0) { + synchronized(diningHall) { + diningHall.sortMeals( + diningHall.menusByDate[formatted] + ?: ArrayList(), + ) + } + } + } } } - } - }, { throwable -> - Log.e("DiningFragment", "Error getting Menus", throwable) - }) - } catch (e: Exception) { - e.printStackTrace() + }, { throwable -> + Log.e("DiningFragment", "Error getting menus for $formatted", throwable) + }) + } catch (e: Exception) { + Log.e("DiningFragment", "Exception fetching menus for $formatted", e) + } } } + // single-day fetch + fun getMenus(venues: MutableList) { + getMenusForWeek(venues) + } + // Takes a venue then adds an image and modifies venue name if name is too long fun createHall(venue: Venue): DiningHall { when (venue.id) { diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningInfoFragment.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningInfoFragment.kt index 72b548d10..04a9b642d 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningInfoFragment.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningInfoFragment.kt @@ -20,12 +20,12 @@ import org.joda.time.format.DateTimeFormat */ class DiningInfoFragment : Fragment() { private lateinit var menuParent: RelativeLayout - private var mDiningHall: DiningHall? = null + var mDiningHall: DiningHall? = null private lateinit var mActivity: MainActivity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mDiningHall = arguments?.getParcelable("DiningHall") +// mDiningHall = arguments?.getParcelable("DiningHall") // removed mActivity = activity as MainActivity } diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt index a90e34aae..7497a54c1 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt @@ -5,6 +5,7 @@ import android.util.Log import androidx.core.content.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.pennapps.labs.pennmobile.MainActivity import com.pennapps.labs.pennmobile.compose.utils.Result import com.pennapps.labs.pennmobile.compose.utils.SnackBarEvent import com.pennapps.labs.pennmobile.dining.classes.DiningHall @@ -16,9 +17,13 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import rx.schedulers.Schedulers +import java.time.LocalDate +import java.time.format.DateTimeFormatter import javax.inject.Inject @HiltViewModel @@ -40,6 +45,10 @@ class DiningViewModel private val _snackBarEvent = MutableStateFlow(SnackBarEvent.None) val snackBarEvent: StateFlow = _snackBarEvent + private val _menusByDate = + MutableStateFlow>>(emptyMap()) + val menusByDate: StateFlow>> = _menusByDate + private val _favouriteDiningHalls = diningRepo.favouriteDiningHalls.stateIn( viewModelScope, @@ -47,11 +56,17 @@ class DiningViewModel emptyList(), ) + val favouriteDiningHallIds: StateFlow> = _favouriteDiningHalls + val favouriteDiningHalls = _favouriteDiningHalls - .map { favouriteIDs -> - allDiningHalls.value.filter { diningHall -> favouriteIDs.contains(diningHall.id) } + .combine(allDiningHalls) { favouriteIDs, halls -> + halls.filter { diningHall -> favouriteIDs.contains(diningHall.id) } } + // when refreshed, wiped out the hearts in the all-dining-halls list + // .map { favouriteIDs -> + // allDiningHalls.value.filter { diningHall -> favouriteIDs.contains(diningHall.id) } + // } init { fetchSortOrder() @@ -86,6 +101,42 @@ class DiningViewModel } } + fun fetchMenusForWeek(hall: DiningHall) { + _menusByDate.value = emptyMap() + val fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val today = LocalDate.now() + val mealOrder = listOf("Breakfast", "Brunch", "Lunch", "Dinner", "Express") + + for (offset in 0..6) { + val dateStr = today.plusDays(offset.toLong()).format(fmt) + try { + MainActivity.studentLifeInstance + .getMenus(dateStr) + .subscribeOn(Schedulers.io()) + .subscribe({ menus -> + val dayMenus = + menus + ?.filterNotNull() + ?.filter { it.venue?.venueId == hall.id } + ?.sortedWith { a, b -> + mealOrder.indexOf(a.name) - mealOrder.indexOf(b.name) + } + ?: emptyList() + + if (dayMenus.isNotEmpty()) { + _menusByDate.update { current -> + current + (dateStr to dayMenus) + } + } + }, { throwable -> + Log.e("DiningViewModel", "Error fetching menus for $dateStr", throwable) + }) + } catch (e: Exception) { + Log.e("DiningViewModel", "Exception fetching menus for $dateStr", e) + } + } + } + private fun fetchSortOrder() { _sortOrder.value = DiningHallSortOrder.fromKey( diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/MenuFragment.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/MenuFragment.kt index 2ad3ac388..8d7664ba9 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/MenuFragment.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/MenuFragment.kt @@ -1,5 +1,6 @@ package com.pennapps.labs.pennmobile.dining.fragments +import android.content.Intent import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater @@ -8,38 +9,56 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.appcompat.widget.Toolbar +import android.widget.Button +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.PopupMenu +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.viewpager.widget.PagerAdapter import androidx.viewpager.widget.ViewPager +import com.google.android.material.appbar.CollapsingToolbarLayout +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.tabs.TabLayout import com.pennapps.labs.pennmobile.MainActivity import com.pennapps.labs.pennmobile.R import com.pennapps.labs.pennmobile.dining.classes.DiningHall +import com.pennapps.labs.pennmobile.dining.classes.VenueInterval +import kotlinx.coroutines.launch import org.apache.commons.lang3.StringUtils class MenuFragment : Fragment() { private lateinit var mActivity: MainActivity - private lateinit var toolBar: Toolbar + private lateinit var viewModel: DiningViewModel private var mDiningHall: DiningHall? = null private var pageAdapter: PagerAdapter? = null - inner class TabAdapter( + private var availableDays: List = emptyList() + private var selectedDayIndex: Int = 0 + private lateinit var pager: ViewPager + private lateinit var tabLayout: TabLayout + private lateinit var dateHoursRow: View + + inner class MenuTabAdapter( fm: FragmentManager, + private val menus: List, + private val hallName: String?, ) : FragmentStatePagerAdapter(fm) { - // for each meal: {name of station: arraylist of foods at the station} var foods: ArrayList>> = ArrayList() var headers: ArrayList = ArrayList() var name: String? = null - fun addTabs(hall: DiningHall?) { - val menus = hall?.menus ?: ArrayList() - name = hall?.name - headers.add("HOURS") - foods.add(HashMap()) // first menu is empty for dining hall info tab + init { for (menu in menus) { val stations = HashMap>() headers.add(menu.name) @@ -62,24 +81,15 @@ class MenuFragment : Fragment() { } override fun getItem(position: Int): Fragment { - val myFragment: Fragment - if (position == 0) { - myFragment = DiningInfoFragment() - val args = Bundle() - args.putParcelable("DiningHall", mDiningHall) - args.putString(getString(R.string.menu_arg_name), name) - myFragment.arguments = args - } else { - myFragment = MenuTab() - val args = Bundle() - args.putString(getString(R.string.menu_arg_name), name) - args.putStringArrayList(getString(R.string.menu_arg_stations), ArrayList(foods[position].keys)) - val stations = foods[position] - for (station in stations.keys) { - args.putStringArrayList(station, stations[station]) - } - myFragment.arguments = args + val myFragment = MenuTab() + val args = Bundle() + args.putString(getString(R.string.menu_arg_name), name) + args.putStringArrayList(getString(R.string.menu_arg_stations), ArrayList(foods[position].keys)) + val stations = foods[position] + for (station in stations.keys) { + args.putStringArrayList(station, stations[station]) } + myFragment.arguments = args return myFragment } @@ -88,11 +98,64 @@ class MenuFragment : Fragment() { override fun getCount(): Int = foods.size } + /** + * Adapter used when a dining hall has no menu data for a specific date. + */ + inner class NoMenuDataAdapter( + fm: FragmentManager, + ) : FragmentStatePagerAdapter(fm) { + override fun getItem(position: Int): Fragment = NoMenuDataFragment() + + override fun getPageTitle(position: Int): CharSequence = "NO MENU" + + override fun getCount(): Int = 1 + } + + class NoMenuDataFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = + TextView(requireContext()).apply { + text = "No menu data available for this date." + setTextColor(Color.GRAY) + textSize = 15f + gravity = android.view.Gravity.CENTER + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(Color.WHITE) + } + } + + /** + * Adapter used when the hall has no menu (i.e. joe's cafe) + */ + inner class HoursOnlyTabAdapter( + fm: FragmentManager, + ) : FragmentStatePagerAdapter(fm) { + override fun getItem(position: Int): Fragment { + // Pass venue data directly without parceling to avoid venue being null + val fragment = DiningInfoFragment() + fragment.mDiningHall = mDiningHall + return fragment + } + + override fun getPageTitle(position: Int): CharSequence = "HOURS" + + override fun getCount(): Int = 1 + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mDiningHall = arguments?.getParcelable("DiningHall") mActivity = activity as MainActivity + viewModel = ViewModelProvider(requireActivity())[DiningViewModel::class.java] setHasOptionsMenu(true) + viewModel.refreshData() } override fun onCreateView( @@ -101,12 +164,36 @@ class MenuFragment : Fragment() { savedInstanceState: Bundle?, ): View? { val v = inflater.inflate(R.layout.fragment_menu, container, false) - pageAdapter = TabAdapter(mActivity.supportFragmentManager) - (pageAdapter as TabAdapter).addTabs(mDiningHall) - val pager: ViewPager = v.findViewById(R.id.menu_pager) - pager.adapter = pageAdapter + pager = v.findViewById(R.id.menu_pager) + tabLayout = v.findViewById(R.id.dining_tab_layout) + dateHoursRow = v.findViewById(R.id.dining_date_hours_row) + tabLayout.setTabTextColors(Color.WHITE, Color.WHITE) v.setBackgroundColor(Color.WHITE) - mActivity.addTabs(pageAdapter as TabAdapter, pager, true) + + val hasAnyMenus = MENU_HALLS.contains(mDiningHall?.name) + + if (hasAnyMenus) { + // date & hours row, tabs = meal types + dateHoursRow.visibility = View.VISIBLE + mDiningHall?.let { viewModel.fetchMenusForWeek(it) } + + // refresh tabs whenever the selected date's data arrives + viewLifecycleOwner.lifecycleScope.launch { + viewModel.menusByDate.collect { menusByDate -> + val dateStr = selectedDateString() + val menus = menusByDate[dateStr] + rebuildMenuTabs(menus ?: emptyList()) + } + } + } else { + // No menu data: hide date picker row, only show HOURS tab + dateHoursRow.visibility = View.GONE + val adapter = HoursOnlyTabAdapter(mActivity.supportFragmentManager) + pageAdapter = adapter + pager.adapter = adapter + tabLayout.setupWithViewPager(pager) + tabLayout.setTabTextColors(Color.WHITE, Color.WHITE) + } return v } @@ -114,12 +201,200 @@ class MenuFragment : Fragment() { view: View, savedInstanceState: Bundle?, ) { - super.onViewCreated(view, savedInstanceState) - toolBar = mActivity.findViewById(R.id.toolbar) - toolBar.visibility = View.VISIBLE + WindowInsetsControllerCompat(requireActivity().window, requireView()).isAppearanceLightStatusBars = false + + val localToolbar = view.findViewById(R.id.dining_toolbar) + (activity as AppCompatActivity).setSupportActionBar(localToolbar) + (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) + localToolbar.navigationIcon?.setTint(Color.WHITE) + + // set image + val imageView = view.findViewById(R.id.dining_header_image) + imageView.setImageResource(mDiningHall?.image ?: 0) + + // set title + view.findViewById(R.id.collapsing_toolbar).title = mDiningHall?.name + view.findViewById(R.id.collapsing_toolbar).setExpandedTitleColor(Color.WHITE) + view.findViewById(R.id.collapsing_toolbar).setCollapsedTitleTextColor(Color.WHITE) + + view.findViewById(R.id.dining_location).setOnClickListener { + val location = + when (mDiningHall?.name) { + "Accenture Café" -> "Towne Building" + "Cafe West" -> "Gutmann College House" + "Falk Kosher Dining" -> "Penn Hillel" + "Joe's Café" -> "3620 Locust Walk" + "McClelland Express" -> "3700 Spruce Street" + "Pret a Manger Locust", "Pret a Manger MBA" -> "3730 Walnut St, Philadelphia, PA 19104" + else -> mDiningHall?.name + } + val diningHallMapUrl = "https://maps.google.com/?q=$location" + val intent = Intent(Intent.ACTION_VIEW, diningHallMapUrl.toUri()) + startActivity(intent) + } + + view.findViewById(R.id.dining_website).setOnClickListener { + val website = + when (mDiningHall?.name) { + "Falk Kosher Dining" -> "Falk Dining Commons" + "Accenture Café" -> "Accenture Cafe" + "Joe's Café" -> "Joes Cafe" + "English House" -> "kings court english house" + "McClelland Express" -> "pdss" + "Pret a Manger Locust" -> "Pret a Manger Lower" + "Pret a Manger MBA" -> "Pret a Manger Upper" + else -> mDiningHall?.name + } + val formattedDiningName = website?.lowercase()?.replace(" ", "-") + val diningHallMenuUrl = "https://university-of-pennsylvania.cafebonappetit.com/cafe/$formattedDiningName/" + val intent = Intent(Intent.ACTION_VIEW, diningHallMenuUrl.toUri()) + startActivity(intent) + } + + val favoriteButton = view.findViewById(R.id.favorite_dining) + + fun updateFavoriteButton(isFavourite: Boolean) { + val icon = + if (isFavourite) { + R.drawable.ic_star_24dp + } else { + R.drawable.ic_star_border_24dp + } + favoriteButton.setImageResource(icon) + favoriteButton.setColorFilter(ContextCompat.getColor(requireContext(), android.R.color.holo_orange_light)) + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.favouriteDiningHallIds.collect { favoriteIds -> + updateFavoriteButton(favoriteIds.contains(mDiningHall?.id)) + } + } + + favoriteButton.setOnClickListener { + mDiningHall?.let { hall -> + viewModel.toggleFavourite(hall) + updateFavoriteButton(viewModel.isFavourite(hall)) + } + } + + // Date picker + hours row (only shows up when hall has menus) + val allDays = + (mDiningHall?.venue?.allHours() ?: emptyList()) + .filter { it.meals.isNotEmpty() } + val todayStr = + org.joda.time.LocalDate + .now() + .toString("yyyy-MM-dd") + + // append td's menu if missing + availableDays = + if (allDays.none { it.date == todayStr }) { + val todayInterval = VenueInterval().also { it.date = todayStr } + listOf(todayInterval) + allDays + } else { + allDays + } + selectedDayIndex = findTodayIndex() + + val datePickerButton = view.findViewById