Archived
WeserPlaner 1.1.0: implemented canteen view in CompactOverviewFragment.kt
This commit is contained in:
Generated
-17
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="deploymentTargetDropDown">
|
|
||||||
<runningDeviceTargetSelectedWithDropDown>
|
|
||||||
<Target>
|
|
||||||
<type value="RUNNING_DEVICE_TARGET" />
|
|
||||||
<deviceKey>
|
|
||||||
<Key>
|
|
||||||
<type value="SERIAL_NUMBER" />
|
|
||||||
<value value="RF8R50XDV6F" />
|
|
||||||
</Key>
|
|
||||||
</deviceKey>
|
|
||||||
</Target>
|
|
||||||
</runningDeviceTargetSelectedWithDropDown>
|
|
||||||
<timeTargetWasSelectedWithDropDown value="2023-06-04T14:14:50.814776700Z" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 7,
|
"versionCode": 8,
|
||||||
"versionName": "1.0.3",
|
"versionName": "1.1.0",
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.denizk0461.weserplaner.data.getThemedColor
|
|||||||
import com.denizk0461.weserplaner.databinding.ItemCanteenBinding
|
import com.denizk0461.weserplaner.databinding.ItemCanteenBinding
|
||||||
import com.denizk0461.weserplaner.databinding.ItemCanteenLineBinding
|
import com.denizk0461.weserplaner.databinding.ItemCanteenLineBinding
|
||||||
import com.denizk0461.weserplaner.databinding.ItemIconBinding
|
import com.denizk0461.weserplaner.databinding.ItemIconBinding
|
||||||
|
import com.denizk0461.weserplaner.model.CanteenOffer
|
||||||
import com.denizk0461.weserplaner.model.CanteenOfferGroup
|
import com.denizk0461.weserplaner.model.CanteenOfferGroup
|
||||||
import com.denizk0461.weserplaner.model.CanteenOfferGroupElement
|
import com.denizk0461.weserplaner.model.CanteenOfferGroupElement
|
||||||
import com.denizk0461.weserplaner.values.DietaryPreferences
|
import com.denizk0461.weserplaner.values.DietaryPreferences
|
||||||
@@ -227,6 +228,173 @@ class CanteenOfferItemAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets new data according to the user's set dietary preferences and allergens. Inserts an item
|
||||||
|
* telling the user that no offers are present if that is the case.
|
||||||
|
*
|
||||||
|
* @param offers offers for a given day
|
||||||
|
* @param regex string to filter dietary preferences with
|
||||||
|
* @param allergenPrefs allergens the user wants to avoid
|
||||||
|
*/
|
||||||
|
fun setData(offers: List<CanteenOffer>, regex: String, allergenPrefs: String) {
|
||||||
|
/*
|
||||||
|
* Filter items that contain user-selected allergens or user-excluded dietary
|
||||||
|
* preferences.
|
||||||
|
*/
|
||||||
|
val filteredOffers = offers.filterElements(regex, allergenPrefs)
|
||||||
|
|
||||||
|
// Put elements into groups
|
||||||
|
val groupedOffers = filteredOffers.groupElements().distinct()
|
||||||
|
|
||||||
|
val difference = offers.size - filteredOffers.size
|
||||||
|
|
||||||
|
val newOffers = if (groupedOffers.isEmpty() && difference > 0) {
|
||||||
|
listOf(CanteenOfferGroup(
|
||||||
|
"ALL\$HIDDEN", 0, "", "", listOf(),
|
||||||
|
))
|
||||||
|
} else groupedOffers
|
||||||
|
|
||||||
|
// Set new items into the adapter
|
||||||
|
setNewData(
|
||||||
|
newOffers,
|
||||||
|
// Difference is calculated from the unfiltered and filtered data sets
|
||||||
|
difference,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups [CanteenOffer] elements by their categories into [CanteenOfferGroup] elements and
|
||||||
|
* filters for the user's dietary preferences.
|
||||||
|
*
|
||||||
|
* @return the grouped and filtered elements
|
||||||
|
*/
|
||||||
|
private fun List<CanteenOffer>.groupElements(): List<CanteenOfferGroup> {
|
||||||
|
|
||||||
|
return map { (_, date, dateId, category, _, canteen, _, _, _, _, _) ->
|
||||||
|
// Create new group elements to group the already filtered elements by their categories
|
||||||
|
CanteenOfferGroup(
|
||||||
|
date,
|
||||||
|
dateId,
|
||||||
|
category,
|
||||||
|
canteen,
|
||||||
|
filter {
|
||||||
|
it.category == category && it.canteen == canteen
|
||||||
|
}.map {
|
||||||
|
// Map the individual items to their respective groups
|
||||||
|
CanteenOfferGroupElement(
|
||||||
|
it.title,
|
||||||
|
it.price,
|
||||||
|
it.dietaryPreferences,
|
||||||
|
it.allergens
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the list of canteen offers by dietary and allergen preferences.
|
||||||
|
*
|
||||||
|
* @param regex string to filter dietary preferences with
|
||||||
|
* @param allergenPrefs allergens the user wants to avoid
|
||||||
|
* @return filtered list of offers
|
||||||
|
*/
|
||||||
|
private fun List<CanteenOffer>.filterElements(regex: String, allergenPrefs: String): List<CanteenOffer> {
|
||||||
|
|
||||||
|
// Get dietary preference regex
|
||||||
|
val prefsRegex = getPrefRegex(regex)
|
||||||
|
val allergenFilteredList = mutableListOf<CanteenOffer>()
|
||||||
|
|
||||||
|
// Filter offers not conforming to the dietary preferences set by the user
|
||||||
|
val newList = if (prefsRegex.toString() == DietaryPreferences.TEMPLATE_EMPTY) {
|
||||||
|
// Show all elements and skip filtering if no preference is set
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
// Filter for dietary preferences
|
||||||
|
this.filter {
|
||||||
|
prefsRegex.matches(it.dietaryPreferences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter offers containing allergens the user wishes to have hidden
|
||||||
|
return if (allergenPrefs.isBlank()) {
|
||||||
|
// If no preferences have been set by the user, skip the checks
|
||||||
|
newList
|
||||||
|
} else {
|
||||||
|
// If the user has set allergens, check each item individually
|
||||||
|
newList.forEach outer@{ item ->
|
||||||
|
|
||||||
|
// If the item in question doesn't contain any allergens, skip the check
|
||||||
|
if (item.allergens.isBlank()) {
|
||||||
|
// Add the item to the new list directly
|
||||||
|
allergenFilteredList.add(item)
|
||||||
|
} else {
|
||||||
|
// Check each individual allergen in the offering
|
||||||
|
item.allergens.split(",").forEach inner@{ allergen ->
|
||||||
|
|
||||||
|
// If an allergen can be found in the user-excluded list, stop checking
|
||||||
|
if (allergenPrefs.contains(allergen)) {
|
||||||
|
return@outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no allergens in the offering are present in the exclusion list, add it
|
||||||
|
allergenFilteredList.add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return the allergen-filtered list
|
||||||
|
allergenFilteredList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the user's dietary preferences and compiles them into a regular expression that can
|
||||||
|
* be used to filter out items that don't fulfil the preferences. Example:
|
||||||
|
* .......t..|........t.
|
||||||
|
* This will retrieve all items that marked as either vegetarian or vegan
|
||||||
|
*
|
||||||
|
* @param regex string to filter dietary preferences with
|
||||||
|
* @return regular expression object reflecting the user's preferences
|
||||||
|
*/
|
||||||
|
private fun getPrefRegex(regex: String): Regex {
|
||||||
|
// Retrieve the user's dietary preference as a string
|
||||||
|
|
||||||
|
// Return the 'empty' template string if no preference has been set or found
|
||||||
|
return if (regex == DietaryPreferences.TEMPLATE_EMPTY) {
|
||||||
|
Regex(DietaryPreferences.TEMPLATE_EMPTY)
|
||||||
|
} else {
|
||||||
|
// Create the variable to insert the regular expression into
|
||||||
|
var regexString = ""
|
||||||
|
|
||||||
|
// Create the object to store which preferences need to be met
|
||||||
|
val indices = mutableListOf<Int>()
|
||||||
|
|
||||||
|
// Find all preferences that need to be met
|
||||||
|
regex.forEachIndexed { index, c ->
|
||||||
|
if (c == DietaryPreferences.C_TRUE) indices.add(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs to be set to correctly assemble the regular expression
|
||||||
|
var isFirst = true
|
||||||
|
|
||||||
|
// Iterate through every preference that has been set
|
||||||
|
indices.forEach { index ->
|
||||||
|
// Set an OR if more than one preference needs to be met
|
||||||
|
if (!isFirst) regexString += '|'
|
||||||
|
|
||||||
|
// Add the expression looking for the single preference to the entire string
|
||||||
|
regexString += DietaryPreferences.TEMPLATE_EMPTY.substring(0 until index) +
|
||||||
|
DietaryPreferences.C_TRUE +
|
||||||
|
DietaryPreferences.TEMPLATE_EMPTY.substring(index + 1)
|
||||||
|
|
||||||
|
// Ensure that OR statements will be placed between the individual expressions
|
||||||
|
isFirst = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the string to a regular expression object
|
||||||
|
Regex(regexString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the data and calculates the difference between the old dataset and the newly provided
|
* Updates the data and calculates the difference between the old dataset and the newly provided
|
||||||
* dataset.
|
* dataset.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ fun Resources.Theme.showSnackBar(view: CoordinatorLayout, text: String, anchor:
|
|||||||
* @param text text to present in the snack bar
|
* @param text text to present in the snack bar
|
||||||
* @param anchor view to anchor the snack bar to
|
* @param anchor view to anchor the snack bar to
|
||||||
*/
|
*/
|
||||||
fun Resources.Theme.showErrorSnackBar(view: CoordinatorLayout, text: String, anchor: View? = null) {
|
fun Resources.Theme.showErrorSnackBar(view: View, text: String, anchor: View? = null) {
|
||||||
val s = Snackbar
|
val s = Snackbar
|
||||||
.make(view, text, Snackbar.LENGTH_SHORT)
|
.make(view, text, Snackbar.LENGTH_SHORT)
|
||||||
// Set colours to signify an error
|
// Set colours to signify an error
|
||||||
|
|||||||
@@ -80,7 +80,11 @@ class CanteenPageFragment : AppFragment<RecyclerViewBinding>(), CanteenOfferItem
|
|||||||
offerList.addAll(offers)
|
offerList.addAll(offers)
|
||||||
|
|
||||||
// Set new data
|
// Set new data
|
||||||
recyclerViewAdapter.setData(offers)
|
recyclerViewAdapter.setData(
|
||||||
|
offers,
|
||||||
|
viewModel.getDietaryPrefs().deconstruct(),
|
||||||
|
viewModel.preferenceAllergenConfig,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,168 +118,13 @@ class CanteenPageFragment : AppFragment<RecyclerViewBinding>(), CanteenOfferItem
|
|||||||
*/
|
*/
|
||||||
viewModel.dietaryPreferencesUpdate.observe(viewLifecycleOwner) {
|
viewModel.dietaryPreferencesUpdate.observe(viewLifecycleOwner) {
|
||||||
// Re-set data
|
// Re-set data
|
||||||
recyclerViewAdapter.setData(offerList)
|
recyclerViewAdapter.setData(offerList,
|
||||||
}
|
viewModel.getDietaryPrefs().deconstruct(),
|
||||||
}
|
viewModel.preferenceAllergenConfig,
|
||||||
|
|
||||||
private fun CanteenOfferItemAdapter.setData(offers: List<CanteenOffer>) {
|
|
||||||
/*
|
|
||||||
* Filter items that contain user-selected allergens or user-excluded dietary
|
|
||||||
* preferences.
|
|
||||||
*/
|
|
||||||
val filteredOffers = offers.filterElements()
|
|
||||||
|
|
||||||
// Put elements into groups
|
|
||||||
val groupedOffers = filteredOffers.groupElements().distinct()
|
|
||||||
|
|
||||||
val difference = offers.size - filteredOffers.size
|
|
||||||
|
|
||||||
val newOffers = if (groupedOffers.isEmpty() && difference > 0) {
|
|
||||||
listOf(CanteenOfferGroup(
|
|
||||||
"ALL\$HIDDEN", 0, "", "", listOf(),
|
|
||||||
))
|
|
||||||
} else groupedOffers
|
|
||||||
|
|
||||||
// Set new items into the adapter
|
|
||||||
setNewData(
|
|
||||||
newOffers,
|
|
||||||
// Difference is calculated from the unfiltered and filtered data sets
|
|
||||||
difference,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Groups [CanteenOffer] elements by their categories into [CanteenOfferGroup] elements and
|
|
||||||
* filters for the user's dietary preferences.
|
|
||||||
*
|
|
||||||
* @return the grouped and filtered elements
|
|
||||||
*/
|
|
||||||
private fun List<CanteenOffer>.groupElements(): List<CanteenOfferGroup> {
|
|
||||||
|
|
||||||
return map { (_, date, dateId, category, _, canteen, _, _, _, _, _) ->
|
|
||||||
// Create new group elements to group the already filtered elements by their categories
|
|
||||||
CanteenOfferGroup(
|
|
||||||
date,
|
|
||||||
dateId,
|
|
||||||
category,
|
|
||||||
canteen,
|
|
||||||
filter {
|
|
||||||
it.category == category && it.canteen == canteen
|
|
||||||
}.map {
|
|
||||||
// Map the individual items to their respective groups
|
|
||||||
CanteenOfferGroupElement(
|
|
||||||
it.title,
|
|
||||||
it.price,
|
|
||||||
it.dietaryPreferences,
|
|
||||||
it.allergens
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters the list of canteen offers by dietary and allergen preferences.
|
|
||||||
*
|
|
||||||
* @return filtered list of offers
|
|
||||||
*/
|
|
||||||
private fun List<CanteenOffer>.filterElements(): List<CanteenOffer> {
|
|
||||||
|
|
||||||
// Get dietary preference regex
|
|
||||||
val prefsRegex = getPrefRegex()
|
|
||||||
val allergenPrefs = viewModel.preferenceAllergenConfig
|
|
||||||
val allergenFilteredList = mutableListOf<CanteenOffer>()
|
|
||||||
|
|
||||||
// Filter offers not conforming to the dietary preferences set by the user
|
|
||||||
val newList = if (prefsRegex.toString() == DietaryPreferences.TEMPLATE_EMPTY) {
|
|
||||||
// Show all elements and skip filtering if no preference is set
|
|
||||||
this
|
|
||||||
} else {
|
|
||||||
// Filter for dietary preferences
|
|
||||||
this.filter {
|
|
||||||
prefsRegex.matches(it.dietaryPreferences)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter offers containing allergens the user wishes to have hidden
|
|
||||||
return if (allergenPrefs.isBlank()) {
|
|
||||||
// If no preferences have been set by the user, skip the checks
|
|
||||||
newList
|
|
||||||
} else {
|
|
||||||
// If the user has set allergens, check each item individually
|
|
||||||
newList.forEach outer@{ item ->
|
|
||||||
|
|
||||||
// If the item in question doesn't contain any allergens, skip the check
|
|
||||||
if (item.allergens.isBlank()) {
|
|
||||||
// Add the item to the new list directly
|
|
||||||
allergenFilteredList.add(item)
|
|
||||||
} else {
|
|
||||||
// Check each individual allergen in the offering
|
|
||||||
item.allergens.split(",").forEach inner@{ allergen ->
|
|
||||||
|
|
||||||
// If an allergen can be found in the user-excluded list, stop checking
|
|
||||||
if (allergenPrefs.contains(allergen)) {
|
|
||||||
return@outer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If no allergens in the offering are present in the exclusion list, add it
|
|
||||||
allergenFilteredList.add(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return the allergen-filtered list
|
|
||||||
allergenFilteredList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the user's dietary preferences and compiles them into a regular expression that can
|
|
||||||
* be used to filter out items that don't fulfil the preferences. Example:
|
|
||||||
* .......t..|........t.
|
|
||||||
* This will retrieve all items that marked as either vegetarian or vegan
|
|
||||||
* *
|
|
||||||
* @return regular expression object reflecting the user's preferences
|
|
||||||
*/
|
|
||||||
private fun getPrefRegex(): Regex {
|
|
||||||
// Retrieve the user's dietary preference as a string
|
|
||||||
val prefs = viewModel.getDietaryPrefs().deconstruct()
|
|
||||||
|
|
||||||
// Return the 'empty' template string if no preference has been set or found
|
|
||||||
return if (prefs == DietaryPreferences.TEMPLATE_EMPTY) {
|
|
||||||
Regex(DietaryPreferences.TEMPLATE_EMPTY)
|
|
||||||
} else {
|
|
||||||
// Create the variable to insert the regular expression into
|
|
||||||
var regexString = ""
|
|
||||||
|
|
||||||
// Create the object to store which preferences need to be met
|
|
||||||
val indices = mutableListOf<Int>()
|
|
||||||
|
|
||||||
// Find all preferences that need to be met
|
|
||||||
prefs.forEachIndexed { index, c ->
|
|
||||||
if (c == DietaryPreferences.C_TRUE) indices.add(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needs to be set to correctly assemble the regular expression
|
|
||||||
var isFirst = true
|
|
||||||
|
|
||||||
// Iterate through every preference that has been set
|
|
||||||
indices.forEach { index ->
|
|
||||||
// Set an OR if more than one preference needs to be met
|
|
||||||
if (!isFirst) regexString += '|'
|
|
||||||
|
|
||||||
// Add the expression looking for the single preference to the entire string
|
|
||||||
regexString += DietaryPreferences.TEMPLATE_EMPTY.substring(0 until index) +
|
|
||||||
DietaryPreferences.C_TRUE +
|
|
||||||
DietaryPreferences.TEMPLATE_EMPTY.substring(index+1)
|
|
||||||
|
|
||||||
// Ensure that OR statements will be placed between the individual expressions
|
|
||||||
isFirst = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile the string to a regular expression object
|
|
||||||
Regex(regexString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executed when an item has been clicked.
|
* Executed when an item has been clicked.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,11 +4,34 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.denizk0461.weserplaner.R
|
import com.denizk0461.weserplaner.R
|
||||||
|
import com.denizk0461.weserplaner.adapter.CanteenOfferItemAdapter
|
||||||
|
import com.denizk0461.weserplaner.data.setRainbowProgressCircle
|
||||||
|
import com.denizk0461.weserplaner.data.showErrorSnackBar
|
||||||
import com.denizk0461.weserplaner.databinding.FragmentCompactOverviewBinding
|
import com.denizk0461.weserplaner.databinding.FragmentCompactOverviewBinding
|
||||||
|
import com.denizk0461.weserplaner.model.CanteenOfferGroupElement
|
||||||
|
import com.denizk0461.weserplaner.sheet.AllergenSheet
|
||||||
|
import com.denizk0461.weserplaner.viewmodel.CompactOverviewViewModel
|
||||||
|
|
||||||
class CompactOverviewFragment : AppFragment<FragmentCompactOverviewBinding>() {
|
class CompactOverviewFragment : AppFragment<FragmentCompactOverviewBinding>(),
|
||||||
|
CanteenOfferItemAdapter.OnClickListener {
|
||||||
|
|
||||||
|
// View model
|
||||||
|
private val viewModel: CompactOverviewViewModel by viewModels()
|
||||||
|
|
||||||
|
// Recycler view adapter for displaying the most recent day's offers
|
||||||
|
private lateinit var offerAdapter: CanteenOfferItemAdapter
|
||||||
|
|
||||||
|
// Whether news for the selected canteen are available
|
||||||
|
private var areNewsAvailable = false
|
||||||
|
|
||||||
|
// Date for which offers will be shown
|
||||||
|
private var offerDate = ""
|
||||||
|
|
||||||
// Instantiate the view binding
|
// Instantiate the view binding
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
@@ -19,8 +42,217 @@ class CompactOverviewFragment : AppFragment<FragmentCompactOverviewBinding>() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Set progress circle colours
|
||||||
|
binding.swipeRefreshLayout.setRainbowProgressCircle()
|
||||||
|
|
||||||
|
// Set up on click listener for the settings button
|
||||||
binding.buttonSettings.setOnClickListener {
|
binding.buttonSettings.setOnClickListener {
|
||||||
findNavController().navigate(R.id.action_compact_overview_to_settings)
|
findNavController().navigate(R.id.action_compact_overview_to_settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up click listener for offer field
|
||||||
|
binding.fieldTimetable.setOnClickListener {
|
||||||
|
viewTimetable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up click listener for offer button
|
||||||
|
binding.buttonTimetable.setOnClickListener {
|
||||||
|
viewTimetable()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.eventRecyclerView.apply {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set header text for the offers
|
||||||
|
binding.textOfferHeader.text = getString(
|
||||||
|
R.string.compact_overview_header_offers,
|
||||||
|
getCurrentlySelectedCanteenName(),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.getCanteen().observe(viewLifecycleOwner) { canteen ->
|
||||||
|
// Decide and save whether news are available
|
||||||
|
areNewsAvailable = !(canteen == null || canteen.news.isBlank())
|
||||||
|
|
||||||
|
// Refresh offer subheader text
|
||||||
|
binding.textOfferSubheader.refreshOfferSubheader()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set offer headers to be selected so it scrolls (marquee)
|
||||||
|
binding.textOfferHeader.isSelected = true
|
||||||
|
binding.textOfferSubheader.isSelected = true
|
||||||
|
|
||||||
|
// Set up click listener for offer field
|
||||||
|
binding.fieldAllOffers.setOnClickListener {
|
||||||
|
viewAllOffers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up click listener for offer button
|
||||||
|
binding.buttonAllOffers.setOnClickListener {
|
||||||
|
viewAllOffers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up refresh button
|
||||||
|
binding.buttonRefreshOffers.setOnClickListener {
|
||||||
|
refreshOffers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the refresh layout to disabled to disallow pull-to-refresh
|
||||||
|
binding.swipeRefreshLayout.isEnabled = false
|
||||||
|
|
||||||
|
// Set up offer adapter
|
||||||
|
offerAdapter = CanteenOfferItemAdapter(
|
||||||
|
this,
|
||||||
|
viewModel.preferenceAllergen,
|
||||||
|
viewModel.preferenceColour,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set up offer recycler view
|
||||||
|
binding.offerRecyclerView.apply {
|
||||||
|
// Set page to scroll vertically
|
||||||
|
layoutManager = LinearLayoutManager(
|
||||||
|
context, LinearLayoutManager.VERTICAL, false
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter = offerAdapter
|
||||||
|
|
||||||
|
// Animate creation of new page
|
||||||
|
scheduleLayoutAnimation()
|
||||||
|
|
||||||
|
// Observe offers and re-set them if they change
|
||||||
|
viewModel.getOffersByDay(0).observe(viewLifecycleOwner) { offers ->
|
||||||
|
|
||||||
|
offerDate = if (offers.isEmpty()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
offers[0].date
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh offer subheader text
|
||||||
|
binding.textOfferSubheader.refreshOfferSubheader()
|
||||||
|
|
||||||
|
// Set new data
|
||||||
|
offerAdapter.setData(
|
||||||
|
offers,
|
||||||
|
viewModel.getDietaryPrefs().deconstruct(),
|
||||||
|
viewModel.preferenceAllergenConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Set header text for the offers in case the user has switched canteens
|
||||||
|
binding.textOfferHeader.text = getString(
|
||||||
|
R.string.compact_overview_header_offers,
|
||||||
|
getCurrentlySelectedCanteenName(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the appropriate text for the offer subheader.
|
||||||
|
*/
|
||||||
|
private fun AppCompatTextView.refreshOfferSubheader() {
|
||||||
|
// Set specific text if no offers have been found or fetched
|
||||||
|
if (offerDate.isBlank()) {
|
||||||
|
text = getString(R.string.compact_overview_subheader_offers_empty)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set text depending on whether news are available
|
||||||
|
text = getString(if (areNewsAvailable) {
|
||||||
|
R.string.compact_overview_subheader_offers_news
|
||||||
|
} else {
|
||||||
|
R.string.compact_overview_subheader_offers_no_news
|
||||||
|
}, offerDate)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to [EventFragment].
|
||||||
|
*/
|
||||||
|
private fun viewTimetable() {
|
||||||
|
findNavController().navigate(R.id.action_compact_overview_to_schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to [CanteenFragment].
|
||||||
|
*/
|
||||||
|
private fun viewAllOffers() {
|
||||||
|
findNavController().navigate(R.id.action_compact_overview_to_canteen)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the localised string for the canteen selected by the user.
|
||||||
|
*
|
||||||
|
* @return the localised canteen name
|
||||||
|
*/
|
||||||
|
private fun getCurrentlySelectedCanteenName(): String = getString(
|
||||||
|
when (viewModel.preferenceCanteen) {
|
||||||
|
0 -> R.string.mensa_uni
|
||||||
|
1 -> R.string.cafe_central
|
||||||
|
2 -> R.string.mensa_nw1
|
||||||
|
3 -> R.string.cafeteria_gw2
|
||||||
|
4 -> R.string.mensa_neustadt
|
||||||
|
5 -> R.string.mensa_werder
|
||||||
|
6 -> R.string.mensa_airport
|
||||||
|
7 -> R.string.mensa_bhv
|
||||||
|
8 -> R.string.cafeteria_bhv
|
||||||
|
9 -> R.string.mensa_hfk
|
||||||
|
else -> R.string.mensa_uni
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the canteen offers and refreshes them in the app.
|
||||||
|
*/
|
||||||
|
private fun refreshOffers() {
|
||||||
|
// Tell the swipe refresh layout to refresh
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = true
|
||||||
|
|
||||||
|
// Retrieve new offers from the website(s)
|
||||||
|
viewModel.fetchOffers(viewModel.preferenceCanteen, onFinish = {
|
||||||
|
// Check if the fragment is still active
|
||||||
|
if (_binding != null) {
|
||||||
|
/*
|
||||||
|
* Tell the swipe refresh layout to stop refreshing.
|
||||||
|
*/
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}, onError = {
|
||||||
|
// Check if the fragment is still active
|
||||||
|
if (_binding != null) {
|
||||||
|
// Tell the user that an error occurred
|
||||||
|
context.theme?.showErrorSnackBar(
|
||||||
|
binding.rootView,
|
||||||
|
getString(R.string.canteen_fetch_error)
|
||||||
|
)
|
||||||
|
/*
|
||||||
|
* Tell the swipe refresh layout to stop refreshing.
|
||||||
|
*/
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(offer: CanteenOfferGroupElement, category: String) {
|
||||||
|
openBottomSheet(
|
||||||
|
AllergenSheet().also { sheet ->
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable("offer", offer)
|
||||||
|
bundle.putString("category", category)
|
||||||
|
bundle.putBoolean("preferenceColour", viewModel.preferenceColour)
|
||||||
|
sheet.arguments = bundle
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongClick(offer: CanteenOfferGroupElement): Boolean {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import com.denizk0461.weserplaner.values.DietaryPreferences
|
|||||||
class CanteenPageViewModel(app: Application) : AppViewModel(app) {
|
class CanteenPageViewModel(app: Application) : AppViewModel(app) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all canteen offers that match given day.
|
* Retrieves all canteen offers that match a given day.
|
||||||
*
|
*
|
||||||
* @return all canteen offers matching the given day exposed through a LiveData object
|
* @return all canteen offers matching the given day exposed through a LiveData object
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.denizk0461.weserplaner.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.denizk0461.weserplaner.data.StwParser
|
||||||
|
import com.denizk0461.weserplaner.model.CanteenOffer
|
||||||
|
import com.denizk0461.weserplaner.model.OfferCanteen
|
||||||
|
import com.denizk0461.weserplaner.values.DietaryPreferences
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CompactOverviewViewModel(application: Application) : AppViewModel(application) {
|
||||||
|
|
||||||
|
// Instantiate a parser, should the user want to refresh the canteen offers
|
||||||
|
private val parser = StwParser(application)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the canteen offers asynchronously. Updates will be provided through a LiveData object.
|
||||||
|
*
|
||||||
|
* @param canteen canteen from which to fetch offers from
|
||||||
|
* @param onFinish action to execute when the operation has finished
|
||||||
|
* @param onError action to execute when the operation encountered an error
|
||||||
|
*/
|
||||||
|
fun fetchOffers(
|
||||||
|
canteen: Int,
|
||||||
|
onFinish: () -> Unit,
|
||||||
|
onError: () -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
parser.parse(canteen, onFinish, onError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offers that match given day.
|
||||||
|
*
|
||||||
|
* @return all canteen offers matching the given day exposed through a LiveData object
|
||||||
|
*/
|
||||||
|
fun getOffersByDay(day: Int): LiveData<List<CanteenOffer>> =
|
||||||
|
repo.getOffersByDay(day)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the canteen stored in the database. Should always be 0 or 1.
|
||||||
|
*
|
||||||
|
* @return canteen
|
||||||
|
*/
|
||||||
|
fun getCanteen(): LiveData<OfferCanteen> = repo.getCanteen()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This value determines which canteen the user has selected.
|
||||||
|
*/
|
||||||
|
val preferenceCanteen: Int
|
||||||
|
get() = repo.getPreferenceCanteen()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the user's dietary preferences.
|
||||||
|
*
|
||||||
|
* @return dietary preferences
|
||||||
|
*/
|
||||||
|
fun getDietaryPrefs(): DietaryPreferences.Object = repo.getDietaryPrefsAsObject()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This value determines which allergens the user wants to have displayed or hidden.
|
||||||
|
*/
|
||||||
|
val preferenceAllergenConfig: String
|
||||||
|
get() = repo.getPreferenceAllergenConfig()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This value determines whether the user wants to have allergens marked.
|
||||||
|
*/
|
||||||
|
val preferenceAllergen: Boolean
|
||||||
|
get() = repo.getPreferenceAllergen()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This value determines which canteen the user has selected.
|
||||||
|
*/
|
||||||
|
val preferenceColour: Boolean
|
||||||
|
get() = repo.getPreferenceColour()
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
<?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.constraintlayout.widget.ConstraintLayout 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:id="@+id/root_view"
|
||||||
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.CompactOverviewFragment">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -61,4 +64,171 @@
|
|||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/field_timetable"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:background="@drawable/selectable_item_background"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:layout_marginTop="24dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/text_event_header"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/lato_bolditalic"
|
||||||
|
android:text="@string/compact_overview_header_events"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_timetable"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:id="@+id/button_timetable"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
app:icon="@drawable/arrow_forward"
|
||||||
|
app:iconPadding="0dp"
|
||||||
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/text_event_header"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/text_event_header"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:iconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:tooltipText="@string/compact_overview_button_events_hint"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/event_recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:scrollbarStyle="outsideOverlay"
|
||||||
|
android:layoutAnimation="@anim/layout_animation_fall_down"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/field_all_offers"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/selectable_item_background"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_refresh_offers"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/text_offer_header"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/lato_bolditalic"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:scrollHorizontally="true"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/text_offer_subheader"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:scrollHorizontally="true"/>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Outlined"
|
||||||
|
android:id="@+id/button_refresh_offers"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
app:icon="@drawable/refresh"
|
||||||
|
app:iconPadding="0dp"
|
||||||
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_all_offers"
|
||||||
|
app:iconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:tooltipText="@string/compact_overview_button_offers_hint"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:id="@+id/button_all_offers"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
app:icon="@drawable/arrow_forward"
|
||||||
|
app:iconPadding="0dp"
|
||||||
|
app:strokeColor="?attr/colorOutlineVariant"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:iconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:tooltipText="@string/compact_overview_button_offers_hint"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipe_refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/offer_recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:scrollbarStyle="outsideOverlay"
|
||||||
|
android:layoutAnimation="@anim/layout_animation_fall_down"/>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -25,12 +25,21 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="introduction_welcome_header">Willkommen zu &app_name;!</string>
|
<string name="introduction_welcome_header">Willkommen zu &app_name;!</string>
|
||||||
<string name="introduction_welcome_description">&app_name; ist eine inoffizielle App für Studierende der Universität Bremen, mit welcher du deinen Stundenplan immer griffbereit hast. Erstelle deine eigenen wöchentlichen Veranstaltungen und bearbeite sie, oder lade sie direkt von Stud.IP herunter.\n\nDu kannst außerdem alle Angebote aus den Mensen in Bremen und Bremerhaven einsehen. Filtere nach Präferenzen mit den Knöpfen oben am Bildschirm, oder gehe in die Einstellungen, um zu konfigurieren, gegen welche Stoffe du allergisch bist, und die App wird so gut wie möglich versuchen, dir nur Angebote zu zeigen, die relevant für dich sind.\n\nKlicke auf den Knopf unten, um deinen Stud.IP-Stundenplan direkt herunterzuladen! Solltest du dies überspringen, kannst du dies jederzeit in den Einstellungen der App nachholen.</string>
|
<string name="introduction_welcome_description">&app_name; ist eine inoffizielle App für Studierende der Universität Bremen, mit welcher du deinen Stundenplan immer griffbereit hast. Trage deine eigenen wöchentlichen Veranstaltungen ein und bearbeite sie, oder lade sie direkt von Stud.IP herunter.\n\nDu kannst außerdem alle Angebote aus den Mensen in Bremen und Bremerhaven einsehen. Filtere nach Präferenzen mit den Knöpfen oben am Bildschirm, oder gehe in die Einstellungen, um zu konfigurieren, gegen welche Stoffe du allergisch bist, und die App wird so gut wie möglich versuchen, dir nur Angebote zu zeigen, die relevant für dich sind.\n\nKlicke auf den Knopf unten, um deinen Stud.IP-Stundenplan direkt herunterzuladen! Solltest du dies überspringen, kannst du dies jederzeit in den Einstellungen der App nachholen.</string>
|
||||||
<string name="introduction_button_download">Lade deinen Stud.IP-Stundenplan herunter</string>
|
<string name="introduction_button_download">Lade deinen Stud.IP-Stundenplan herunter</string>
|
||||||
<string name="introduction_or">– oder –</string>
|
<string name="introduction_or">– oder –</string>
|
||||||
<string name="introduction_button_continue">Gehe direkt zur App</string>
|
<string name="introduction_button_continue">Gehe direkt zur App</string>
|
||||||
|
|
||||||
<string name="compact_overview_button_settings_hint">App-Einstellungen</string>
|
<string name="compact_overview_button_settings_hint">App-Einstellungen</string>
|
||||||
|
<string name="compact_overview_button_events_hint">Stundenplan</string>
|
||||||
|
<string name="compact_overview_button_refresh_offers_hint">Aktualisiere Angebote</string>
|
||||||
|
<string name="compact_overview_button_offers_hint">Alle Angebote</string>
|
||||||
|
<string name="compact_overview_header_events">Veranstaltungen heute</string>
|
||||||
|
<string name="compact_overview_header_offers">Im Angebot bei der %s</string>
|
||||||
|
<string name="compact_overview_subheader_offers_empty">Keine Infos verfügbar</string>
|
||||||
|
<string name="compact_overview_subheader_offers_no_news">Angebote vom %s</string>
|
||||||
|
<string name="compact_overview_subheader_offers_news">Angebote vom %s; Neuigkeiten verfügbar</string>
|
||||||
|
|
||||||
<string name="fragment_button_back_to_overview_hint">Zurück zur Übersicht</string>
|
<string name="fragment_button_back_to_overview_hint">Zurück zur Übersicht</string>
|
||||||
|
|
||||||
<string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string>
|
<string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string>
|
||||||
|
|||||||
@@ -26,12 +26,21 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="introduction_welcome_header">Welcome to &app_name;!</string>
|
<string name="introduction_welcome_header">Welcome to &app_name;!</string>
|
||||||
<string name="introduction_welcome_description">&app_name; is an unofficial companion app for students of the University of Bremen that allows you to keep your own schedule in close reach. Create your own weekly events and edit them as you wish, or download them straight from Stud.IP.\n\nYou can also view the offers for all of the canteens in Bremen and Bremerhaven. Filter by preferences using the buttons at the top of the canteen screen, or go into the settings to configure what substances you are allergic against, and the app will do its best to only show you offers that are relevant for you.\n\nClick the button below to download your schedule right away! If you skip this, you\'ll always have the option to download your schedule from the app\'s settings later.</string>
|
<string name="introduction_welcome_description">&app_name; is an unofficial companion app for students of the University of Bremen that allows you to keep your own schedule in close reach. Enter your own weekly events and edit them as you wish, or download them straight from Stud.IP.\n\nYou can also view the offers for all of the canteens in Bremen and Bremerhaven. Filter by preferences using the buttons at the top of the canteen screen, or go into the settings to configure what substances you are allergic against, and the app will do its best to only show you offers that are relevant for you.\n\nClick the button below to download your schedule right away! If you skip this, you\'ll always have the option to download your schedule from the app\'s settings later.</string>
|
||||||
<string name="introduction_button_download">Download your Stud.IP schedule</string>
|
<string name="introduction_button_download">Download your Stud.IP schedule</string>
|
||||||
<string name="introduction_or">– or –</string>
|
<string name="introduction_or">– or –</string>
|
||||||
<string name="introduction_button_continue">Continue to the app</string>
|
<string name="introduction_button_continue">Continue to the app</string>
|
||||||
|
|
||||||
<string name="compact_overview_button_settings_hint">App settings</string>
|
<string name="compact_overview_button_settings_hint">App settings</string>
|
||||||
|
<string name="compact_overview_button_events_hint">Timetable</string>
|
||||||
|
<string name="compact_overview_button_refresh_offers_hint">Refresh offers</string>
|
||||||
|
<string name="compact_overview_button_offers_hint">All offers</string>
|
||||||
|
<string name="compact_overview_header_events">Today\'s events</string>
|
||||||
|
<string name="compact_overview_header_offers">On offer at the %s</string>
|
||||||
|
<string name="compact_overview_subheader_offers_empty">No information available</string>
|
||||||
|
<string name="compact_overview_subheader_offers_no_news">Offers from %s</string>
|
||||||
|
<string name="compact_overview_subheader_offers_news">Offers from %s; news available</string>
|
||||||
|
|
||||||
<string name="fragment_button_back_to_overview_hint">Back to overview</string>
|
<string name="fragment_button_back_to_overview_hint">Back to overview</string>
|
||||||
|
|
||||||
<string name="fetch_error_snack">Something went wrong!</string>
|
<string name="fetch_error_snack">Something went wrong!</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user