WeserPlaner 1.0.3: adjusted layout of AllergenSheet.kt to display allergens and additives separately; FetcherActivity.kt now features a button to go directly to the user's schedule, if logged in; replaced toast messages with snack bars; ScheduleUpdateSheet.kt now notifies the user about database transactions via snack bar; schedule view now tells the user if there are no events scheduled for a day

This commit is contained in:
denizk0461
2023-05-14 23:09:06 +02:00
parent 8f69b5f381
commit 1c31d422fa
22 changed files with 261 additions and 85 deletions
+6 -6
View File
@@ -4,17 +4,17 @@ WeserPlaner is a work-in-progress Android app for students at the University of
![Screenshots of the WeserPlaner app](https://denizk0461.github.io/weserplaner_showcase.png) ![Screenshots of the WeserPlaner app](https://denizk0461.github.io/weserplaner_showcase.png)
## Progress ## Progress
- Downloading and creating events, as well as displaying them is fully functional. - Downloading and creating events, as well as displaying them.
- Downloading canteen offers, filtering them, and viewing details is fully functional. - Downloading canteen offers, filtering them, and viewing details.
- Viewing additional information on the canteen (opening hours, news) works. - Viewing additional information on the canteen (opening hours, news).
## What's broken ## What's broken
Currently, nothing is outright broken, but a few things may be bodged together. Currently, nothing is outright broken, but a few things may be bodged together badly.
## TODO ## TODO
- Adding exams, homework, etc.: users will be able to add any tasks they need to do as reminders. Those can be associated with events that either have been downloaded from the user's Stud.IP schedule, or ones the user has created manually. An overview will be provided that allows the user to have, well, an overview of everything they need to do. - Adding exams, homework, etc.: users will be able to add any tasks they need to do as reminders. Those can be associated with events that either have been downloaded from the user's Stud.IP schedule, or ones the user has created manually. An overview will be provided that allows the user to have, well, an overview of everything they need to do.
- Optimisations to data handling will be made. The app currently only supports storing a single timetable, which will be changed in future releases. A planned implementation will see the user being able to download schedules as well as create ones from scratch, with the schedules able to be selected via a dropdown menu. - Optimisations to data handling will be made. The app currently only supports storing a single timetable, which will be changed in future releases. A planned implementation will see the user being able to download schedules as well as create ones from scratch, with the schedules able to be selected via a dropdown menu.
- Some UI restructuring to declutter certain screens and improve the UX. - Some improvements to the Play Store screenshots are sorely needed...
## Why Android-only? ## Why Android-only?
Because: Because:
@@ -27,6 +27,6 @@ I may reconsider once Apple finally opens up iOS for third-party apps.
The colour scheme is inspired by the University of Bremen's colour scheme, as well as the city of Bremen's colour scheme. The logo represents both the University of Bremen as well as (kind of) the Weserstadion, our local sports stadium. The colour scheme is inspired by the University of Bremen's colour scheme, as well as the city of Bremen's colour scheme. The logo represents both the University of Bremen as well as (kind of) the Weserstadion, our local sports stadium.
I admit the Weserstadion idea came to me after I had created the logo. I admit the Weserstadion idea only came to me after I had created the logo.
![WeserPlaner app icon](https://denizk0461.github.io/weserplaner_app_icon.png) ![WeserPlaner app icon](https://denizk0461.github.io/weserplaner_app_icon.png)
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.denizk0461.weserplaner" applicationId "com.denizk0461.weserplaner"
minSdk 24 minSdk 24
targetSdk 33 targetSdk 33
versionCode 6 versionCode 7
versionName "1.0.2" versionName "1.0.3"
resourceConfigurations += ["en", "de"] resourceConfigurations += ["en", "de"]
+2 -2
View File
@@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 6, "versionCode": 7,
"versionName": "1.0.2", "versionName": "1.0.3",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],
@@ -10,6 +10,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.denizk0461.weserplaner.R import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.data.showErrorSnackBar import com.denizk0461.weserplaner.data.showErrorSnackBar
import com.denizk0461.weserplaner.data.showSnackBar
import com.denizk0461.weserplaner.data.showToast import com.denizk0461.weserplaner.data.showToast
import com.denizk0461.weserplaner.databinding.ActivityFetcherBinding import com.denizk0461.weserplaner.databinding.ActivityFetcherBinding
import com.denizk0461.weserplaner.model.TextSheetContentId import com.denizk0461.weserplaner.model.TextSheetContentId
@@ -50,7 +51,11 @@ class FetcherActivity : FragmentActivity() {
setContentView(binding.root) setContentView(binding.root)
// Remind the user of the help button // Remind the user of the help button
showToast(this, getString(R.string.toast_info_reminder)) theme.showSnackBar(
binding.coordinatorLayout,
getString(R.string.fetch_snack_info_reminder),
binding.bottomAppBar,
)
// Enabling JavaScript. See comment above lint suppression for reason why this is done. // Enabling JavaScript. See comment above lint suppression for reason why this is done.
binding.webview.settings.apply { binding.webview.settings.apply {
@@ -104,15 +109,19 @@ class FetcherActivity : FragmentActivity() {
}.show(supportFragmentManager, TextSheet::class.java.simpleName) }.show(supportFragmentManager, TextSheet::class.java.simpleName)
true true
} }
R.id.schedule -> {
binding.webview.loadUrl(scheduleUrl)
true
}
else -> false else -> false
} }
} }
/* /*
* Attempt to load the user-set Stud.IP front page. Redirects to login page if user is * Attempt to load the user's Stud.IP schedule page. Redirects to login page if user is
* not logged in. * not logged in.
*/ */
binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes") binding.webview.loadUrl(scheduleUrl)
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
@@ -146,18 +155,18 @@ class FetcherActivity : FragmentActivity() {
} catch (e: IOException) { } catch (e: IOException) {
// Let the user know that an error occurred // Let the user know that an error occurred
theme.showErrorSnackBar( theme.showErrorSnackBar(
binding.rootView, binding.coordinatorLayout,
getString(R.string.fetch_error_snack), getString(R.string.fetch_error_snack),
binding.bottomAppBar binding.bottomAppBar,
) )
} }
} }
} else { } else {
// Tell the user that they must navigate to their timetable // Tell the user that they must navigate to their timetable
theme.showErrorSnackBar( theme.showErrorSnackBar(
binding.rootView, binding.coordinatorLayout,
getString(R.string.fetch_error_webpage_snack), getString(R.string.fetch_error_webpage_snack),
binding.bottomAppBar binding.bottomAppBar,
) )
} }
} }
@@ -76,19 +76,15 @@ class CanteenOfferItemAdapter(
// Display // Display
if (position == (offers.size - 1) && difference > 0) { if (position == (offers.size - 1) && difference > 0) {
holder.binding.textDifference.apply { holder.binding.differenceContainer.visibility = View.VISIBLE
visibility = View.VISIBLE holder.binding.textDifference.text = if (difference == 1) {
text = if (difference == 1) {
context.getString(R.string.canteen_item_difference_text_one) context.getString(R.string.canteen_item_difference_text_one)
} else { } else {
context.getString(R.string.canteen_item_difference_text_other, difference) context.getString(R.string.canteen_item_difference_text_other, difference)
} }
}
} else { } else {
holder.binding.textDifference.apply { holder.binding.differenceContainer.visibility = View.GONE
visibility = View.GONE holder.binding.textDifference.text = ""
text = ""
}
} }
/* /*
@@ -2,12 +2,13 @@ package com.denizk0461.weserplaner.adapter
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.denizk0461.weserplaner.data.AppDiffUtilCallback import com.denizk0461.weserplaner.data.AppDiffUtilCallback
import com.google.android.material.R import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.data.getThemedColor import com.denizk0461.weserplaner.data.getThemedColor
import com.denizk0461.weserplaner.data.parseToMinutes import com.denizk0461.weserplaner.data.parseToMinutes
import com.denizk0461.weserplaner.model.StudIPEvent import com.denizk0461.weserplaner.model.StudIPEvent
@@ -19,7 +20,7 @@ import java.util.*
* *
* @param currentDay current day that is used to show only a given day's events (0 = Monday, * @param currentDay current day that is used to show only a given day's events (0 = Monday,
* 4 = Friday) * 4 = Friday, 6 = Sunday)
* @param onClickListener used for listening to clicks and long presses * @param onClickListener used for listening to clicks and long presses
*/ */
class StudIPEventItemAdapter( class StudIPEventItemAdapter(
@@ -65,9 +66,28 @@ class StudIPEventItemAdapter(
override fun getItemCount(): Int = events.size override fun getItemCount(): Int = events.size
override fun onBindViewHolder(holder: EventViewHolder, position: Int) { override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
// Retrieve item for current position // Retrieve item for current position
val currentItem = events[position] val currentItem = events[position]
// Context for easier reference
val context = holder.binding.root.context
// Display a notice to the user saying they have no events scheduled for the day
if (currentItem.eventId == -1) {
holder.binding.cardBackground.visibility = View.GONE
holder.binding.noEventsContainer.visibility = View.VISIBLE
holder.binding.textNoEvents.text = context.getString(
R.string.schedule_no_events,
context.resources.getStringArray(R.array.weekdays)[currentDay]
)
return
}
// Set visibility of the elements accordingly if there are events scheduled
holder.binding.cardBackground.visibility = View.VISIBLE
holder.binding.noEventsContainer.visibility = View.GONE
// Set up text fields with corresponding values // Set up text fields with corresponding values
holder.binding.textTitle.text = currentItem.title holder.binding.textTitle.text = currentItem.title
holder.binding.textLecturers.text = currentItem.lecturer holder.binding.textLecturers.text = currentItem.lecturer
@@ -77,14 +97,17 @@ class StudIPEventItemAdapter(
holder.binding.textTimeslot.text = currentItem.timeslot() holder.binding.textTimeslot.text = currentItem.timeslot()
// Get current theme // Get current theme
val theme = holder.binding.root.context.theme val theme = context.theme
/* /*
* Highlight the next upcoming course of the day if the user selected the option, if the day * Highlight the next upcoming course of the day if the user selected the option, if the day
* of the adapter matches the current day, and if no other course has been highlighted, to * of the adapter matches the current day, and if no other course has been highlighted, to
* avoid double highlighting. * avoid double highlighting.
*/ */
if (highlightNextCourse && !isAnyCourseHighlighted && currentItem.isCurrentCourse(currentCalendar)) { if (highlightNextCourse &&
!isAnyCourseHighlighted &&
currentItem.isCurrentCourse(currentCalendar)
) {
// Ensure that no other course will be highlighted // Ensure that no other course will be highlighted
isAnyCourseHighlighted = true isAnyCourseHighlighted = true
@@ -98,12 +121,12 @@ class StudIPEventItemAdapter(
} }
// Set the divider colour to the text colour // Set the divider colour to the text colour
holder.binding.divider.dividerColor = holder.binding.divider.dividerColor =
theme.getThemedColor(com.denizk0461.weserplaner.R.attr.colorText) theme.getThemedColor(R.attr.colorText)
// Set ripple colour // Set ripple colour
holder.binding.linearLayout.background = getDrawable( holder.binding.linearLayout.background = getDrawable(
holder.binding.root.context, holder.binding.root.context,
com.denizk0461.weserplaner.R.drawable.selectable_item_background_highlighted, R.drawable.selectable_item_background_highlighted,
) )
// Otherwise, apply colours to ensure that the item will not be highlighted // Otherwise, apply colours to ensure that the item will not be highlighted
@@ -111,7 +134,9 @@ class StudIPEventItemAdapter(
holder.binding.cardBackground.apply { holder.binding.cardBackground.apply {
// Set the card's background colour to its default value // Set the card's background colour to its default value
backgroundTintList = backgroundTintList =
ColorStateList.valueOf(theme.getThemedColor(R.attr.colorSurface)) ColorStateList.valueOf(theme.getThemedColor(
com.google.android.material.R.attr.colorSurface
))
// Set the card's stroke to its default colour // Set the card's stroke to its default colour
strokeColor = theme.getThemedColor(R.attr.colorOutlineVariant) strokeColor = theme.getThemedColor(R.attr.colorOutlineVariant)
@@ -122,7 +147,7 @@ class StudIPEventItemAdapter(
// Set ripple colour // Set ripple colour
holder.binding.linearLayout.background = getDrawable( holder.binding.linearLayout.background = getDrawable(
holder.binding.root.context, holder.binding.root.context,
com.denizk0461.weserplaner.R.drawable.selectable_item_background, R.drawable.selectable_item_background,
) )
} }
@@ -10,6 +10,7 @@ import androidx.viewpager2.widget.ViewPager2
import com.denizk0461.weserplaner.R import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.activity.FetcherActivity import com.denizk0461.weserplaner.activity.FetcherActivity
import com.denizk0461.weserplaner.adapter.StudIPEventPageAdapter import com.denizk0461.weserplaner.adapter.StudIPEventPageAdapter
import com.denizk0461.weserplaner.data.showSnackBar
import com.denizk0461.weserplaner.databinding.FragmentEventBinding import com.denizk0461.weserplaner.databinding.FragmentEventBinding
import com.denizk0461.weserplaner.sheet.ScheduleUpdateSheet import com.denizk0461.weserplaner.sheet.ScheduleUpdateSheet
import com.denizk0461.weserplaner.viewmodel.EventViewModel import com.denizk0461.weserplaner.viewmodel.EventViewModel
@@ -144,6 +145,15 @@ class EventFragment : AppFragment() {
binding.fabAddEvent.extend() binding.fabAddEvent.extend()
} }
/**
* Creates and shows a snack bar.
*
* @param text content to display in the snack bar
*/
fun showSnackBar(text: String) {
context.theme.showSnackBar(binding.coordinatorLayout, text)
}
// Invalidate the view binding // Invalidate the view binding
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
@@ -96,7 +96,20 @@ class EventPageFragment : AppFragment(), StudIPEventItemAdapter.OnClickListener
// Set up LiveData observer to refresh the view on update // Set up LiveData observer to refresh the view on update
viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events -> viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events ->
eventAdapter.setNewData(events) eventAdapter.setNewData(events.ifEmpty { listOf(
// Use an event with eventId = -1 to denote that there are no events for this day
StudIPEvent(
-1,
"",
"",
"",
0,
"",
"",
0,
0,
)
) })
} }
} }
@@ -238,8 +238,7 @@ class SettingsFragment : AppFragment() {
licencesLongClick = 0 licencesLongClick = 0
viewModel.preferenceExperimentalSettingsEnabled = true viewModel.preferenceExperimentalSettingsEnabled = true
binding.cardExperimentalSettings.visibility = View.VISIBLE binding.cardExperimentalSettings.visibility = View.VISIBLE
context.theme.showSnackBar( showSnackBar(
binding.coordinatorLayout,
getString(R.string.settings_unlocked_experiments), getString(R.string.settings_unlocked_experiments),
) )
} else { } else {
@@ -320,8 +319,7 @@ class SettingsFragment : AppFragment() {
binding.buttonHideExperiments.setOnClickListener { binding.buttonHideExperiments.setOnClickListener {
viewModel.preferenceExperimentalSettingsEnabled = false viewModel.preferenceExperimentalSettingsEnabled = false
binding.cardExperimentalSettings.visibility = View.GONE binding.cardExperimentalSettings.visibility = View.GONE
context.theme.showSnackBar( showSnackBar(
binding.coordinatorLayout,
getString(R.string.settings_hide_experiments), getString(R.string.settings_hide_experiments),
) )
} }
@@ -397,8 +395,7 @@ class SettingsFragment : AppFragment() {
viewModel.nukeEverything() viewModel.nukeEverything()
showToast(context, "Nuked everything") showToast(context, "Nuked everything")
} }
else -> context.theme.showSnackBar( else -> showSnackBar(
binding.coordinatorLayout,
getString(R.string.settings_dev_codes_invalid) getString(R.string.settings_dev_codes_invalid)
) )
} }
@@ -418,6 +415,15 @@ class SettingsFragment : AppFragment() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
} }
/**
* Creates and shows a snack bar.
*
* @param text content to display in the snack bar
*/
fun showSnackBar(text: String) {
context.theme.showSnackBar(binding.coordinatorLayout, text)
}
/** /**
* Decodes Base64 to a regular string. * Decodes Base64 to a regular string.
* *
@@ -4,10 +4,11 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import com.denizk0461.weserplaner.R import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.data.showToast
import com.denizk0461.weserplaner.data.viewBinding import com.denizk0461.weserplaner.data.viewBinding
import com.denizk0461.weserplaner.databinding.SheetAllergenConfigBinding import com.denizk0461.weserplaner.databinding.SheetAllergenConfigBinding
import com.denizk0461.weserplaner.fragment.SettingsFragment
import com.denizk0461.weserplaner.model.AllergenPreferences import com.denizk0461.weserplaner.model.AllergenPreferences
import com.denizk0461.weserplaner.viewmodel.AllergenConfigViewModel import com.denizk0461.weserplaner.viewmodel.AllergenConfigViewModel
@@ -26,6 +27,17 @@ class AllergenConfigSheet : AppSheet(R.layout.sheet_allergen_config) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Set up FAB behaviour for parent fragment's FAB if activity is not null
val parentFragment = activity?.let { activity ->
// Retrieve parent fragment to access its functions
val navHostFragment = activity
.supportFragmentManager
.fragments[0] as NavHostFragment
navHostFragment
.childFragmentManager
.primaryNavigationFragment as SettingsFragment
}
// Retrieve allergens as currently set by the user from view model // Retrieve allergens as currently set by the user from view model
val currentAllergenConfig = AllergenPreferences.construct( val currentAllergenConfig = AllergenPreferences.construct(
viewModel.preferenceAllergenConfig viewModel.preferenceAllergenConfig
@@ -73,7 +85,7 @@ class AllergenConfigSheet : AppSheet(R.layout.sheet_allergen_config) {
parentFragment?.setFragmentResult("allergenConfig", Bundle()) parentFragment?.setFragmentResult("allergenConfig", Bundle())
// Tell the user that saving was successful // Tell the user that saving was successful
showToast(context, getString(R.string.allergens_config_confirmation)) parentFragment?.showSnackBar(getString(R.string.allergens_config_confirmation))
// Close the sheet // Close the sheet
dismiss() dismiss()
@@ -84,20 +84,28 @@ class AllergenSheet : AppSheet(R.layout.sheet_allergen) {
} }
} }
// Allergen and additive notice // Retrieve allergen and additive strings
// textContent.text = offer.allergens.compileAllergenString() val (allergens, additives) = offer.allergens.compileStrings()
val (a, b) = offer.allergens.compileStrings()
if (a.isBlank() && b.isBlank()) { // Display notices for allergens and additives, if necessary
if (allergens.isBlank() && additives.isBlank()) {
// Tell the user that no allergens and no additives are present in the offer
textAllergensTitle.text = getString(R.string.allergens_none) textAllergensTitle.text = getString(R.string.allergens_none)
textAllergensContent.visibility = View.GONE textAllergensContent.visibility = View.GONE
textAdditivesTitle.visibility = View.GONE textAdditivesTitle.visibility = View.GONE
textAdditivesContent.visibility = View.GONE textAdditivesContent.visibility = View.GONE
} else { } else {
// Set allergen and additives accordingly if any are present
textAllergensTitle.text = getString(R.string.allergens_allergens_title) textAllergensTitle.text = getString(R.string.allergens_allergens_title)
textAllergensContent.text = getString(R.string.allergens_content_template, a) textAllergensContent.text = getString(
R.string.allergens_content_template,
allergens,
)
textAdditivesTitle.text = getString(R.string.allergens_additives_title) textAdditivesTitle.text = getString(R.string.allergens_additives_title)
textAdditivesContent.text = getString(R.string.allergens_content_template, b) textAdditivesContent.text = getString(
R.string.allergens_content_template,
additives,
)
} }
// Show a price, if one is available // Show a price, if one is available
@@ -124,22 +132,23 @@ class AllergenSheet : AppSheet(R.layout.sheet_allergen) {
} }
/** /**
* Retrieves localised strings for all allergens present in a given offer and compiles them into * Retrieves localised strings for all allergens and additives present in a given offer and
* a human-readable string, if any allergens are present. * compiles them into human-readable strings, if any are present.
* *
* @return a string of all allergens * @receiver string to parse allergens and additives from
* @return allergens and additives formatted as strings
*/ */
private fun String.compileStrings(): Pair<String, String> { private fun String.compileStrings(): Pair<String, String> {
// Let the user know if no allergens are present // Return empty values if the received string is blank
if (this.isBlank()) return Pair("", "") if (this.isBlank()) return Pair("", "")
// Stores all localised additives
val additiveList = mutableListOf<String>()
// Stores all localised allergens // Stores all localised allergens
val allergenList = mutableListOf<String>() val allergenList = mutableListOf<String>()
// Stores all localised additives
val additiveList = mutableListOf<String>()
// Split all allergens to iterate through them // Split all allergens to iterate through them
val allergens = this.replace(" ", "").split(",") val allergens = this.replace(" ", "").split(",")
@@ -156,16 +165,10 @@ class AllergenSheet : AppSheet(R.layout.sheet_allergen) {
} }
} }
// Insert the items into a template string /*
// return getString( * Join the lists to formatted strings, separated by commas. Put a localised "none" if
// R.string.allergens_contains, * either no allergens or additives could be found respectively.
// allergenList.joinToString(", ").ifBlank { */
// getString(R.string.allergens_single_none)
// },
// additiveList.joinToString(", ").ifBlank {
// getString(R.string.allergens_single_none)
// },
// )
return Pair( return Pair(
allergenList.joinToString(", ").ifBlank { allergenList.joinToString(", ").ifBlank {
getString(R.string.allergens_single_none) getString(R.string.allergens_single_none)
@@ -6,6 +6,7 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.denizk0461.weserplaner.R import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.data.getParcelableCompat import com.denizk0461.weserplaner.data.getParcelableCompat
@@ -18,6 +19,7 @@ import com.denizk0461.weserplaner.data.viewBinding
import com.denizk0461.weserplaner.databinding.SheetScheduleUpdateBinding import com.denizk0461.weserplaner.databinding.SheetScheduleUpdateBinding
import com.denizk0461.weserplaner.exception.AcademicQuarterNotApplicableException import com.denizk0461.weserplaner.exception.AcademicQuarterNotApplicableException
import com.denizk0461.weserplaner.exception.ParcelNotFoundException import com.denizk0461.weserplaner.exception.ParcelNotFoundException
import com.denizk0461.weserplaner.fragment.EventFragment
import com.denizk0461.weserplaner.model.StudIPEvent import com.denizk0461.weserplaner.model.StudIPEvent
import com.denizk0461.weserplaner.viewmodel.ScheduleUpdateViewModel import com.denizk0461.weserplaner.viewmodel.ScheduleUpdateViewModel
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
@@ -51,6 +53,17 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Set up FAB behaviour for parent fragment's FAB if activity is not null
val parentFragment = activity?.let { activity ->
// Retrieve parent fragment to access its functions
val navHostFragment = activity
.supportFragmentManager
.fragments[0] as NavHostFragment
navHostFragment
.childFragmentManager
.primaryNavigationFragment as EventFragment
}
if (arguments?.getBoolean("isEditing") == true) { if (arguments?.getBoolean("isEditing") == true) {
try { try {
@@ -88,7 +101,15 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update) {
* accidentally deleting an event. * accidentally deleting an event.
*/ */
if (hasClickedDelete) { if (hasClickedDelete) {
// Delete the event
viewModel.delete(event) viewModel.delete(event)
// Tell the user that the event has been deleted
parentFragment?.showSnackBar(
getString(R.string.sheet_schedule_snack_delete)
)
// Dismiss the sheet upon deletion // Dismiss the sheet upon deletion
dismiss() dismiss()
} else { } else {
@@ -133,6 +154,12 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update) {
colour = colour, colour = colour,
) )
) )
// Tell the user that the event has been updated
parentFragment?.showSnackBar(
getString(R.string.sheet_schedule_snack_update)
)
// Dismiss the sheet upon update // Dismiss the sheet upon update
dismiss() dismiss()
} }
@@ -188,6 +215,11 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update) {
) )
) )
// Tell the user that the event has been updated
parentFragment?.showSnackBar(
getString(R.string.sheet_schedule_snack_add)
)
// Dismiss the sheet upon update // Dismiss the sheet upon update
dismiss() dismiss()
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#000000" <vector android:height="24dp" android:tint="?attr/colorOnSurfaceVariant"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16.24,11.51l1.57,-1.57l-3.75,-3.75l-1.57,1.57L8.35,3.63c-0.78,-0.78 -2.05,-0.78 -2.83,0l-1.9,1.9c-0.78,0.78 -0.78,2.05 0,2.83l4.13,4.13L3,17.25V21h3.75l4.76,-4.76l4.13,4.13c0.95,0.95 2.23,0.6 2.83,0l1.9,-1.9c0.78,-0.78 0.78,-2.05 0,-2.83L16.24,11.51zM9.18,11.07L5.04,6.94l1.89,-1.9c0,0 0,0 0,0l1.27,1.27L7.02,7.5l1.41,1.41l1.19,-1.19l1.45,1.45L9.18,11.07zM17.06,18.96l-4.13,-4.13l1.9,-1.9l1.45,1.45l-1.19,1.19l1.41,1.41l1.19,-1.19l1.27,1.27L17.06,18.96z"/> <path android:fillColor="@android:color/white" android:pathData="M16.24,11.51l1.57,-1.57l-3.75,-3.75l-1.57,1.57L8.35,3.63c-0.78,-0.78 -2.05,-0.78 -2.83,0l-1.9,1.9c-0.78,0.78 -0.78,2.05 0,2.83l4.13,4.13L3,17.25V21h3.75l4.76,-4.76l4.13,4.13c0.95,0.95 2.23,0.6 2.83,0l1.9,-1.9c0.78,-0.78 0.78,-2.05 0,-2.83L16.24,11.51zM9.18,11.07L5.04,6.94l1.89,-1.9c0,0 0,0 0,0l1.27,1.27L7.02,7.5l1.41,1.41l1.19,-1.19l1.45,1.45L9.18,11.07zM17.06,18.96l-4.13,-4.13l1.9,-1.9l1.45,1.45l-1.19,1.19l1.41,1.41l1.19,-1.19l1.27,1.27L17.06,18.96z"/>
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12.126,8.125l1.937,-1.937 3.747,3.747 -1.937,1.938zM20.71,5.63l-2.34,-2.34a1,1 0,0 0,-1.41 0l-1.83,1.83 3.75,3.75L20.71,7a1,1 0,0 0,0 -1.37zM2,5l6.63,6.63L3,17.25V21h3.75l5.63,-5.62L18,21l2,-2L4,3 2,5z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_view" android:id="@+id/coordinator_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true"> android:animateLayoutChanges="true">
@@ -2,6 +2,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".fragment.EventFragment"> tools:context=".fragment.EventFragment">
+17
View File
@@ -47,10 +47,27 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/difference_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginHorizontal="4dp"
android:scaleType="centerInside"
app:tint="?attr/colorText"
android:src="@drawable/visibility_off"/>
<TextView <TextView
android:id="@+id/text_difference" android:id="@+id/text_difference"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"/> android:layout_gravity="center_horizontal"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
+26 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingHorizontal="8dp" android:paddingHorizontal="8dp"
android:paddingVertical="4dp" android:paddingVertical="4dp"
android:orientation="vertical"> android:orientation="vertical">
@@ -55,4 +55,28 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/no_events_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginHorizontal="4dp"
android:scaleType="centerInside"
app:tint="?attr/colorText"
android:src="@drawable/edit_off"/>
<TextView
android:id="@+id/text_no_events"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/schedule_no_events"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
+12 -6
View File
@@ -4,19 +4,25 @@
<item <item
android:id="@+id/close" android:id="@+id/close"
android:icon="@drawable/cross" android:icon="@drawable/cross"
android:title="@string/fetch_bar_close_title" android:title="@string/fetch_bar_close_desc"
android:contentDescription="@string/fetch_bar_close_desc" android:contentDescription="@string/fetch_bar_close_desc"
app:showAsAction="ifRoom"/> app:showAsAction="always"/>
<item <item
android:id="@+id/refresh" android:id="@+id/refresh"
android:icon="@drawable/refresh" android:icon="@drawable/refresh"
android:title="@string/fetch_bar_refresh_title" android:title="@string/fetch_bar_refresh_desc"
android:contentDescription="@string/fetch_bar_refresh_desc" android:contentDescription="@string/fetch_bar_refresh_desc"
app:showAsAction="ifRoom"/> app:showAsAction="always"/>
<item <item
android:id="@+id/help" android:id="@+id/help"
android:icon="@drawable/info" android:icon="@drawable/info"
android:title="@string/fetch_bar_help_title" android:title="@string/fetch_bar_help_desc"
android:contentDescription="@string/fetch_bar_help_desc" android:contentDescription="@string/fetch_bar_help_desc"
app:showAsAction="ifRoom"/> app:showAsAction="always"/>
<item
android:id="@+id/schedule"
android:icon="@drawable/design"
android:title="@string/fetch_bar_schedule_desc"
android:contentDescription="@string/fetch_bar_schedule_desc"
app:showAsAction="always"/>
</menu> </menu>
+8 -2
View File
@@ -34,24 +34,30 @@
<string name="toast_fetch_finished_one">Dein Stundenplan wurde aktualisiert, doch eine Veranstaltung konnte nicht heruntergeladen werden.</string> <string name="toast_fetch_finished_one">Dein Stundenplan wurde aktualisiert, doch eine Veranstaltung konnte nicht heruntergeladen werden.</string>
<string name="toast_fetch_finished_other">Dein Stundenplan wurde aktualisiert, doch %d Veranstaltungen konnten nicht heruntergeladen werden.</string> <string name="toast_fetch_finished_other">Dein Stundenplan wurde aktualisiert, doch %d Veranstaltungen konnten nicht heruntergeladen werden.</string>
<string name="toast_info_reminder">Klick\' das Info-Icon unten, falls du Hilfe benötigst.</string> <string name="fetch_snack_info_reminder">Klick\' das Info-Icon unten, falls du Hilfe benötigst.</string>
<string name="fetch_bar_close_title">Schließen</string> <string name="fetch_bar_close_title">Schließen</string>
<string name="fetch_bar_close_desc">Schließe die Webseite, ohne etwas zu speichern</string> <string name="fetch_bar_close_desc">Schließe die Webseite, ohne etwas zu speichern</string>
<string name="fetch_bar_refresh_title">Aktualisieren</string> <string name="fetch_bar_refresh_title">Aktualisieren</string>
<string name="fetch_bar_refresh_desc">Aktualisiere die Webseite</string> <string name="fetch_bar_refresh_desc">Aktualisiere die Webseite</string>
<string name="fetch_bar_help_title">Hilfe</string> <string name="fetch_bar_help_title">Hilfe</string>
<string name="fetch_bar_help_desc">Zeige Hilfe</string> <string name="fetch_bar_help_desc">Zeige Hilfe</string>
<string name="fetch_bar_schedule_title">Stundenplan</string>
<string name="fetch_bar_schedule_desc">Gehe zu deinem Stundenplan</string>
<string name="fetch_bar_help_sheet_title">Wie du deinen Stundenplan in der App speichern kannst</string> <string name="fetch_bar_help_sheet_title">Wie du deinen Stundenplan in der App speichern kannst</string>
<string name="fetch_bar_help_sheet_content"> • Logge dich mit deinem Stud.IP-Login ein\n • Öffne deinen Stundenplan (falls er sich nicht automatisch öffnet)\n • Klicke auf den \'Speichern\'-Knopf, der unten in der rechten Ecke auftauchen sollte\n • Fertig!</string> <string name="fetch_bar_help_sheet_content"> • Logge dich mit deinem Stud.IP-Login ein\n • Öffne deinen Stundenplan, falls er sich nicht automatisch öffnet, indem du in der unteren Leiste auf das rechte Logo klickst, das wie ein Stift und ein Lineal aussieht\n • Klicke auf den \'Speichern\'-Knopf, der unten in der rechten Ecke auftaucht\n • Fertig!</string>
<string name="fetch_bar_save_desc">Speichere den Stundenplan in der App ab</string> <string name="fetch_bar_save_desc">Speichere den Stundenplan in der App ab</string>
<string name="schedule_fab_add_text">Neue Veranstaltung</string> <string name="schedule_fab_add_text">Neue Veranstaltung</string>
<string name="schedule_no_events">%ss hast du keine Veranstaltungen.</string>
<string name="save">Speichern</string> <string name="save">Speichern</string>
<string name="delete">Löschen</string> <string name="delete">Löschen</string>
<string name="sheet_schedule_update_delete_confirm">Sicher?</string> <string name="sheet_schedule_update_delete_confirm">Sicher?</string>
<string name="cancel">Abbrechen</string> <string name="cancel">Abbrechen</string>
<string name="confirm">Bestätigen</string> <string name="confirm">Bestätigen</string>
<string name="sheet_schedule_snack_delete">Veranstaltung wurde gelöscht.</string>
<string name="sheet_schedule_snack_add">Veranstaltung wurde hinzugefügt.</string>
<string name="sheet_schedule_snack_update">Veranstaltung wurde aktualisiert.</string>
<string name="sheet_schedule_update_header_edit">Bestehende Veranstaltung bearbeiten</string> <string name="sheet_schedule_update_header_edit">Bestehende Veranstaltung bearbeiten</string>
<string name="sheet_schedule_update_header_add">Neue Veranstaltung hinzufügen</string> <string name="sheet_schedule_update_header_add">Neue Veranstaltung hinzufügen</string>
<string name="sheet_schedule_update_title_hint">Veranstaltungstitel</string> <string name="sheet_schedule_update_title_hint">Veranstaltungstitel</string>
+8 -2
View File
@@ -35,24 +35,30 @@
<string name="toast_fetch_finished_one">Your schedule has been refreshed, but one item could not be saved.</string> <string name="toast_fetch_finished_one">Your schedule has been refreshed, but one item could not be saved.</string>
<string name="toast_fetch_finished_other">Your schedule has been refreshed, but %d items could not be saved.</string> <string name="toast_fetch_finished_other">Your schedule has been refreshed, but %d items could not be saved.</string>
<string name="toast_info_reminder">Click the info icon at the bottom if you need any help here.</string> <string name="fetch_snack_info_reminder">Click the info icon at the bottom if you need any help here.</string>
<string name="fetch_bar_close_title">Close</string> <string name="fetch_bar_close_title">Close</string>
<string name="fetch_bar_close_desc">Close the website without saving anything</string> <string name="fetch_bar_close_desc">Close the website without saving anything</string>
<string name="fetch_bar_refresh_title">Refresh</string> <string name="fetch_bar_refresh_title">Refresh</string>
<string name="fetch_bar_refresh_desc">Refresh the page</string> <string name="fetch_bar_refresh_desc">Refresh the page</string>
<string name="fetch_bar_help_title">Help</string> <string name="fetch_bar_help_title">Help</string>
<string name="fetch_bar_help_desc">Show help</string> <string name="fetch_bar_help_desc">Show help</string>
<string name="fetch_bar_schedule_title">Schedule</string>
<string name="fetch_bar_schedule_desc">Go to your schedule</string>
<string name="fetch_bar_help_sheet_title">How to save your timetable in the app</string> <string name="fetch_bar_help_sheet_title">How to save your timetable in the app</string>
<string name="fetch_bar_help_sheet_content"> • Log in with your Stud.IP login\n • Open your timetable view (in case it doesn\'t open automatically)\n • Click the \'save\' button that should appear in the bottom right corner\n • Done!</string> <string name="fetch_bar_help_sheet_content"> • Log in with your Stud.IP login\n • Open your timetable view, in case it doesn\'t open automatically, by clicking the rightmost icon in the bottom bar that looks like a pencil and a ruler\n • Click the \'save\' button that appears in the bottom right corner\n • Done!</string>
<string name="fetch_bar_save_desc">Save the timetable in the app</string> <string name="fetch_bar_save_desc">Save the timetable in the app</string>
<string name="schedule_fab_add_text">New event</string> <string name="schedule_fab_add_text">New event</string>
<string name="schedule_no_events">You don\'t have any events for %s.</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="sheet_schedule_update_delete_confirm">Sure?</string> <string name="sheet_schedule_update_delete_confirm">Sure?</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<string name="sheet_schedule_snack_delete">Event has been deleted.</string>
<string name="sheet_schedule_snack_add">Event has been added.</string>
<string name="sheet_schedule_snack_update">Event has been updated.</string>
<string name="sheet_schedule_update_header_edit">Edit existing event</string> <string name="sheet_schedule_update_header_edit">Edit existing event</string>
<string name="sheet_schedule_update_header_add">Add new event</string> <string name="sheet_schedule_update_header_add">Add new event</string>
<string name="sheet_schedule_update_title_hint">Event title</string> <string name="sheet_schedule_update_title_hint">Event title</string>