User can now select allergens to exclude from the canteen offers

This commit is contained in:
denizk0461
2023-05-01 19:17:35 +02:00
parent 79f8e9cfca
commit b0631d323e
17 changed files with 718 additions and 27 deletions
@@ -224,7 +224,7 @@ class AppRepository(app: Application) {
// --- settings preferences --- //
/**
* Retrieves ta user-set boolean preference.
* Retrieves a user-set boolean preference.
*
* @param pref preference to retrieve
* @return whether the user set this preference
@@ -233,7 +233,16 @@ class AppRepository(app: Application) {
prefs.getBoolean(pref.key, defaultValue)
/**
* Retrieves ta user-set integer preference.
* Retrieves a user-set string preference.
*
* @param pref preference to retrieve
* @return string that was set
*/
fun getStringPreference(pref: SettingsPreferences, defaultValue: String = ""): String =
prefs.getString(pref.key, defaultValue) ?: ""
/**
* Retrieves a user-set integer preference.
*
* @param pref preference to retrieve
* @return whether the user set this preference
@@ -250,6 +259,16 @@ class AppRepository(app: Application) {
prefs.edit().putBoolean(pref.key, newValue).apply()
}
/**
* Updates a user-set string preference.
*
* @param pref preference to set
* @param newValue new value to set the preference to
*/
fun setPreference(pref: SettingsPreferences, newValue: String) {
prefs.edit().putString(pref.key, newValue).apply()
}
/**
* Updates a user-set integer preference.
*
@@ -27,7 +27,6 @@ class CanteenFragment : AppFragment() {
/*
* Non-null reference to the view binding. This property is only valid between onCreateView and
* onDestroyView.
* BUG if the fragment is changed before the refresh is finished, the app crashes with a NPE
*/
private val binding get() = _binding!!
@@ -34,8 +34,10 @@ class CanteenPageFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListen
// View model reference for providing access to the database
private val viewModel: CanteenPageViewModel by viewModels()
// Recycler view adapter
private lateinit var recyclerViewAdapter: CanteenOfferItemAdapter
// List of offers, stored locally to re-set them upon dietary preference update
private val offerList: MutableList<CanteenOffer> = mutableListOf()
/**
@@ -57,6 +59,7 @@ class CanteenPageFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListen
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set up recycler view adapter
recyclerViewAdapter = CanteenOfferItemAdapter(
this,
viewModel.preferenceAllergen,
@@ -74,6 +77,7 @@ class CanteenPageFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListen
// Animate creation of new page
scheduleLayoutAnimation()
// Observe offers and re-set them if they change
viewModel.getOffersByDay(currentDay).observe(viewLifecycleOwner) { offers ->
offerList.clear()
offerList.addAll(offers)
@@ -81,15 +85,13 @@ class CanteenPageFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListen
}
}
// Observe dietary preference updates and re-set the items if a change is detected
viewModel.dietaryPreferencesUpdate.observe(viewLifecycleOwner) {
filterList()
// Re-set the list of items to the recycler view, forcing an update
recyclerViewAdapter.setNewData(offerList.filterElements().groupElements().distinct())
}
}
private fun filterList() {
recyclerViewAdapter.setNewData(offerList.filterElements().groupElements().distinct())
}
/**
* Groups [CanteenOffer] elements by their categories into [CanteenOfferGroup] elements and
* filters for the user's dietary preferences.
@@ -120,12 +122,20 @@ class CanteenPageFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListen
}
}
/**
* Filters the list of canteen offers by dietary and allergen preferences.
*
* @return filtered list of offers
*/
private fun List<CanteenOffer>.filterElements(): List<CanteenOffer> {
// Get dietary preference regex
val prefsRegex = getPrefRegex()
val allergenPrefs = viewModel.preferenceAllergenConfig
val allergenFilteredList = mutableListOf<CanteenOffer>()
return if (prefsRegex.toString() == DietaryPreferences.TEMPLATE_EMPTY) {
// Filter offers not conforming to the dietary preferences set by the user
val newList = if (prefsRegex.toString() == DietaryPreferences.TEMPLATE_EMPTY) {
// Show all elements and skip filtering if no preference is set
this
} else {
@@ -134,6 +144,35 @@ class CanteenPageFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListen
prefsRegex.matches(it.dietaryPreferences)
}
}
// Filter offers containing allergens the user wishes to have hidden
return if (allergenPrefs.isBlank()) {
// If no preferences have been set by the user, skip the checks
newList
} else {
// If the user has set allergens, check each item individually
newList.forEach outer@{ item ->
// If the item in question doesn't contain any allergens, skip the check
if (item.allergens.isBlank()) {
// Add the item to the new list directly
allergenFilteredList.add(item)
} else {
// Check each individual allergen in the offering
item.allergens.split(",").forEach inner@{ allergen ->
// If an allergen can be found in the user-excluded list, stop checking
if (allergenPrefs.contains(allergen)) {
return@outer
}
}
// If no allergens in the offering are present in the exclusion list, add it
allergenFilteredList.add(item)
}
}
// Return the allergen-filtered list
allergenFilteredList
}
}
/**
@@ -14,6 +14,8 @@ import com.denizk0461.studip.activity.FetcherActivity
import com.denizk0461.studip.activity.ImageActivity
import com.denizk0461.studip.data.showToast
import com.denizk0461.studip.databinding.FragmentSettingsBinding
import com.denizk0461.studip.model.AllergenPreferences
import com.denizk0461.studip.sheet.AllergenConfigSheet
import com.denizk0461.studip.sheet.DevCodeSheet
import com.denizk0461.studip.sheet.TextSheet
import com.denizk0461.studip.viewmodel.SettingsViewModel
@@ -65,6 +67,16 @@ class SettingsFragment : AppFragment() {
}
}
binding.buttonAllergensConfig.setOnClickListener {
openBottomSheet(
AllergenConfigSheet(
AllergenPreferences.construct(viewModel.preferenceAllergenConfig)
) { obj ->
viewModel.preferenceAllergenConfig = obj.deconstruct()
}
)
}
// Set up switch for displaying allergens
binding.switchAllergens.apply {
isChecked = viewModel.preferenceAllergen
@@ -0,0 +1,208 @@
package com.denizk0461.studip.model
// TODO KDoc
enum class AllergenPreferences {
GLUTEN_WHEAT,
GLUTEN_RYE,
GLUTEN_BARLEY,
GLUTEN_OATS,
GLUTEN_SPELT,
GLUTEN_KAMUT,
CRUSTACEANS,
EGGS,
FISH,
PEANUTS,
SOY,
DAIRY,
ALMONDS,
HAZELNUTS,
WALNUTS,
CASHEW_NUTS,
PECANS,
BRAZIL_NUTS,
PISTACHIOS,
MACADAMIA,
CELERY,
MUSTARD,
SULPHIDES,
LUPINS,
SESAME,
MOLLUSCS,
;
/**
* Object that holds allergy preferences. Can be used to hold both the user-set values as well
* as the values of an individual canteen item.
*
* @param hasWheat
* @param hasRye
* @param hasBarley
* @param hasOats
* @param hasSpelt
* @param hasKamut
* @param hasCrustaceans
* @param hasEggs
* @param hasFish
* @param hasPeanuts
* @param hasSoy
* @param hasDairy
* @param hasAlmonds
* @param hasHazelnuts
* @param hasWalnuts
* @param hasCashewNuts
* @param hasPecans
* @param hasBrazilNuts
* @param hasPistachios
* @param hasMacadamia
* @param hasCelery
* @param hasMustard
* @param hasSulphides
* @param hasLupins
* @param hasSesame
* @param hasMolluscs
*/
data class Object(
val hasWheat: Boolean,
val hasRye: Boolean,
val hasBarley: Boolean,
val hasOats: Boolean,
val hasSpelt: Boolean,
val hasKamut: Boolean,
val hasCrustaceans: Boolean,
val hasEggs: Boolean,
val hasFish: Boolean,
val hasPeanuts: Boolean,
val hasSoy: Boolean,
val hasDairy: Boolean,
val hasAlmonds: Boolean,
val hasHazelnuts: Boolean,
val hasWalnuts: Boolean,
val hasCashewNuts: Boolean,
val hasPecans: Boolean,
val hasBrazilNuts: Boolean,
val hasPistachios: Boolean,
val hasMacadamia: Boolean,
val hasCelery: Boolean,
val hasMustard: Boolean,
val hasSulphides: Boolean,
val hasLupins: Boolean,
val hasSesame: Boolean,
val hasMolluscs: Boolean,
) {
/**
* Deconstructs an AllergenPreferencesObject to a string.
*
* @return string with preferences inserted
*/
fun deconstruct(): String {
val array = arrayListOf<String>()
if (hasWheat) array.add("a1")
if (hasRye) array.add("a2")
if (hasBarley) array.add("a3")
if (hasOats) array.add("a4")
if (hasSpelt) array.add("a5")
if (hasKamut) array.add("a6")
if (hasCrustaceans) array.add("b")
if (hasEggs) array.add("c")
if (hasFish) array.add("d")
if (hasPeanuts) array.add("e")
if (hasSoy) array.add("f")
if (hasDairy) array.add("g")
if (hasAlmonds) array.add("h1")
if (hasHazelnuts) array.add("h2")
if (hasWalnuts) array.add("h3")
if (hasCashewNuts) array.add("h4")
if (hasPecans) array.add("h5")
if (hasBrazilNuts) array.add("h6")
if (hasPistachios) array.add("h7")
if (hasMacadamia) array.add("h8")
if (hasCelery) array.add("i")
if (hasMustard) array.add("j")
if (hasSulphides) array.add("k")
if (hasLupins) array.add("l")
if (hasSesame) array.add("m")
if (hasMolluscs) array.add("n")
return array.joinToString(",")
}
}
companion object {
const val C_TRUE: Char = 'y'
const val C_FALSE: Char = 'n'
const val TEMPLATE: String = ""
/**
* Constructs an AllergenPreferenceObject from a string.
*
* @param input string that must be 26 characters long
* @return instance of AllergenPreferences.Object with preferences inserted
*/
fun construct(input: String): Object {
val elements = input.split(",")
return Object(
elements.contains("a1"),
elements.contains("a2"),
elements.contains("a3"),
elements.contains("a4"),
elements.contains("a5"),
elements.contains("a6"),
elements.contains("b"),
elements.contains("c"),
elements.contains("d"),
elements.contains("e"),
elements.contains("f"),
elements.contains("g"),
elements.contains("h1"),
elements.contains("h2"),
elements.contains("h3"),
elements.contains("h4"),
elements.contains("h5"),
elements.contains("h6"),
elements.contains("h7"),
elements.contains("h8"),
elements.contains("i"),
elements.contains("j"),
elements.contains("k"),
elements.contains("l"),
elements.contains("m"),
elements.contains("n"),
)
}
// String(
// charArrayOf(
// charFor(obj.hasWheat),
// charFor(obj.hasRye),
// charFor(obj.hasBarley),
// charFor(obj.hasOats),
// charFor(obj.hasSpelt),
// charFor(obj.hasKamut),
// charFor(obj.hasCrustaceans),
// charFor(obj.hasEggs),
// charFor(obj.hasFish),
// charFor(obj.hasPeanuts),
// charFor(obj.hasSoy),
// charFor(obj.hasDairy),
// charFor(obj.hasAlmonds),
// charFor(obj.hasHazelnuts),
// charFor(obj.hasWalnuts),
// charFor(obj.hasCashewNuts),
// charFor(obj.hasPecans),
// charFor(obj.hasBrazilNuts),
// charFor(obj.hasPistachios),
// charFor(obj.hasMacadamia),
// charFor(obj.hasCelery),
// charFor(obj.hasMustard),
// charFor(obj.hasSulphides),
// charFor(obj.hasLupins),
// charFor(obj.hasSesame),
// charFor(obj.hasMolluscs),
// )
// )
private fun charFor(pref: Boolean) = if (pref) C_TRUE else C_FALSE
}
}
@@ -70,7 +70,7 @@ enum class DietaryPreferences(val value: String) {
/**
* Offer has encountered an error.
*/
ERROR("onError")
ERROR("onError"),
;
/**
@@ -8,6 +8,11 @@ package com.denizk0461.studip.model
*/
enum class SettingsPreferences(val key: String) {
/**
* Which allergens the user wants displayed or hidden.
*/
ALLERGEN_CONFIG("setting_allergen_config"),
/**
* Whether the user wants to have allergens displayed in the canteen overview.
*/
@@ -0,0 +1,106 @@
package com.denizk0461.studip.sheet
import android.os.Bundle
import android.view.View
import com.denizk0461.studip.R
import com.denizk0461.studip.data.viewBinding
import com.denizk0461.studip.databinding.SheetAllergenConfigBinding
import com.denizk0461.studip.model.AllergenPreferences
/**
* Configuration sheet for the user to pick substances they are allergic against. Offers containing
* these allergens will be hidden.
*
* @param currentAllergenConfig currently set allergen preference
* @param onUpdate action to execute to store the allergen preferences
*/
class AllergenConfigSheet(
private val currentAllergenConfig: AllergenPreferences.Object,
private val onUpdate: (obj: AllergenPreferences.Object) -> Unit,
) : AppSheet(R.layout.sheet_allergen_config) {
// View binding
private val binding: SheetAllergenConfigBinding by viewBinding(SheetAllergenConfigBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set all check boxes to the currently set values
binding.apply {
checkWheat.isChecked = currentAllergenConfig.hasWheat
checkRye.isChecked = currentAllergenConfig.hasRye
checkBarley.isChecked = currentAllergenConfig.hasBarley
checkOat.isChecked = currentAllergenConfig.hasOats
checkSpelt.isChecked = currentAllergenConfig.hasSpelt
checkKamut.isChecked = currentAllergenConfig.hasKamut
checkCrustaceans.isChecked = currentAllergenConfig.hasCrustaceans
checkEggs.isChecked = currentAllergenConfig.hasEggs
checkFish.isChecked = currentAllergenConfig.hasFish
checkPeanuts.isChecked = currentAllergenConfig.hasPeanuts
checkSoy.isChecked = currentAllergenConfig.hasSoy
checkDairy.isChecked = currentAllergenConfig.hasDairy
checkAlmonds.isChecked = currentAllergenConfig.hasAlmonds
checkHazelnuts.isChecked = currentAllergenConfig.hasHazelnuts
checkWalnuts.isChecked = currentAllergenConfig.hasWalnuts
checkCashewNuts.isChecked = currentAllergenConfig.hasCashewNuts
checkPecans.isChecked = currentAllergenConfig.hasPecans
checkBrazilNuts.isChecked = currentAllergenConfig.hasBrazilNuts
checkPistachios.isChecked = currentAllergenConfig.hasPistachios
checkMacadamia.isChecked = currentAllergenConfig.hasMacadamia
checkCelery.isChecked = currentAllergenConfig.hasCelery
checkMustard.isChecked = currentAllergenConfig.hasMustard
checkSulphides.isChecked = currentAllergenConfig.hasSulphides
checkLupins.isChecked = currentAllergenConfig.hasLupins
checkSesame.isChecked = currentAllergenConfig.hasSesame
checkMolluscs.isChecked = currentAllergenConfig.hasMolluscs
}
// Set save button
binding.buttonSave.setOnClickListener {
saveAllergenPrefs()
}
}
/**
* Saves the newly set preferences and closes the sheet.
*/
private fun saveAllergenPrefs() {
binding.apply {
// Save the new preferences
onUpdate(
AllergenPreferences.Object(
hasWheat = checkWheat.isChecked,
hasRye = checkRye.isChecked,
hasBarley = checkBarley.isChecked,
hasOats = checkOat.isChecked,
hasSpelt = checkSpelt.isChecked,
hasKamut = checkKamut.isChecked,
hasCrustaceans = checkCrustaceans.isChecked,
hasEggs = checkEggs.isChecked,
hasFish = checkFish.isChecked,
hasPeanuts = checkPeanuts.isChecked,
hasSoy = checkSoy.isChecked,
hasDairy = checkDairy.isChecked,
hasAlmonds = checkAlmonds.isChecked,
hasHazelnuts = checkHazelnuts.isChecked,
hasWalnuts = checkWalnuts.isChecked,
hasCashewNuts = checkCashewNuts.isChecked,
hasPecans = checkPecans.isChecked,
hasBrazilNuts = checkBrazilNuts.isChecked,
hasPistachios = checkPistachios.isChecked,
hasMacadamia = checkMacadamia.isChecked,
hasCelery = checkCelery.isChecked,
hasMustard = checkMustard.isChecked,
hasSulphides = checkSulphides.isChecked,
hasLupins = checkLupins.isChecked,
hasSesame = checkSesame.isChecked,
hasMolluscs = checkMolluscs.isChecked,
)
)
}
// Close the sheet
dismiss()
}
}
@@ -65,6 +65,7 @@ class DevCodeSheet(
"TUFSR0Uh".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS8tbHFxRHZXRjQ1dw==".d64)
"UE9XRUxM".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9XUGMtVkVxQlBISQ==".d64)
"Q0FMTE1F".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS8xbTV1WnJNMnktMA==".d64)
"SkFNSVJP".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9fMVZoT2c0N3ozNA==".d64)
"Qk9KQUNL".d64 -> {
startActivity(Intent(context, ImageActivity::class.java).also { intent ->
val bundle = Bundle()
@@ -2,6 +2,7 @@ package com.denizk0461.studip.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizk0461.studip.model.AllergenPreferences
import com.denizk0461.studip.model.CanteenOffer
import com.denizk0461.studip.model.DietaryPreferences
import com.denizk0461.studip.model.SettingsPreferences
@@ -25,6 +26,14 @@ class CanteenPageViewModel(app: Application) : AppViewModel(app) {
*/
fun getDietaryPrefs(): DietaryPreferences.Object = repo.getDietaryPrefsAsObject()
/**
* This value determines which allergens the user wants to have displayed or hidden.
*/
val preferenceAllergenConfig: String
get() = repo.getStringPreference(
SettingsPreferences.ALLERGEN_CONFIG, defaultValue = AllergenPreferences.TEMPLATE
)
/**
* This value determines whether the user wants to have allergens marked.
*/
@@ -1,6 +1,7 @@
package com.denizk0461.studip.viewmodel
import android.app.Application
import com.denizk0461.studip.model.AllergenPreferences
import com.denizk0461.studip.model.SettingsPreferences
/**
@@ -56,6 +57,15 @@ class SettingsViewModel(app: Application) : AppViewModel(app) {
get() = repo.getBooleanPreference(SettingsPreferences.COURSE_HIGHLIGHTING)
set(newValue) { repo.setPreference(SettingsPreferences.COURSE_HIGHLIGHTING, newValue) }
/**
* This value determines which allergens the user wants to have displayed or hidden.
*/
var preferenceAllergenConfig: String
get() = repo.getStringPreference(
SettingsPreferences.ALLERGEN_CONFIG, defaultValue = AllergenPreferences.TEMPLATE
)
set(newValue) { repo.setPreference(SettingsPreferences.ALLERGEN_CONFIG, newValue) }
/**
* This value determines whether the user wants to have allergens marked.
*/