FetcherActivity.kt now has a bottom app bar with useful actions and extends FragmentActivity instead of AppCompatActivity to show a tutorial bottom sheet

This commit is contained in:
denizk0461
2023-05-02 22:35:19 +02:00
parent 36a7ae0035
commit a5c66aaaff
17 changed files with 236 additions and 165 deletions
@@ -5,12 +5,13 @@ import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import com.denizk0461.studip.R
import com.denizk0461.studip.data.showErrorSnackBar
import com.denizk0461.studip.data.showToast
import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.sheet.TextSheet
import com.denizk0461.studip.viewmodel.FetcherViewModel
import java.io.IOException
import java.net.URLDecoder
@@ -19,7 +20,7 @@ import java.net.URLDecoder
* Activity for allowing the user to log in to access and fetch their schedule from their Stud.IP
* profile. Launched through a button in the app's settings.
*/
class FetcherActivity : AppCompatActivity() {
class FetcherActivity : FragmentActivity() {
// View binding
private lateinit var binding: ActivityFetcherBinding
@@ -54,13 +55,40 @@ class FetcherActivity : AppCompatActivity() {
}
}
// Set up bottom app bar for various actions
binding.bottomAppBar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.close -> {
// Close the activity
finish()
true
}
R.id.refresh -> {
// Refresh the currently opened page
binding.webview.reload()
true
}
R.id.help -> {
// Present the user with a small tutorial on how to fetch their timetable
TextSheet().also { sheet ->
val bundle = Bundle()
bundle.putString("header", getString(R.string.fetch_bar_help_sheet_title))
bundle.putString("content", getString(R.string.fetch_bar_help_sheet_content))
sheet.arguments = bundle
}.show(supportFragmentManager, TextSheet::class.java.simpleName)
true
}
else -> false
}
}
/*
* Attempt to load the user-set Stud.IP front page. Redirects to login page if user is
* not logged in.
*/
binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes")
binding.fab.setOnClickListener { view ->
binding.fab.setOnClickListener {
if (binding.webview.url?.contains(
"https://elearning.uni-bremen.de/dispatch.php/calendar/schedule"
@@ -78,20 +106,19 @@ class FetcherActivity : AppCompatActivity() {
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()
showToast(this, getString(R.string.toast_fetch_finished))
// 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))
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_snack), binding.bottomAppBar)
}
}
} else {
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_webpage_snack))
// Tell the user that they must navigate to their timetable
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_webpage_snack), binding.bottomAppBar)
}
}
}
@@ -6,6 +6,7 @@ import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.util.TypedValue
import android.view.View
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -51,13 +52,15 @@ fun showToast(context: Context, text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
fun Resources.Theme.showErrorSnackBar(view: CoordinatorLayout, text: String) {
Snackbar
fun Resources.Theme.showErrorSnackBar(view: CoordinatorLayout, text: String, anchor: View? = null) {
val s = Snackbar
.make(view, text, Snackbar.LENGTH_SHORT)
// Set colours to signify an error
.setBackgroundTint(getThemedColor(R.attr.colorErrorContainer))
.setTextColor(getThemedColor(R.attr.colorOnErrorContainer))
.show()
anchor?.let { a -> s.setAnchorView(a) }
s.show()
}
/**
@@ -24,7 +24,7 @@ class AppRepository(app: Application) {
private val dietaryPrefString = "prefs_obj"
// Blank dietary preference regular expression
private val blankDietaryPrefs: String = DietaryPreferences.C_FALSE.toString().repeat(10)
private val blankDietaryPrefs: String = DietaryPreferences.TEMPLATE_EMPTY
/**
* Used to observe just to register an update of the user-set dietary preferences.
@@ -7,20 +7,18 @@ import android.os.Bundle
import android.widget.TimePicker
/**
* Dialogue used for letting the user pick a timestamp.
* Dialogue used for letting the user pick a timestamp. TODO crashes on config change (multitask)
*
* @param timestamp timestamp that is to be edited
* @param listener listener for when the user finishes setting a time
* @param isEventStart whether the timestamp to be edited is the start of the event
*/
class TimePickerFragment(
private val timestamp: String,
private val listener: OnTimeSetListener,
private val isEventStart: Boolean,
) : DialogFragment(), TimePickerDialog.OnTimeSetListener {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val timestampSplit = timestamp.split(":")
val timestampSplit = (arguments?.getString("timestamp") ?: "").split(":")
return TimePickerDialog(
activity,
this,
@@ -121,7 +121,7 @@ class CanteenFragment : AppFragment() {
chip.isChecked = getPreference(pref)
// Set a listener for when the user clicks the chip
chip.setOnCheckedChangeListener { buttonView, newValue ->
chip.setOnCheckedChangeListener { _, newValue ->
// Save the preference change to persistent storage
setPreference(pref, newValue)
}
@@ -141,7 +141,6 @@ class SettingsFragment : AppFragment() {
intent.putExtras(bundle)
})
}
10 -> showToast(context, getString(R.string.settings_app_version_10))
15 -> showToast(context, getString(R.string.settings_app_version_15))
20 -> showToast(context, getString(R.string.settings_app_version_20))
30 -> showToast(context, getString(R.string.settings_app_version_30))
@@ -151,7 +150,10 @@ class SettingsFragment : AppFragment() {
200 -> showToast(context, getString(R.string.settings_app_version_200))
222 -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(mysteryLink)))
appVersionClick = -1
}
238 -> { // after 222 has been reached, this loops every 16 clicks
showToast(context, resources.getStringArray(R.array.cheesy).random())
appVersionClick = 222
}
}
appVersionClick += 1
@@ -1,65 +1,38 @@
package com.denizk0461.studip.model
// TODO KDoc
enum class AllergenPreferences {
GLUTEN_WHEAT,
GLUTEN_RYE,
GLUTEN_BARLEY,
GLUTEN_OATS,
GLUTEN_SPELT,
GLUTEN_KAMUT,
CRUSTACEANS,
EGGS,
FISH,
PEANUTS,
SOY,
DAIRY,
ALMONDS,
HAZELNUTS,
WALNUTS,
CASHEW_NUTS,
PECANS,
BRAZIL_NUTS,
PISTACHIOS,
MACADAMIA,
CELERY,
MUSTARD,
SULPHIDES,
LUPINS,
SESAME,
MOLLUSCS,
;
class AllergenPreferences {
/**
* Object that holds allergy preferences. Can be used to hold both the user-set values as well
* as the values of an individual canteen item.
*
* @param hasWheat
* @param hasRye
* @param hasBarley
* @param hasOats
* @param hasSpelt
* @param hasKamut
* @param hasCrustaceans
* @param hasEggs
* @param hasFish
* @param hasPeanuts
* @param hasSoy
* @param hasDairy
* @param hasAlmonds
* @param hasHazelnuts
* @param hasWalnuts
* @param hasCashewNuts
* @param hasPecans
* @param hasBrazilNuts
* @param hasPistachios
* @param hasMacadamia
* @param hasCelery
* @param hasMustard
* @param hasSulphides
* @param hasLupins
* @param hasSesame
* @param hasMolluscs
* @param hasWheat whether the offer contains gluten through wheat
* @param hasRye whether the offer contains gluten through rye
* @param hasBarley whether the offer contains gluten through barley
* @param hasOats whether the offer contains gluten through oats
* @param hasSpelt whether the offer contains gluten through spelt
* @param hasKamut whether the offer contains gluten through kamut
* @param hasCrustaceans whether the offer contains crustaceans
* @param hasEggs whether the offer contains eggs
* @param hasFish whether the offer contains fish
* @param hasPeanuts whether the offer contains peanuts
* @param hasSoy whether the offer contains soy
* @param hasDairy whether the offer contains dairy
* @param hasAlmonds whether the offer contains almonds
* @param hasHazelnuts whether the offer contains hazelnuts
* @param hasWalnuts whether the offer contains walnuts
* @param hasCashewNuts whether the offer contains cashew nuts
* @param hasPecans whether the offer contains pecan nuts
* @param hasBrazilNuts whether the offer contains brazil nuts
* @param hasPistachios whether the offer contains pistachios
* @param hasMacadamia whether the offer contains macadamia nuts
* @param hasCelery whether the offer contains celery
* @param hasMustard whether the offer contains mustard
* @param hasSulphides whether the offer contains highly concentrated sulphides
* @param hasLupins whether the offer contains lupins
* @param hasSesame whether the offer contains sesame
* @param hasMolluscs whether the offer contains molluscs
*/
data class Object(
val hasWheat: Boolean,
@@ -130,15 +103,16 @@ enum class AllergenPreferences {
}
companion object {
const val C_TRUE: Char = 'y'
const val C_FALSE: Char = 'n'
/**
* Template string representing no allergens.
*/
const val TEMPLATE: String = ""
/**
* Constructs an AllergenPreferenceObject from a string.
*
* @param input string that must be 26 characters long
* @param input string that must be 26 characters long. This will NOT be checked
* @return instance of AllergenPreferences.Object with preferences inserted
*/
fun construct(input: String): Object {
@@ -172,37 +146,5 @@ enum class AllergenPreferences {
elements.contains("n"),
)
}
// String(
// charArrayOf(
// charFor(obj.hasWheat),
// charFor(obj.hasRye),
// charFor(obj.hasBarley),
// charFor(obj.hasOats),
// charFor(obj.hasSpelt),
// charFor(obj.hasKamut),
// charFor(obj.hasCrustaceans),
// charFor(obj.hasEggs),
// charFor(obj.hasFish),
// charFor(obj.hasPeanuts),
// charFor(obj.hasSoy),
// charFor(obj.hasDairy),
// charFor(obj.hasAlmonds),
// charFor(obj.hasHazelnuts),
// charFor(obj.hasWalnuts),
// charFor(obj.hasCashewNuts),
// charFor(obj.hasPecans),
// charFor(obj.hasBrazilNuts),
// charFor(obj.hasPistachios),
// charFor(obj.hasMacadamia),
// charFor(obj.hasCelery),
// charFor(obj.hasMustard),
// charFor(obj.hasSulphides),
// charFor(obj.hasLupins),
// charFor(obj.hasSesame),
// charFor(obj.hasMolluscs),
// )
// )
private fun charFor(pref: Boolean) = if (pref) C_TRUE else C_FALSE
}
}
@@ -128,17 +128,23 @@ enum class DietaryPreferences(val value: String) {
companion object {
/**
* Char used to construct a regex to denote that a preference is met
* Char used to construct a regex to denote that a preference is met.
*/
const val C_TRUE: Char = 't'
/**
* Char used to construct a regex to denote that a preference is not met
* Char used to construct a regex to denote that a preference is not met.
*/
const val C_FALSE: Char = '.'
/**
* Default value for preferences if none are set.
*/
const val TEMPLATE_EMPTY: String = ".........."
/**
* Object denoting that no dietary preferences apply.
*/
val NONE_MET: Object = construct(TEMPLATE_EMPTY)
/**
@@ -37,6 +37,9 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
// View model
private val viewModel: ScheduleUpdateViewModel by viewModels()
// Whether the user has once clicked the delete button; used to prevent an accidental click
private var hasClickedDelete: Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -54,6 +57,7 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
binding.editTextLecturers.setText(event.lecturer)
binding.editTextRoom.setText(event.room)
/*
* Show academic quarter suggestion if it is applicable. For this to be the case, both the
* start timestamp and the end timestamp must match up in a pattern that would make it
@@ -62,63 +66,63 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
*
* 13:00 - 15:30 -> likely no academic quarter intended
*/
binding.buttonAcademicQuarter.visibility = if (
timeslotsAcademicQuarter.contains(timeslotStart) &&
timeslotsAcademicQuarter.contains(timeslotEnd)
) {
binding.buttonAcademicQuarter.setOnClickListener {
try {
// Retrieve new timestamps with the academic quarter applied, or throw a tantrum
val newStart = getTimestampAcademicQuarterStart(timeslotStart)
val newEnd = getTimestampAcademicQuarterEnd(timeslotEnd)
binding.buttonAcademicQuarter.setOnClickListener {
try {
// Retrieve new timestamps with the academic quarter applied, or throw a tantrum
val newStart = getTimestampAcademicQuarterStart(timeslotStart)
val newEnd = getTimestampAcademicQuarterEnd(timeslotEnd)
// Set the buttons to the new timesatmps
timeslotStart = newStart
binding.buttonTimeStart.text = newStart
timeslotEnd = newEnd
binding.buttonTimeEnd.text = newEnd
// Set the buttons to the new timesatmps
timeslotStart = newStart
binding.buttonTimeStart.text = newStart
timeslotEnd = newEnd
binding.buttonTimeEnd.text = newEnd
/*
/*
* Attempt to fix the issue of the bottom sheet jumping up when hiding the
* button.
*/
TransitionManager.beginDelayedTransition(binding.sheet.parent as ViewGroup)
binding.buttonAcademicQuarter.visibility = View.GONE
TransitionManager.beginDelayedTransition(binding.sheet.parent as ViewGroup)
binding.buttonAcademicQuarter.visibility = View.GONE
// Catch if a timeslot couldn't be found in the list, and tell the user
} catch (e: AcademicQuarterNotApplicableException) {
showToast(
context,
getString(R.string.sheet_schedule_update_hint_quarter_error)
)
}
// Catch if a timeslot couldn't be found in the list, and tell the user
} catch (e: AcademicQuarterNotApplicableException) {
showToast(
context,
getString(R.string.sheet_schedule_update_hint_quarter_error)
)
}
// If the pattern applies, show the button to the user
View.VISIBLE
} else {
// Hide the button if the action is not necessary or applicable
View.GONE
}
binding.buttonAcademicQuarter.visibility = getVisibilityForAcademicQuarter(
timeslotsAcademicQuarter.contains(timeslotStart),
timeslotsAcademicQuarter.contains(timeslotEnd),
)
// Prepare timestamp buttons
binding.buttonTimeStart.text = timeslotStart
binding.buttonTimeStart.setOnClickListener {
// Launch a time picker dialogue to pick a new start timestamp
TimePickerFragment(
binding.buttonTimeStart.text.toString(),
this,
true,
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
).also { sheet ->
val bundle = Bundle()
bundle.putString("timestamp", binding.buttonTimeStart.text.toString())
sheet.arguments = bundle
}.show((context as FragmentActivity).supportFragmentManager, "timePicker")
}
binding.buttonTimeEnd.text = timeslotEnd
binding.buttonTimeEnd.setOnClickListener {
// Launch a time picker dialogue to pick a new end timestamp
TimePickerFragment(
binding.buttonTimeEnd.text.toString(),
this,
false,
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
).also { sheet ->
val bundle = Bundle()
bundle.putString("timestamp", binding.buttonTimeEnd.text.toString())
sheet.arguments = bundle
}.show((context as FragmentActivity).supportFragmentManager, "timePicker")
}
/*
@@ -134,9 +138,20 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
// Prepare delete button
binding.buttonDelete.setOnClickListener {
viewModel.delete(event)
// Dismiss the sheet upon deletion
dismiss()
/*
* Check if the user has already clicked the button before; this is done to prevent
* accidentally deleting an event.
*/
if (hasClickedDelete) {
viewModel.delete(event)
// Dismiss the sheet upon deletion
dismiss()
} else {
// Update the text to show the user that they clicked the button once
TransitionManager.beginDelayedTransition(binding.buttonContainer as ViewGroup)
binding.buttonDelete.text = getString(R.string.sheet_schedule_update_delete_confirm)
hasClickedDelete = true
}
}
// Prepare update/save button
@@ -193,6 +208,26 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
timeslotEnd = newTimestamp
binding.buttonTimeEnd.text = newTimestamp
}
// Set appropriate visibility to the academic quarter button
TransitionManager.beginDelayedTransition(binding.sheet.parent as ViewGroup)
binding.buttonAcademicQuarter.visibility = getVisibilityForAcademicQuarter(
timeslotsAcademicQuarter.contains(timeslotStart),
timeslotsAcademicQuarter.contains(timeslotEnd),
)
}
}
/**
* Determines which visibility the academic quarter button should have given the timestamps.
*
* @param isStartValid whether the starting timestamp can be converted to an academic quarter
* @param isEndValid whether the ending timestamp can be converted to an academic quarter
*/
private fun getVisibilityForAcademicQuarter(isStartValid: Boolean, isEndValid: Boolean): Int =
if (isStartValid && isEndValid) {
View.VISIBLE
} else {
View.GONE
}
}