Archived
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:
@@ -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,30 +71,47 @@ 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)
|
||||||
insertCanteens(canteens)
|
insertCanteens(canteens)
|
||||||
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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user