diff --git a/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt b/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt index 2a82018..cd089a4 100644 --- a/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt +++ b/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt @@ -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) } } } diff --git a/app/src/main/java/com/denizk0461/studip/data/Misc.kt b/app/src/main/java/com/denizk0461/studip/data/Misc.kt index 22bb677..1179fc3 100644 --- a/app/src/main/java/com/denizk0461/studip/data/Misc.kt +++ b/app/src/main/java/com/denizk0461/studip/data/Misc.kt @@ -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() } /** diff --git a/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt b/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt index 0dd8a2a..27528ac 100644 --- a/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt +++ b/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt @@ -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. diff --git a/app/src/main/java/com/denizk0461/studip/dialog/TimePickerFragment.kt b/app/src/main/java/com/denizk0461/studip/dialog/TimePickerFragment.kt index 0cd2ed1..39e9e71 100644 --- a/app/src/main/java/com/denizk0461/studip/dialog/TimePickerFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/dialog/TimePickerFragment.kt @@ -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, diff --git a/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt index cec507d..3b481d0 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt @@ -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) } diff --git a/app/src/main/java/com/denizk0461/studip/fragment/SettingsFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/SettingsFragment.kt index eb75af2..88b1d83 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/SettingsFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/SettingsFragment.kt @@ -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 diff --git a/app/src/main/java/com/denizk0461/studip/model/AllergenPreferences.kt b/app/src/main/java/com/denizk0461/studip/model/AllergenPreferences.kt index 82404d5..b1f0ead 100644 --- a/app/src/main/java/com/denizk0461/studip/model/AllergenPreferences.kt +++ b/app/src/main/java/com/denizk0461/studip/model/AllergenPreferences.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/model/DietaryPreferences.kt b/app/src/main/java/com/denizk0461/studip/model/DietaryPreferences.kt index 2502a6c..5aa28d5 100644 --- a/app/src/main/java/com/denizk0461/studip/model/DietaryPreferences.kt +++ b/app/src/main/java/com/denizk0461/studip/model/DietaryPreferences.kt @@ -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) /** diff --git a/app/src/main/java/com/denizk0461/studip/sheet/ScheduleUpdateSheet.kt b/app/src/main/java/com/denizk0461/studip/sheet/ScheduleUpdateSheet.kt index 1bee730..5cc3df3 100644 --- a/app/src/main/java/com/denizk0461/studip/sheet/ScheduleUpdateSheet.kt +++ b/app/src/main/java/com/denizk0461/studip/sheet/ScheduleUpdateSheet.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/cross.xml b/app/src/main/res/drawable/cross.xml index 844b6b6..f732437 100644 --- a/app/src/main/res/drawable/cross.xml +++ b/app/src/main/res/drawable/cross.xml @@ -1,4 +1,4 @@ - diff --git a/app/src/main/res/drawable/info.xml b/app/src/main/res/drawable/info.xml index e0ecb40..239cf29 100644 --- a/app/src/main/res/drawable/info.xml +++ b/app/src/main/res/drawable/info.xml @@ -1,4 +1,4 @@ - diff --git a/app/src/main/res/drawable/refresh.xml b/app/src/main/res/drawable/refresh.xml index 98469ca..700692c 100644 --- a/app/src/main/res/drawable/refresh.xml +++ b/app/src/main/res/drawable/refresh.xml @@ -1,4 +1,4 @@ - diff --git a/app/src/main/res/layout/activity_fetcher.xml b/app/src/main/res/layout/activity_fetcher.xml index b713010..41bbcb0 100644 --- a/app/src/main/res/layout/activity_fetcher.xml +++ b/app/src/main/res/layout/activity_fetcher.xml @@ -8,19 +8,25 @@ + android:layout_height="match_parent"/> + + + app:srcCompat="@drawable/save" + android:contentDescription="@string/fetch_bar_save_desc" + app:layout_anchor="@id/bottom_app_bar"/> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/sheet_schedule_update.xml b/app/src/main/res/layout/sheet_schedule_update.xml index d835eb5..692068a 100644 --- a/app/src/main/res/layout/sheet_schedule_update.xml +++ b/app/src/main/res/layout/sheet_schedule_update.xml @@ -165,11 +165,13 @@ + android:layout_marginBottom="8dp" + android:animateLayoutChanges="true"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 372a9a5..a2f077a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -23,8 +23,19 @@ Du musst deinen Stundenplan öffnen! Mensaplan konnte nicht heruntergeladen werden! + Schließen + Schließe die Webseite, ohne etwas zu speichern + Aktualisieren + Aktualisiere die Webseite + Hilfe + Zeige Hilfe + Wie Du Deinen Stundenplan in der App speichern kannst + • Logge dich mit deinem Stud.IP-Login ein\n • Öffne deinen Stundenplan (falls er sich nicht automatisch öffnet)\n • Klicke auf den \'Speichern\'-Knopf rechts in der unteren Ecke\n • Fertig! + Speichere den Stundenplan in der App ab + Speichern Löschen + Sicher? Abbrechen Bestätigen Veranstaltung bearbeiten @@ -164,7 +175,6 @@ App-Version - oh jaa klick weiter Solltest du nicht für deine Klausuren lernen? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a56b52a..55354ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,8 +23,19 @@ You must navigate to your schedule! Couldn\'t retrieve the canteen plan! + Close + Close the website without saving anything + Refresh + Refresh the page + Help + Show help + How to save your timetable in the app + • Log in with your Stud.IP login\n • Open your timetable view (in case it doesn\'t open automatically)\n • Click the \'save\' button in the bottom right corner\n • Done! + Save the timetable in the app + Save Delete + Sure? Cancel Confirm Edit Event @@ -153,7 +164,7 @@ This colours the icons similar to how they look the website Show canteen plan on app launch - Otherwise your schedule will be shown at launch + Otherwise your schedule will be shown at launch; CURRENTLY BROKEN Opt-in to sharing crash data Data will be anonymised @@ -170,7 +181,6 @@ App Version - ahhh yes keep clicking ^_^ ^_____^ @@ -181,5 +191,13 @@ with ♡️ from Bremen-Osterholz\nmade by denizk0461 \\__* - In our hearts, she lives forevermore + + in our hearts, she lives forevermore + it\'s only real when it hurts + can\'t go back in time – every second is a new one + doctor, i am the great clown Pagliacci + if we only had one more day, what is the last thing you\'d say? + love with your heart, use your head for everything else + if I die, I hope I\'m buried in a forest – that is, if any of it\'s left + \ No newline at end of file