Further error handling. Also given the user the option to colour dietary preference icons. Colours have partially been derived from those used on stw-bremen.de but have all been processed through the theme builder on m3.material.io

This commit is contained in:
denizk0461
2023-04-29 22:26:26 +02:00
parent 2bf340bdff
commit 7243df0f06
21 changed files with 449 additions and 114 deletions
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import com.denizk0461.studip.R
import com.denizk0461.studip.data.showErrorSnackBar
@@ -60,38 +61,37 @@ class FetcherActivity : Activity() {
binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes")
binding.fab.setOnClickListener { view ->
/*
* Retrieve encoded HTML via JavaScript function. Encoding is done as otherwise not all
* symbols will be accurately fetched.
*/
binding.webview.evaluateJavascript(
"(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();"
) { p0 ->
try {
// Decode HTML and parse it into a list of StudIPEvent.kt
// TODO this exception handling and finish() is broken
viewModel.parse(URLDecoder.decode(p0, "UTF-8"))
// StudIPParser(application).parse(URLDecoder.decode(p0, "UTF-8")) { events ->
//
// // Delete all previously fetched elements
//// viewModel.nukeEvents()
//// viewModel.replaceEvents(events)
//
// // Insert the list into the database
//// viewModel.insertEvents(events)
//
// // Notify the user that the fetch was successful
// Toast.makeText(
// this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
// ).show()
//
// // Close the activity
// finish()
// }
} catch (e: IOException) {
// Let the user know that an error occurred
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_snack))
if (binding.webview.url?.contains(
"https://elearning.uni-bremen.de/dispatch.php/calendar/schedule"
) == true) {
/*
* Retrieve encoded HTML via JavaScript function. Encoding is done as otherwise not all
* symbols will be accurately fetched.
*/
binding.webview.evaluateJavascript(
"(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();"
) { p0 ->
try {
// Decode HTML and parse it into a list of StudIPEvent.kt
// TODO add a tutorial for the user to know what to do
viewModel.parse(URLDecoder.decode(p0, "UTF-8"))
// Notify the user that the fetch was successful
Toast.makeText(
this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
).show()
// Close the activity
finish()
} catch (e: IOException) {
// Let the user know that an error occurred
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_snack))
}
}
} else {
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_webpage_snack))
}
}
}
@@ -1,5 +1,6 @@
package com.denizk0461.studip.adapter
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
@@ -7,6 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.denizk0461.studip.R
import com.denizk0461.studip.data.AppDiffUtilCallback
import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.databinding.ItemCanteenBinding
import com.denizk0461.studip.databinding.ItemCanteenLineBinding
import com.denizk0461.studip.databinding.ItemIconBinding
@@ -19,10 +21,12 @@ import com.denizk0461.studip.model.DietaryPreferences
*
* @param onClickListener used for listening to clicks and long presses
* @param displayAllergens whether the user wants allergens to be marked
* @param displayColours whether the user wants dietary preferences to be marked with colours
*/
class CanteenOfferItemAdapter(
private val onClickListener: OnClickListener,
private val displayAllergens: Boolean,
private val displayColours: Boolean,
) : RecyclerView.Adapter<CanteenOfferItemAdapter.OfferViewHolder>() {
private val offers: MutableList<CanteenOfferGroup> = mutableListOf()
@@ -79,7 +83,8 @@ class CanteenOfferItemAdapter(
// Inflate a new icon holder
val img = ItemIconBinding.inflate(LayoutInflater.from(holder.binding.root.context))
val (_, drawableId) = DietaryPreferences.getData(DietaryPreferences.ERROR.ordinal)
// Retrieve data for the dietary preference
val (_, drawableId, colourId) = DietaryPreferences.getData(DietaryPreferences.ERROR.ordinal)
// Set a cross icon
img.imageView.setImageDrawable(
@@ -89,6 +94,17 @@ class CanteenOfferItemAdapter(
)
)
// Set tint of the icon; themed to the preference, if the user has the option enabled
img.imageView.imageTintList = ColorStateList.valueOf(
holder.binding.root.context.theme.getThemedColor(
if (displayColours) {
colourId
} else {
R.attr.colorText
}
)
)
// Bind the new icon to the line view
line.imageViewContainer.addView(img.root)
@@ -130,7 +146,8 @@ class CanteenOfferItemAdapter(
// Inflate a new icon holder
val img = ItemIconBinding.inflate(LayoutInflater.from(holder.binding.root.context))
val (_, drawableId) = DietaryPreferences.getData(index)
// Retrieve data for the dietary preference
val (_, drawableId, colourId) = DietaryPreferences.getData(index)
// Set the appropriate icon
img.imageView.setImageDrawable(
@@ -140,6 +157,17 @@ class CanteenOfferItemAdapter(
)
)
// Set tint of the icon
img.imageView.imageTintList = ColorStateList.valueOf(
holder.binding.root.context.theme.getThemedColor(
if (displayColours) {
colourId
} else {
R.attr.colorText
}
)
)
// Bind the new icon to the line view
line.imageViewContainer.addView(img.root)
}
@@ -5,6 +5,7 @@ import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.StudIPEvent
import org.jsoup.Jsoup
import java.io.IOException
import kotlin.jvm.Throws
/**
* Parser class used for fetching and collecting scheduled events from a Stud.IP timetable.
@@ -17,11 +18,10 @@ class StudIPParser(application: Application) {
* Parse a given HTML string. HTML must be of a Stud.IP timetable.
*
* @param html website content of the Stud.IP timetable
* @param insert action call to save the contents to persistent storage
* @throws IOException when an error in fetching or the database transaction occurs
*/
@kotlin.jvm.Throws(IOException::class)
fun parse(html: String, insert: (events: List<StudIPEvent>) -> Unit) {
@Throws(IOException::class)
fun parse(html: String) {
// Primary key value to uniquely identify entries in the database
var id = 0
@@ -72,7 +72,7 @@ class CanteenFragment : AppFragment() {
binding.buttonCanteenPicker.text = getCurrentlySelectedCanteenName()
// 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()
@@ -54,6 +54,7 @@ class CanteenPageFragment(
recyclerViewAdapter = CanteenOfferItemAdapter(
this,
viewModel.preferenceAllergen,
viewModel.preferenceColour,
)
binding.recyclerView.apply {
@@ -62,13 +63,6 @@ class CanteenPageFragment(
context, LinearLayoutManager.VERTICAL, false
)
/*
* Create new adapter for every page. Attribute position denotes day that will be set up
* by the newly created adapter (0 = Monday, 4 = Friday)
* TODO check if the filtered list is empty, and tell the user if it is
*/
// val o = offers.filter { it.dateId == currentDay }
adapter = recyclerViewAdapter
// Animate creation of new page
@@ -78,7 +72,6 @@ class CanteenPageFragment(
offerList.clear()
offerList.addAll(offers)
recyclerViewAdapter.setNewData(offers.filterElements().groupElements().distinct())
// filterList()
}
}
@@ -192,7 +185,7 @@ class CanteenPageFragment(
* @param offer item that has been clicked
*/
override fun onClick(offer: CanteenOfferGroupElement, category: String) {
openBottomSheet(AllergenSheet(offer, category))
openBottomSheet(AllergenSheet(offer, category, viewModel.preferenceColour))
}
/**
@@ -79,6 +79,14 @@ class SettingsFragment : AppFragment() {
}
}
// Set up switch for colouring dietary preferences
binding.switchPrefColour.apply {
isChecked = viewModel.preferenceColour
setOnCheckedChangeListener { _, newValue ->
viewModel.preferenceColour = newValue
}
}
// Set up switch for crash report opt-in
binding.switchCrashlytics.apply {
isChecked = viewModel.preferenceDataHandling
@@ -167,21 +167,21 @@ enum class DietaryPreferences(val value: String) {
* string and drawable values.
*
* @param index ordinal of the enum item
* @return string resource ID and drawable resource ID as a pair
* @return string resource ID, drawable resource ID, colour attribute ID
*/
fun getData(index: Int): Pair<Int, Int> = when (index) {
WELFARE.ordinal -> Pair(R.string.pref_fair, R.drawable.handshake)
FISH.ordinal -> Pair(R.string.pref_fish, R.drawable.fish)
POULTRY.ordinal -> Pair(R.string.pref_poultry, R.drawable.chicken)
LAMB.ordinal -> Pair(R.string.pref_lamb, R.drawable.sheep)
VITAL.ordinal -> Pair(R.string.pref_vital, R.drawable.yoga)
BEEF.ordinal -> Pair(R.string.pref_beef, R.drawable.cow)
PORK.ordinal -> Pair(R.string.pref_pork, R.drawable.pig)
VEGAN.ordinal -> Pair(R.string.pref_vegan, R.drawable.leaf)
VEGETARIAN.ordinal -> Pair(R.string.pref_vegetarian, R.drawable.carrot)
GAME.ordinal -> Pair(R.string.pref_game, R.drawable.deer)
NONE.ordinal -> Pair(R.string.question_mark, R.drawable.circle)
else -> Pair(R.string.question_mark, R.drawable.cross) // ERROR.ordinal
fun getData(index: Int): Triple<Int, Int, Int> = when (index) {
WELFARE.ordinal -> Triple(R.string.pref_fair, R.drawable.handshake, R.attr.colorPrimaryFair)
FISH.ordinal -> Triple(R.string.pref_fish, R.drawable.fish, R.attr.colorPrimaryFish)
POULTRY.ordinal -> Triple(R.string.pref_poultry, R.drawable.chicken, R.attr.colorPrimaryPoultry)
LAMB.ordinal -> Triple(R.string.pref_lamb, R.drawable.sheep, R.attr.colorPrimaryLamb)
VITAL.ordinal -> Triple(R.string.pref_vital, R.drawable.yoga, R.attr.colorPrimaryVital)
BEEF.ordinal -> Triple(R.string.pref_beef, R.drawable.cow, R.attr.colorPrimaryBeef)
PORK.ordinal -> Triple(R.string.pref_pork, R.drawable.pig, R.attr.colorPrimaryPork)
VEGAN.ordinal -> Triple(R.string.pref_vegan, R.drawable.leaf, R.attr.colorPrimaryVegan)
VEGETARIAN.ordinal -> Triple(R.string.pref_vegetarian, R.drawable.carrot, R.attr.colorPrimaryVegetarian)
GAME.ordinal -> Triple(R.string.pref_game, R.drawable.deer, R.attr.colorPrimaryGame)
NONE.ordinal -> Triple(R.string.question_mark, R.drawable.circle, R.attr.colorText)
else -> Triple(R.string.question_mark, R.drawable.cross, R.attr.colorErrorText) // ERROR.ordinal
}
}
}
@@ -17,11 +17,17 @@ enum class SettingsPreferences(val key: String) {
* Whether the user wants the next course in his schedule to be highlighted.
*/
COURSE_HIGHLIGHTING("setting_highlight"),
/**
* Whether the user wants the app to launch with the canteen fragment.
*/
LAUNCH_CANTEEN_ON_START("settings_launch_canteen"),
/**
* Whether the user wants dietary preferences to be coloured.
*/
COLOUR_PREFS("settings_prefs_colour"),
/**
* Whether the user opts into sending crash report.
*/
@@ -1,10 +1,12 @@
package com.denizk0461.studip.sheet
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import com.denizk0461.studip.R
import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.data.viewBinding
import com.denizk0461.studip.databinding.ItemSheetPreferenceBinding
import com.denizk0461.studip.databinding.SheetAllergenBinding
@@ -18,10 +20,12 @@ import com.denizk0461.studip.model.DietaryPreferences
*
* @param offer item to display further information on
* @param category category the offer belongs to
* @param displayColours whether the user wants dietary preferences to be marked with colours
*/
class AllergenSheet(
private val offer: CanteenOfferGroupElement,
private val category: String,
private val displayColours: Boolean,
) : AppSheet(R.layout.sheet_allergen) {
// View binding
@@ -45,7 +49,7 @@ class AllergenSheet(
// Inflate the view
val prefLine = ItemSheetPreferenceBinding.inflate(LayoutInflater.from(context))
val (stringId, drawableId) = DietaryPreferences.getData(index)
val (stringId, drawableId, colourId) = DietaryPreferences.getData(index)
// Add the localised text for the preference
prefLine.preferenceText.text = getString(stringId)
@@ -58,6 +62,17 @@ class AllergenSheet(
)
)
// Set tint of the icon; themed to the preference, if the user has the option enabled
prefLine.preferenceImage.imageTintList = ColorStateList.valueOf(
context.theme.getThemedColor(
if (displayColours) {
colourId
} else {
R.attr.colorText
}
)
)
// Add the view to the sheet's view container
containerPreferences.addView(prefLine.root)
}
@@ -62,6 +62,7 @@ class DevCodeSheet(
"SU5TQU5F".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS90NE9kYTlUZFYwbw==".d64)
"X0RSV05f".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9SUVpUUy1CWjhtcw==".d64)
"TUFSR0Uh".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS8tbHFxRHZXRjQ1dw==".d64)
"UE9XRUxM".d64 -> launchLink("aHR0cHM6Ly95b3V0dS5iZS9XUGMtVkVxQlBISQ==".d64)
"NEVENT" -> { // nuke events
nukeEvents()
showToast(context, "Nuked all events")
@@ -86,7 +87,8 @@ class DevCodeSheet(
nukeEverything()
showToast(context, "Nuked everything")
}
else -> showToast(context, "code is invalid")
"HACK()" -> showToast(context, "Hack attempt unsuccessful")
else -> showToast(context, "Code is invalid")
}
dismiss()
}
@@ -30,4 +30,11 @@ class CanteenPageViewModel(app: Application) : AppViewModel(app) {
*/
val preferenceAllergen: Boolean
get() = repo.getBooleanPreference(SettingsPreferences.ALLERGEN, defaultValue = true)
/**
* This value determines which canteen the user has selected.
*/
var preferenceColour: Boolean
get() = repo.getBooleanPreference(SettingsPreferences.COLOUR_PREFS)
set(newValue) { repo.setPreference(SettingsPreferences.COLOUR_PREFS, newValue) }
}
@@ -2,6 +2,8 @@ package com.denizk0461.studip.viewmodel
import android.app.Application
import com.denizk0461.studip.data.StudIPParser
import java.io.IOException
import kotlin.jvm.Throws
/**
* View model for [com.denizk0461.studip.activity.FetcherActivity]
@@ -10,7 +12,20 @@ import com.denizk0461.studip.data.StudIPParser
*/
class FetcherViewModel(app: Application) : AppViewModel(app) {
/**
* Instance of the parser used to fetch and parse the user's Stud.IP schedule.
*/
private val parser: StudIPParser = StudIPParser(app)
fun parse(html: String) { doAsync { parser.parse(html, {}) }}
/**
* Fetches and parses the user's Stud.IP schedule.
*
* @param html source code of the schedule website
*/
@Throws(IOException::class)
fun parse(html: String) {
doAsync {
parser.parse(html)
}
}
}
@@ -70,6 +70,13 @@ class SettingsViewModel(app: Application) : AppViewModel(app) {
get() = repo.getBooleanPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START)
set(newValue) { repo.setPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START, newValue) }
/**
* This value determines which canteen the user has selected.
*/
var preferenceColour: Boolean
get() = repo.getBooleanPreference(SettingsPreferences.COLOUR_PREFS)
set(newValue) { repo.setPreference(SettingsPreferences.COLOUR_PREFS, newValue) }
/**
* This value determines whether the user opts into submitting crash reports.
*/