TabLayoutMediator has been re-instantiated, hopefully in a form that is less crash-prone. Crashes will also be handled now in StwParser.kt

This commit is contained in:
denizk0461
2023-04-29 10:56:52 +02:00
parent 8fb41d6520
commit 2bf340bdff
13 changed files with 158 additions and 214 deletions
@@ -8,10 +8,9 @@ import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.denizk0461.studip.R import com.denizk0461.studip.R
import com.denizk0461.studip.data.getThemedColor import com.denizk0461.studip.data.showErrorSnackBar
import com.denizk0461.studip.databinding.ActivityFetcherBinding import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.viewmodel.FetcherViewModel import com.denizk0461.studip.viewmodel.FetcherViewModel
import com.google.android.material.snackbar.Snackbar
import java.io.IOException import java.io.IOException
import java.net.URLDecoder import java.net.URLDecoder
@@ -90,18 +89,8 @@ class FetcherActivity : Activity() {
// finish() // finish()
// } // }
} catch (e: IOException) { } catch (e: IOException) {
// Let the user know that an error occurred // Let the user know that an error occurred
Snackbar theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_snack))
.make(
binding.rootView,
getString(R.string.fetch_error_snack),
Snackbar.LENGTH_SHORT
)
// Set colours to signify an error
.setBackgroundTint(theme.getThemedColor(R.attr.colorErrorContainer))
.setTextColor(theme.getThemedColor(R.attr.colorOnErrorContainer))
.show()
} }
} }
} }
@@ -5,7 +5,10 @@ import android.content.res.Resources
import android.util.TypedValue import android.util.TypedValue
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.denizk0461.studip.R
import com.denizk0461.studip.exception.AcademicQuarterNotApplicableException import com.denizk0461.studip.exception.AcademicQuarterNotApplicableException
import com.google.android.material.snackbar.Snackbar
import kotlin.jvm.Throws import kotlin.jvm.Throws
/** /**
@@ -42,6 +45,15 @@ fun showToast(context: Context, text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show() Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
} }
fun Resources.Theme.showErrorSnackBar(view: CoordinatorLayout, text: String) {
Snackbar
.make(view, text, Snackbar.LENGTH_SHORT)
// Set colours to signify an error
.setBackgroundTint(getThemedColor(R.attr.colorErrorContainer))
.setTextColor(getThemedColor(R.attr.colorOnErrorContainer))
.show()
}
/** /**
* Provides a conversion method between a timestamp ending in a full hour, and one with an academic * Provides a conversion method between a timestamp ending in a full hour, and one with an academic
* quarter applied. * quarter applied.
@@ -3,9 +3,12 @@ package com.denizk0461.studip.data
import android.app.Application import android.app.Application
import com.denizk0461.studip.db.AppRepository import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.* import com.denizk0461.studip.model.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import kotlin.jvm.Throws
/** /**
* Parser class used for fetching and collecting canteen offers from the website of the * Parser class used for fetching and collecting canteen offers from the website of the
@@ -36,11 +39,14 @@ class StwParser(application: Application) {
/** /**
* Parses through a list of canteen plans and saves them to persistent storage. * Parses through a list of canteen plans and saves them to persistent storage.
* *
* @param onRefreshUpdate action call for when an update on the status of the fetch is
* available
* @param onFinish action call for when the fetch has finished * @param onFinish action call for when the fetch has finished
* @param onError action call for when an error has occurred
*/ */
fun parse(canteen: Int, onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) { suspend fun parse(
canteen: Int,
onFinish: () -> Unit,
onError: () -> Unit
) {
/* /*
* Reset primary key values. This needs to be done in case this method is called multiple * Reset primary key values. This needs to be done in case this method is called multiple
* times within the app's lifespan * times within the app's lifespan
@@ -65,12 +71,18 @@ class StwParser(application: Application) {
else -> urlUniMensa else -> urlUniMensa
} }
// Delete all previous entries and start afresh /*
repo.nukeOffers() * Handle the fetch operation in an encompassing try-catch to prevent the app from crashing.
* User will be notified if an error occurs.
*/
try {
// Fetch offers from the chosen canteen // Fetch offers from the chosen canteen
val (dates, canteens, categories, items) = parseFromPage(link) val (dates, canteens, categories, items) = parseFromPage(link)
// Delete all previous entries and start afresh
repo.nukeOffers()
// Save everything all at once into the database // Save everything all at once into the database
with(repo) { with(repo) {
insertDates(dates) insertDates(dates)
@@ -78,17 +90,28 @@ class StwParser(application: Application) {
insertCategories(categories) insertCategories(categories)
insertItems(items) insertItems(items)
} }
// TODO implement onRefresh(Int)
// Action call once all fetching activities have finished // Call action to let the user know the fetch has finished
onFinish() onFinish()
} catch (e: RuntimeException) {
/*
* Call action to let the user know an error occurred. Do this on the main thread to
* access UI.
*/
withContext(Dispatchers.Main) {
onError()
}
}
} }
/** /**
* Fetch and parse the plan of a specific canteen. * Fetch and parse the plan of a specific canteen.
* *
* @param url link to the canteen to be scraped. Must be a subpage of stw-bremen.de * @param url link to the canteen to be scraped. Must be a subpage of stw-bremen.de
* @throws RuntimeException if something goes wrong during the fetch
*/ */
@Throws(RuntimeException::class)
private fun parseFromPage(url: String): StwResults { private fun parseFromPage(url: String): StwResults {
val dates = mutableListOf<OfferDate>() val dates = mutableListOf<OfferDate>()
@@ -189,25 +212,34 @@ class StwParser(application: Application) {
// Retrieve the parent element holding the item's title and price // Retrieve the parent element holding the item's title and price
val tableRows = element.getElementsByTag("td") val tableRows = element.getElementsByTag("td")
// Retrieve the dietary preferences the item meets /*
val prefs = element * Retrieve the dietary preferences the item meets and parse them into a
* string that can be inserted into the database.
*/
val prefs = try {
with(element
.getElementsByClass("field field-name-field-food-types")[0] .getElementsByClass("field field-name-field-food-types")[0]
) {
DietaryPreferences.Object(
isFair = isDietaryPreferenceMet(imageLinkPrefFair),
isFish = isDietaryPreferenceMet(imageLinkPrefFish),
isPoultry = isDietaryPreferenceMet(imageLinkPrefPoultry),
isLamb = isDietaryPreferenceMet(imageLinkPrefLamb),
isVital = isDietaryPreferenceMet(imageLinkPrefVital),
isBeef = isDietaryPreferenceMet(imageLinkPrefBeef),
isPork = isDietaryPreferenceMet(imageLinkPrefPork),
isVegan = isDietaryPreferenceMet(imageLinkPrefVegan),
isVegetarian = isDietaryPreferenceMet(imageLinkPrefVegetarian),
isGame = isDietaryPreferenceMet(imageLinkPrefGame),
)
}
} catch (e: IndexOutOfBoundsException) {
DietaryPreferences.NONE_MET
}
// Parse the preferences into a string for the database val filteredText = getOrElse(orElse = Pair("", "")) {
val prefString = DietaryPreferences.Object( tableRows[1].getFilteredText()
isFair = prefs.isDietaryPreferenceMet(imageLinkPrefFair), }
isFish = prefs.isDietaryPreferenceMet(imageLinkPrefFish),
isPoultry = prefs.isDietaryPreferenceMet(imageLinkPrefPoultry),
isLamb = prefs.isDietaryPreferenceMet(imageLinkPrefLamb),
isVital = prefs.isDietaryPreferenceMet(imageLinkPrefVital),
isBeef = prefs.isDietaryPreferenceMet(imageLinkPrefBeef),
isPork = prefs.isDietaryPreferenceMet(imageLinkPrefPork),
isVegan = prefs.isDietaryPreferenceMet(imageLinkPrefVegan),
isVegetarian = prefs.isDietaryPreferenceMet(imageLinkPrefVegetarian),
isGame = prefs.isDietaryPreferenceMet(imageLinkPrefGame),
).deconstruct()
val filteredText = tableRows[1].getFilteredText()
// Save the item to its list // Save the item to its list
items.add( items.add(
@@ -216,7 +248,7 @@ class StwParser(application: Application) {
categoryId, categoryId,
title = filteredText.first, title = filteredText.first,
price = tableRows.getTextOrEmpty(2), price = tableRows.getTextOrEmpty(2),
dietaryPreferences = prefString, dietaryPreferences = prefs.deconstruct(),
allergens = filteredText.second, allergens = filteredText.second,
) )
) )
@@ -300,7 +332,6 @@ class StwParser(application: Application) {
/* /*
* The Studierendenwerk's website lists allergens, but they are invisible, rendering them * The Studierendenwerk's website lists allergens, but they are invisible, rendering them
* pointless to the website user. As of now, this information is discarded in the app. * pointless to the website user. As of now, this information is discarded in the app.
* TODO implement allergen functionality
*/ */
while (text.contains("<sup>")) { while (text.contains("<sup>")) {
@@ -367,6 +398,12 @@ class StwParser(application: Application) {
else -> "00" // shouldn't occur else -> "00" // shouldn't occur
} }
private fun <T> getOrElse(orElse: T, action: () -> T): T = try {
action()
} catch (e: Exception) {
orElse
}
/** /**
* Retrieves a text and adds <br> tags with line breaks. * Retrieves a text and adds <br> tags with line breaks.
* *
@@ -407,7 +444,7 @@ class StwParser(application: Application) {
private val urlGW2 = "https://www.stw-bremen.de/de/cafeteria/gw2" private val urlGW2 = "https://www.stw-bremen.de/de/cafeteria/gw2"
// private val urlGraz = "" // No link since there are only snacks on offer that are not listed online // private val urlGraz = "" // No link since there are only snacks on offer that are not listed online
private val urlHSBNeustadt = "https://www.stw-bremen.de/de/mensa/neustadtswall" private val urlHSBNeustadt = "https://www.stw-bremen.de/de/mensa/neustadtswall"
private val urlHSBWerder = "https://www.stw-bremen.de/de/mensa/werderstra%C3%9Fe" private val urlHSBWerder = "https://www.stw-bremen.de/de/mensa/werderstraße"
private val urlHSBAirport = "https://www.stw-bremen.de/de/mensa/airport" private val urlHSBAirport = "https://www.stw-bremen.de/de/mensa/airport"
private val urlHfK = "https://www.stw-bremen.de/de/mensa/interimsmensa-hfk" private val urlHfK = "https://www.stw-bremen.de/de/mensa/interimsmensa-hfk"
private val urlMensaBHV = "https://www.stw-bremen.de/de/mensa/bremerhaven" private val urlMensaBHV = "https://www.stw-bremen.de/de/mensa/bremerhaven"
@@ -67,20 +67,6 @@ interface AppDAO {
/* --- canteen offers --- */ /* --- canteen offers --- */
/**
* Retrieves all canteen offers. Objects will be joined through their primary/foreign keys from
* instances of OfferDate.kt, OfferCanteen.kt, OfferCategory.kt, and OfferItem.kt.
*
* @return all canteen offers exposed through a LiveData object
*/
@get:Query(
"SELECT * FROM offer_item " +
"JOIN offer_category ON offer_item.categoryId = offer_category.id " +
"JOIN offer_canteen ON offer_category.canteenId = offer_canteen.id " +
"JOIN offer_date ON offer_category.dateId = offer_date.id"
)
val allOffers: LiveData<List<CanteenOffer>>
/** /**
* Retrieves all canteen offers that match given dietary preferences. Objects will be joined * Retrieves all canteen offers that match given dietary preferences. Objects will be joined
* through their primary/foreign keys from instances of OfferDate.kt, OfferCanteen.kt, * through their primary/foreign keys from instances of OfferDate.kt, OfferCanteen.kt,
@@ -113,21 +99,13 @@ interface AppDAO {
) )
fun getOffersByDay(day: Int): LiveData<List<CanteenOffer>> fun getOffersByDay(day: Int): LiveData<List<CanteenOffer>>
/**
* Retrieves the amount of dates represented in the offers stored locally. Can be observed.
*
* @return date count as LiveData
*/
@Query("SELECT COUNT(*) FROM offer_date")
fun getDateCount(): LiveData<Int>
/** /**
* Retrieves all canteen offer date objects. * Retrieves all canteen offer date objects.
* *
* @return a list of instances of canteen offer dates * @return a list of instances of canteen offer dates
*/ */
@Query("SELECT * FROM offer_date ORDER BY id") @Query("SELECT * FROM offer_date ORDER BY id")
fun getDates(): List<OfferDate> fun getDates(): LiveData<List<OfferDate>>
/** /**
* Deletes all dates from the database. * Deletes all dates from the database.
@@ -78,13 +78,6 @@ class AppRepository(app: Application) {
*/ */
fun getCanteenOpeningHours(): String = dao.getCanteenOpeningHours() fun getCanteenOpeningHours(): String = dao.getCanteenOpeningHours()
/**
* Retrieves all canteen offers.
*
* @return all canteen offers exposed through a LiveData object
*/
val allOffers: LiveData<List<CanteenOffer>> = dao.allOffers
/** /**
* Retrieves all canteen offers that match given day. * Retrieves all canteen offers that match given day.
* *
@@ -92,13 +85,6 @@ class AppRepository(app: Application) {
*/ */
fun getOffersByDay(day: Int): LiveData<List<CanteenOffer>> = dao.getOffersByDay(day) fun getOffersByDay(day: Int): LiveData<List<CanteenOffer>> = dao.getOffersByDay(day)
/**
* Retrieves the amount of dates represented in the offers stored locally. Can be observed.
*
* @return date count as LiveData
*/
fun getDateCount(): LiveData<Int> = dao.getDateCount()
/** /**
* Updates a schedule element. * Updates a schedule element.
* *
@@ -122,7 +108,7 @@ class AppRepository(app: Application) {
* *
* @return a list of instances of canteen offer dates * @return a list of instances of canteen offer dates
*/ */
fun getDates(): List<OfferDate> = dao.getDates() fun getDates(): LiveData<List<OfferDate>> = dao.getDates()
/** /**
* Retrieves the regex string used to determine the user's dietary preferences, or, if no * Retrieves the regex string used to determine the user's dietary preferences, or, if no
@@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.denizk0461.studip.R import com.denizk0461.studip.R
import com.denizk0461.studip.adapter.CanteenOfferPageAdapter import com.denizk0461.studip.adapter.CanteenOfferPageAdapter
import com.denizk0461.studip.data.showErrorSnackBar
import com.denizk0461.studip.databinding.FragmentCanteenBinding import com.denizk0461.studip.databinding.FragmentCanteenBinding
import com.denizk0461.studip.model.* import com.denizk0461.studip.model.*
import com.denizk0461.studip.sheet.TextSheet import com.denizk0461.studip.sheet.TextSheet
@@ -37,17 +38,10 @@ class CanteenFragment : AppFragment() {
// View model reference for providing access to the database // View model reference for providing access to the database
private val viewModel: CanteenViewModel by viewModels() private val viewModel: CanteenViewModel by viewModels()
// Elements in the canteen plan
private var elements: List<CanteenOffer> = listOf()
// Count of days displayed in the canteen plan
private var dateSize: Int = 0
// Used if no preference has been set or none can be found
private val emptyPreferenceRegex: String = ".........."
private var openingHours = "" private var openingHours = ""
private var dates: List<String> = listOf()
// Instantiate the view binding // Instantiate the view binding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCanteenBinding.inflate(inflater, container, false) _binding = FragmentCanteenBinding.inflate(inflater, container, false)
@@ -78,10 +72,10 @@ class CanteenFragment : AppFragment() {
binding.buttonCanteenPicker.text = getCurrentlySelectedCanteenName() binding.buttonCanteenPicker.text = getCurrentlySelectedCanteenName()
// Display to the user that the canteen plan will be refreshed // Display to the user that the canteen plan will be refreshed
binding.swipeRefreshLayout.isRefreshing = true // binding.swipeRefreshLayout.isRefreshing = true
// Refresh the canteen menu for the newly selected canteen // Refresh the canteen menu for the newly selected canteen
// refresh() refresh()
true true
} }
inflate(R.menu.menu_canteens) inflate(R.menu.menu_canteens)
@@ -92,6 +86,7 @@ class CanteenFragment : AppFragment() {
// Set currently selected canteen to the button // Set currently selected canteen to the button
binding.buttonCanteenPicker.text = getCurrentlySelectedCanteenName() binding.buttonCanteenPicker.text = getCurrentlySelectedCanteenName()
// Set up button to display info (most likely opening hours)
binding.buttonInfo.setOnClickListener { binding.buttonInfo.setOnClickListener {
openBottomSheet( openBottomSheet(
TextSheet( TextSheet(
@@ -104,7 +99,6 @@ class CanteenFragment : AppFragment() {
) )
} }
// Assign a preference value to every button to filter for dietary preferences // Assign a preference value to every button to filter for dietary preferences
val chipMap = mapOf( val chipMap = mapOf(
binding.chipPrefFair to DietaryPreferences.WELFARE, binding.chipPrefFair to DietaryPreferences.WELFARE,
@@ -134,8 +128,7 @@ class CanteenFragment : AppFragment() {
* it removes the view before immediately adding it back, this method causes a * it removes the view before immediately adding it back, this method causes a
* flicker. * flicker.
* TODO implement a more efficient / better-looking method * TODO implement a more efficient / better-looking method
* TODO order the chips alphabetically * TODO order the chips alphabetically?
* TODO the refreshing ability stopped working
*/ */
val index = binding.chipsPreference.indexOfChild(buttonView) val index = binding.chipsPreference.indexOfChild(buttonView)
binding.chipsPreference.removeView(buttonView) binding.chipsPreference.removeView(buttonView)
@@ -144,43 +137,25 @@ class CanteenFragment : AppFragment() {
} }
// Set up the view pager's adapter // Set up the view pager's adapter
viewPagerAdapter = CanteenOfferPageAdapter( viewPagerAdapter = CanteenOfferPageAdapter(activity as FragmentActivity)
activity as FragmentActivity,
)
viewModel.getDateCount().observe(viewLifecycleOwner) { dateCount -> // Date count is observed to correctly set the amount of pages and the corresponding tabs
viewPagerAdapter.itemCount = dateCount viewModel.getDates().observe(viewLifecycleOwner) { newDates ->
binding.swipeRefreshLayout.isRefreshing = false
// Retrieve the dates individually and store them for the tab mediator to use
dates = newDates.map { it.date }
// Let the adapter know of the new amount of dates (pages) to display
viewPagerAdapter.itemCount = dates.size
} }
// Assign the adapter to the view pager // Assign the adapter to the view pager
binding.viewPager.adapter = viewPagerAdapter binding.viewPager.adapter = viewPagerAdapter
// Set up LiveData observer to refresh the view on update // Set up TabLayoutMediator to populate tabs
// viewModel.allOffers.observe(viewLifecycleOwner) { offers -> TabLayoutMediator(binding.dayTabLayout, binding.viewPager) { tab, position ->
// // Update the element list stored in this fragment tab.text = dates[position]
// elements = offers }.attach()
//
// // Group elements by category
// val groupedElements = offers.groupElements().distinct()
//
// // Find all dates for which items are available
// val newDates = groupedElements.map { it.date }.distinct()
//
// // Update the date count stored in this fragment
// dateSize = newDates.size
//
// // Update the item list in the view pager's adapter
//// viewPagerAdapter.setNewItems(groupedElements, dateSize)
//
// // Set the text for the opening hours dialogue
// openingHours = viewModel.getCanteenOpeningHours()
//
// binding.swipeRefreshLayout.isRefreshing = false
//
// // Create and attach the tab layout for the ViewPager
// createTabLayoutMediator(newDates)
// }
// Set up functions for when the user swipes to refresh the view // Set up functions for when the user swipes to refresh the view
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
@@ -188,23 +163,6 @@ class CanteenFragment : AppFragment() {
} }
} }
/**
* Create and attach the object mediating the tabs for the view pager.
*
* TODO this is error-prone, likes to crash with an IndexOutOfBoundsException, and I have no
* fucking idea why
*
* @param dates titles for the individual tabs
*/
private fun createTabLayoutMediator(dates: List<String>) {
// Only attach the mediator if the list has items, otherwise an error would occur
if (dates.isNotEmpty()) {
TabLayoutMediator(binding.dayTabLayout, binding.viewPager) { tab, position ->
tab.text = dates[position]
}.attach()
}
}
// Invalidate the view binding // Invalidate the view binding
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
@@ -223,7 +181,6 @@ class CanteenFragment : AppFragment() {
// Update the view // Update the view
viewModel.registerDietaryPreferencesUpdate() viewModel.registerDietaryPreferencesUpdate()
// viewPagerAdapter.setNewItems(elements.groupElements().distinct(), dateSize)
} }
/** /**
@@ -258,38 +215,21 @@ class CanteenFragment : AppFragment() {
/** /**
* Downloads the canteen offers and refreshes them in the app. * Downloads the canteen offers and refreshes them in the app.
* TODO handle SocketTimeoutException
*/ */
private fun refresh() { private fun refresh() {
// Retrieve new offers from the website(s) // Retrieve new offers from the website(s)
viewModel.fetchOffers(viewModel.preferenceCanteen, onRefreshUpdate = { status -> viewModel.fetchOffers(viewModel.preferenceCanteen, onFinish = {
// TODO refresh updates
}, onFinish = {
/* /*
* Tell the swipe refresh layout to stop refreshing. * Tell the swipe refresh layout to stop refreshing.
* TODO if an exception is raised, this will not be fired. This must be changed
*/ */
// binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
// createTabLayoutMediator()
}, onError = {
context?.theme?.showErrorSnackBar(
binding.snackbarContainer,
getString(R.string.canteen_fetch_error)
)
binding.swipeRefreshLayout.isRefreshing = false
}) })
} }
/**
* Executed when an item has been clicked.
*
* @param offer item that has been clicked
*/
// override fun onClick(offer: CanteenOfferGroupElement, category: String) {
// openBottomSheet(AllergenSheet(offer, category))
// }
//
// /**
// * Executed when an item has been long-pressed.
// *
// * @param offer item that has been long-pressed
// * @return whether the long press was successful
// */
// override fun onLongClick(offer: CanteenOfferGroupElement): Boolean {
// return false
// }
} }
@@ -139,6 +139,8 @@ enum class DietaryPreferences(val value: String) {
const val TEMPLATE_EMPTY: String = ".........." const val TEMPLATE_EMPTY: String = ".........."
val NONE_MET: Object = construct(TEMPLATE_EMPTY)
/** /**
* Constructs a DietaryPrefObject from a regex string. String must be 10 characters long. * Constructs a DietaryPrefObject from a regex string. String must be 10 characters long.
* A char equal to C_TRUE is treated as a true boolean value. Any other char is treated as a * A char equal to C_TRUE is treated as a true boolean value. Any other char is treated as a
@@ -44,11 +44,11 @@ class DevCodeSheet(
* *
* About the inks; they are safe (which is suspicious of me to say), just encoded so * About the inks; they are safe (which is suspicious of me to say), just encoded so
* people can't just look at the file and figure out all the codes! Of course, they're * people can't just look at the file and figure out all the codes! Of course, they're
* easily decoded, but who would go for that? You would have to be a pretty big nerd to * easily decoded, but who would go for that? You would have to have a lot of time to
* do that. Then again, I was the one who encoded all of these links in the first place, * waste to do that. Then again, I was the one who encoded all of these links in the
* so I suppose the joke's on me... * first place, so I suppose the joke's on me...
* *
* If you're reading this why? * If you're reading this, I ask you why?
*/ */
when (binding.input.text.toString().uppercase()) { when (binding.input.text.toString().uppercase()) {
"U1RST0JF".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS90S2k5Wi1mNnFYNA==".d64) "U1RST0JF".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS90S2k5Wi1mNnFYNA==".d64)
@@ -59,6 +59,9 @@ class DevCodeSheet(
"SE9ORVNU".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9DSEg0OE5LUEFraw==".d64) "SE9ORVNU".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9DSEg0OE5LUEFraw==".d64)
"QkJVT0s/".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS90Rm1PMm1TY0tHNA==".d64) "QkJVT0s/".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS90Rm1PMm1TY0tHNA==".d64)
"SUhUU0Mq".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS85bXZ4SVdhWHZuWQ==".d64) "SUhUU0Mq".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS85bXZ4SVdhWHZuWQ==".d64)
"SU5TQU5F".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS90NE9kYTlUZFYwbw==".d64)
"X0RSV05f".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9SUVpUUy1CWjhtcw==".d64)
"TUFSR0Uh".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS8tbHFxRHZXRjQ1dw==".d64)
"NEVENT" -> { // nuke events "NEVENT" -> { // nuke events
nukeEvents() nukeEvents()
showToast(context, "Nuked all events") showToast(context, "Nuked all events")
@@ -2,8 +2,11 @@ package com.denizk0461.studip.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.denizk0461.studip.data.StwParser import com.denizk0461.studip.data.StwParser
import com.denizk0461.studip.model.* import com.denizk0461.studip.model.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/** /**
* View model for [com.denizk0461.studip.fragment.CanteenFragment] * View model for [com.denizk0461.studip.fragment.CanteenFragment]
@@ -15,21 +18,12 @@ class CanteenViewModel(app: Application) : AppViewModel(app) {
// Instantiate a parser, should the user want to refresh the canteen offers // Instantiate a parser, should the user want to refresh the canteen offers
private val parser = StwParser(app) private val parser = StwParser(app)
/**
* Retrieves all Stud.IP events.
*
* @return all Stud.IP events exposed through a LiveData object
*/
val allOffers: LiveData<List<CanteenOffer>> = repo.allOffers
/** /**
* Retrieves all canteen offer date objects. * Retrieves all canteen offer date objects.
* *
* @return a list of instances of canteen offer dates * @return a list of instances of canteen offer dates
*/ */
fun getDates(): List<OfferDate> = returnBlocking { repo.getDates() } fun getDates(): LiveData<List<OfferDate>> = repo.getDates()
/** /**
* Retrieves the opening hours of the canteen as a string. * Retrieves the opening hours of the canteen as a string.
@@ -42,24 +36,23 @@ class CanteenViewModel(app: Application) : AppViewModel(app) {
* Fetch the canteen offers asynchronously. Updates will be provided through a LiveData object. * Fetch the canteen offers asynchronously. Updates will be provided through a LiveData object.
* *
* @param canteen canteen from which to fetch offers from * @param canteen canteen from which to fetch offers from
* @param onRefreshUpdate action to execute when a status update is available
* @param onFinish action to execute when the operation has finished * @param onFinish action to execute when the operation has finished
* @param onError action to execute when the operation encountered an error
*/ */
fun fetchOffers(canteen: Int, onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) { fun fetchOffers(
doAsync { parser.parse(canteen, onRefreshUpdate, onFinish) } canteen: Int,
onFinish: () -> Unit,
onError: () -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
parser.parse(canteen, onFinish, onError)
}
} }
fun registerDietaryPreferencesUpdate() { fun registerDietaryPreferencesUpdate() {
repo.dietaryPreferencesUpdate.postValue(repo.dietaryPreferencesUpdate.value?.plus(1)) repo.dietaryPreferencesUpdate.postValue(repo.dietaryPreferencesUpdate.value?.plus(1))
} }
/**
* Retrieves the amount of dates represented in the offers stored locally. Can be observed.
*
* @return date count as LiveData
*/
fun getDateCount(): LiveData<Int> = repo.getDateCount()
/** /**
* Updates a given dietary preference to a new value. * Updates a given dietary preference to a new value.
* *
@@ -78,12 +71,6 @@ class CanteenViewModel(app: Application) : AppViewModel(app) {
*/ */
fun getPreference(pref: DietaryPreferences): Boolean = repo.getBooleanPreference(pref) fun getPreference(pref: DietaryPreferences): Boolean = repo.getBooleanPreference(pref)
/**
* This value determines whether the user wants to have allergens marked.
*/
val preferenceAllergen: Boolean
get() = repo.getBooleanPreference(SettingsPreferences.ALLERGEN, defaultValue = true)
/** /**
* This value determines which canteen the user has selected. * This value determines which canteen the user has selected.
*/ */
@@ -76,6 +76,14 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/scroll_view_chip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/scroll_view_chip" android:id="@+id/scroll_view_chip"
android:layout_width="0dp" android:layout_width="0dp"
@@ -417,7 +417,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:text="@string/with_love" android:text="@string/with_love"
android:textSize="10sp" android:textSize="8sp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:textAlignment="center" android:textAlignment="center"
+1
View File
@@ -20,6 +20,7 @@
</string-array> </string-array>
<string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string> <string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string>
<string name="canteen_fetch_error">Mensaplan konnte nicht heruntergeladen werden!</string>
<string name="save">Speichern</string> <string name="save">Speichern</string>
<string name="delete">Löschen</string> <string name="delete">Löschen</string>
+1
View File
@@ -20,6 +20,7 @@
</string-array> </string-array>
<string name="fetch_error_snack">Something went wrong!</string> <string name="fetch_error_snack">Something went wrong!</string>
<string name="canteen_fetch_error">Couldn\'t retrieve the canteen plan!</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>