Implemented functionality to switch canteens. UI for this is not fully implemented.

This commit is contained in:
denizk0461
2023-04-25 15:38:30 +02:00
parent 840c8f1db7
commit 6b1d77dfb3
21 changed files with 363 additions and 106 deletions
@@ -11,6 +11,7 @@ import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import com.denizk0461.studip.R
import com.denizk0461.studip.data.StudIPParser
import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.viewmodel.FetcherViewModel
import com.google.android.material.snackbar.Snackbar
@@ -86,16 +87,6 @@ class FetcherActivity : Activity() {
}
} catch (e: IOException) {
// Set up error colours
val colorErrorContainer = TypedValue()
val colorOnErrorContainer = TypedValue()
// Resolve themed attributes to get the right values for day and night modes
theme.apply {
resolveAttribute(R.attr.colorErrorContainer, colorErrorContainer, true)
resolveAttribute(R.attr.colorOnErrorContainer, colorOnErrorContainer, true)
}
// Let the user know that an error occurred
Snackbar
.make(
@@ -104,8 +95,8 @@ class FetcherActivity : Activity() {
Snackbar.LENGTH_SHORT
)
// Set colours to signify an error
.setBackgroundTint(colorErrorContainer.data)
.setTextColor(colorOnErrorContainer.data)
.setBackgroundTint(theme.getThemedColor(R.attr.colorErrorContainer))
.setTextColor(theme.getThemedColor(R.attr.colorOnErrorContainer))
.show()
}
}
@@ -75,11 +75,12 @@ class MainActivity : AppCompatActivity() {
// Prepare and commit a transaction between the current and the next fragment
supportFragmentManager.beginTransaction()
.replace(R.id.nav_host_fragment_content_main, fragment, id.toString())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
// .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
// Set text of the app bar accordingly
binding.contentMain.appTitleBar.text = getTitleString(id)
// binding.contentMain.appTitleBar.text = getTitleString(id)
// Note down the new current fragment
currentFragment = id
@@ -1,8 +1,11 @@
package com.denizk0461.studip.adapter
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.R
import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.data.parseToMinutes
import com.denizk0461.studip.model.StudIPEvent
import com.denizk0461.studip.databinding.ItemEventBinding
@@ -83,30 +86,32 @@ class StudIPEventItemAdapter(
// resolveAttribute(R.attr.colorTextHintLighter, colorTextHintLighter, true)
// }
val theme = holder.binding.root.context.theme
/* Highlight the next upcoming course of the day if the day of the adapter matches the
* current day, and if no other course has been highlighted, to avoid double highlighting.
*/
// if (!isAnyCourseHighlighted && currentItem.isCurrentCourse(currentCalendar)) {
// // Ensure that no other course will be highlighted
// isAnyCourseHighlighted = true
//
// holder.binding.cardBackground.apply {
// // Set the card's background colour to a desaturated shade of the primary colour
// backgroundTintList = ColorStateList.valueOf(colorPrimaryTranslucent.data)
//
// // Hide card stroke
// strokeColor = context.getColor(android.R.color.transparent)
// }
// // Otherwise, apply colours to ensure that the item will not be highlighted
// } else {
// holder.binding.cardBackground.apply {
// // Set the card's background colour to its default value
// backgroundTintList = ColorStateList.valueOf(colorCardBackground.data)
//
// // Set the card's stroke to its default colour
// strokeColor = colorTextHintLighter.data
// }
// }
if (!isAnyCourseHighlighted && currentItem.isCurrentCourse(currentCalendar)) {
// Ensure that no other course will be highlighted
isAnyCourseHighlighted = true
holder.binding.cardBackground.apply {
// Set the card's background colour to a desaturated shade of the primary colour
backgroundTintList = ColorStateList.valueOf(theme.getThemedColor(R.attr.colorSecondaryContainer))
// Hide card stroke
strokeColor = context.getColor(android.R.color.transparent)
}
// Otherwise, apply colours to ensure that the item will not be highlighted
} else {
holder.binding.cardBackground.apply {
// Set the card's background colour to its default value
backgroundTintList = ColorStateList.valueOf(theme.getThemedColor(R.attr.colorSurface))
// Set the card's stroke to its default colour
strokeColor = theme.getThemedColor(R.attr.colorOutlineVariant)
}
}
// Set up single click listener
holder.binding.cardBackground.setOnClickListener {
@@ -1,5 +1,10 @@
package com.denizk0461.studip.data
import android.content.res.Resources
import android.util.TypedValue
import androidx.annotation.AttrRes
import com.denizk0461.studip.R
/**
* Miscellaneous functions and variables that don't belong into any specific class.
*/
@@ -15,6 +20,16 @@ fun String.parseToMinutes(): Int {
return (parts[0].toInt() * 60) + parts[1].toInt()
}
/**
* Retrieves a specified colour customised to the currently applied theme.
*
* @param id attribute ID of the colour reference
* @return resolved colour
*/
fun Resources.Theme.getThemedColor(@AttrRes id: Int): Int = TypedValue().also { value ->
resolveAttribute(id, value, true)
}.data
object Misc {
// private val timeslotAcademicQuarter = mapOf(
@@ -1,5 +1,6 @@
package com.denizk0461.studip.data
import com.denizk0461.studip.R
import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.*
import org.jsoup.Jsoup
@@ -31,7 +32,7 @@ class StwParser {
* available
* @param onFinish action call for when the fetch has finished
*/
fun parse(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
fun parse(canteen: Int, onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
/*
* Reset primary key values. This needs to be done in case this method is called multiple
* times within the app's lifespan
@@ -41,21 +42,20 @@ class StwParser {
categoryId = 0
itemId = 0
/*
* Create and populate a list of all the canteen plans that will be fetched.
* TODO implement functionality that will allow for fetching multiple plans, if necessary
*/
val links = mutableListOf<String>()
if (true) { links.add(urlUniMensa) }
// if (true) { links.add(urlCafeCentral) }
// if (true) { links.add(urlNW1) }
// if (true) { links.add(urlGW2) }
// if (true) { links.add(urlHSBNeustadt) }
// if (true) { links.add(urlHSBAirport) }
// if (true) { links.add(urlHSBWerder) }
// if (true) { links.add(urlHfK) }
// if (true) { links.add(urlMensaBHV) }
// if (true) { links.add(urlCafeBHV) }
// Check which plan needs to be fetched
val link = when (canteen) {
0 -> urlUniMensa
1 -> urlCafeCentral
2 -> urlNW1
3 -> urlGW2
4 -> urlHSBNeustadt
5 -> urlHSBWerder
6 -> urlHSBAirport
7 -> urlMensaBHV
8 -> urlCafeBHV
9 -> urlHfK
else -> urlUniMensa
}
// Delete all previous entries and start afresh
Dependencies.repo.nukeOffers()
@@ -68,11 +68,11 @@ class StwParser {
* implemented here instead. Another option would be to set the conflict policy to IGNORE
* and move the dateId reset from parseFromPage() to parse().
*/
links.forEach { link ->
// links.forEach { link ->
parseFromPage(link, Dependencies.repo)
// TODO implement onRefresh(Int)
}
// }
// Action call once all fetching activities have finished
onFinish()
@@ -121,7 +121,7 @@ class AppRepository(app: Application) {
* @param pref the dietary preference that should be retrieved
* @return whether the preference needs to be met1
*/
fun getPreference(pref: DietaryPreferences): Boolean =
fun getBooleanPreference(pref: DietaryPreferences): Boolean =
getDietaryPrefs()[pref.ordinal] == DietaryPrefObject.C_TRUE
/**
@@ -170,7 +170,15 @@ class AppRepository(app: Application) {
* @param pref preference to retrieve
* @return whether the user set this preference
*/
fun getPreference(pref: SettingsPreferences): Boolean = prefs.getBoolean(pref.key, false)
fun getBooleanPreference(pref: SettingsPreferences): Boolean = prefs.getBoolean(pref.key, false)
/**
* Retrieves ta user-set integer preference.
*
* @param pref preference to retrieve
* @return whether the user set this preference
*/
fun getIntPreference(pref: SettingsPreferences): Int = prefs.getInt(pref.key, 0)
/**
* Updates a user-set boolean preference.
@@ -181,4 +189,14 @@ class AppRepository(app: Application) {
fun setPreference(pref: SettingsPreferences, newValue: Boolean) {
prefs.edit().putBoolean(pref.key, newValue).apply()
}
/**
* Updates a user-set integer preference.
*
* @param pref preference to set
* @param newValue new value to set the preference to
*/
fun setPreference(pref: SettingsPreferences, newValue: Int) {
prefs.edit().putInt(pref.key, newValue).apply()
}
}
@@ -2,9 +2,12 @@ package com.denizk0461.studip.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.viewModels
import com.denizk0461.studip.R
import com.denizk0461.studip.adapter.CanteenOfferItemAdapter
import com.denizk0461.studip.adapter.CanteenOfferPageAdapter
import com.denizk0461.studip.databinding.FragmentCanteenBinding
@@ -53,6 +56,31 @@ class CanteenFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// TODO KDoc and finish
binding.buttonCanteenPicker.setOnClickListener {
PopupMenu(binding.root.context, binding.buttonCanteenPicker).apply {
setOnMenuItemClickListener { item ->
viewModel.preferenceCanteen = when (item?.itemId) {
R.id.mensa_uni -> 0
R.id.cafe_central -> 1
R.id.mensa_nw1 -> 2
R.id.cafeteria_gw2 -> 3
R.id.mensa_neustadt -> 4
R.id.mensa_werder -> 5
R.id.mensa_airport -> 6
R.id.mensa_bhv -> 7
R.id.cafeteria_bhv -> 8
R.id.mensa_hfk -> 9
else -> 0
}
true
}
inflate(R.menu.menu_canteens)
show()
}
}
// Assign a preference value to every button to filter for dietary preferences
val chipMap = mapOf(
binding.chipPrefFair to DietaryPreferences.FAIR,
@@ -128,7 +156,7 @@ class CanteenFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListener {
// Set up functions for when the user swipes to refresh the view
binding.swipeRefreshLayout.setOnRefreshListener {
// Retrieve new offers from the website(s)
viewModel.fetchOffers(onRefreshUpdate = { status ->
viewModel.fetchOffers(viewModel.preferenceCanteen, onRefreshUpdate = { status ->
// TODO refresh updates
}, onFinish = {
// createTabLayoutMediator()
@@ -140,7 +168,8 @@ class CanteenFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListener {
/**
* Create and attach the object mediating the tabs for the view pager.
* TODO this is crash-prone
* TODO this is crash-prone and doesn't refresh when the dates get renewed (when the last update
* was from the day prior)
*/
private fun createTabLayoutMediator() {
// Fetch all dates from the database
@@ -68,6 +68,14 @@ class SettingsFragment : AppFragment() {
}
}
// Set up switch for launching the app with a specific view
binding.switchLaunchCanteen.apply {
isChecked = viewModel.preferenceLaunchCanteen
setOnCheckedChangeListener { _, newValue ->
viewModel.preferenceLaunchCanteen = newValue
}
}
// Set up switch for crash report opt-in
binding.switchCrashlytics.apply {
isChecked = viewModel.preferenceDataHandling
@@ -11,7 +11,9 @@ enum class SettingsPreferences(val key: String) {
ALLERGEN("setting_allergen"),
// Whether the user wants the next course in his schedule to be highlighted
COURSE_HIGHLIGHTING("setting_highlight"),
LAUNCH_CANTEEN_ON_START("settings_launch_canteen"),
// Whether the user opts into sending crash report
DATA_HANDLING("setting_data_handling"),
CANTEEN("setting_canteen"),
;
}
@@ -57,6 +57,17 @@ class ScheduleUpdateSheet(
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
}
/*
* TODO implement option to change the colour for the course - single-selection coloured
* filter buttons with checkmarks when they're selected maybe?
*/
// Prepare cancel button
binding.buttonCancel.setOnClickListener {
// Do nothing and dismiss the sheet
dismiss()
}
// Prepare delete button
binding.buttonDelete.setOnClickListener {
onDelete(event)
@@ -67,6 +78,7 @@ class ScheduleUpdateSheet(
// Prepare update/save button
binding.buttonSave.setOnClickListener {
onUpdate(
// construct new StudIPEvent from the data the user may have edited
StudIPEvent(
id = event.id,
title = binding.editTextTitle.text.toString(),
@@ -98,7 +110,7 @@ class ScheduleUpdateSheet(
// If start > end, show an error
if ((isEventStart && newTimestamp.parseToMinutes() > timeslotEnd.parseToMinutes()) ||
(!isEventStart && newTimestamp.parseToMinutes() < timeslotStart.parseToMinutes())) {
// tell user that the timestamp must be set earlier/later
// TODO tell user that the timestamp must be set earlier/later
} else {
// If the new timestamp is valid, save it
if (isEventStart) {
@@ -37,8 +37,8 @@ class CanteenViewModel(app: Application) : AppViewModel(app) {
fun getDates(): List<OfferDate> = returnBlocking { repo.getDates() }
// Fetch the canteen offers asynchronously. Updates will be provided through a LiveData object.
fun fetchOffers(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
doAsync { parser.parse(onRefreshUpdate, onFinish) }
fun fetchOffers(canteen: Int, onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
doAsync { parser.parse(canteen, onRefreshUpdate, onFinish) }
}
/**
@@ -57,11 +57,15 @@ class CanteenViewModel(app: Application) : AppViewModel(app) {
* @param pref the dietary preference that should be retrieved
* @return whether the preference needs to be met
*/
fun getPreference(pref: DietaryPreferences): Boolean = repo.getPreference(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.getPreference(SettingsPreferences.ALLERGEN)
get() = repo.getBooleanPreference(SettingsPreferences.ALLERGEN)
var preferenceCanteen: Int
get() = repo.getIntPreference(SettingsPreferences.CANTEEN)
set(newValue) { repo.setPreference(SettingsPreferences.CANTEEN, newValue) }
}
@@ -11,23 +11,31 @@ import com.denizk0461.studip.model.SettingsPreferences
class SettingsViewModel(app: Application) : AppViewModel(app) {
/**
* This value determines whether the user opts into submitting crash reports.
* This value determines whether the user wants to have the next course in their schedule
* highlighted.
*/
var preferenceCourseHighlighting: Boolean
get() = repo.getPreference(SettingsPreferences.DATA_HANDLING)
set(newValue) { repo.setPreference(SettingsPreferences.DATA_HANDLING, newValue) }
get() = repo.getBooleanPreference(SettingsPreferences.COURSE_HIGHLIGHTING)
set(newValue) { repo.setPreference(SettingsPreferences.COURSE_HIGHLIGHTING, newValue) }
/**
* This value determines whether the user wants to have allergens marked.
*/
var preferenceAllergen: Boolean
get() = repo.getPreference(SettingsPreferences.ALLERGEN)
get() = repo.getBooleanPreference(SettingsPreferences.ALLERGEN)
set(newValue) { repo.setPreference(SettingsPreferences.ALLERGEN, newValue) }
/**
* This value determines whether the user wants the app to launch with the canteen view.
*/
var preferenceLaunchCanteen: Boolean
get() = repo.getBooleanPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START)
set(newValue) { repo.setPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START, newValue) }
/**
* This value determines whether the user opts into submitting crash reports.
*/
var preferenceDataHandling: Boolean
get() = repo.getPreference(SettingsPreferences.DATA_HANDLING)
get() = repo.getBooleanPreference(SettingsPreferences.DATA_HANDLING)
set(newValue) { repo.setPreference(SettingsPreferences.DATA_HANDLING, newValue) }
}