WeserPlaner 1.1.0: implemented canteen view in CompactOverviewFragment.kt

This commit is contained in:
denizk0461
2023-06-04 20:45:38 +02:00
parent 35283f3544
commit 97b713a578
11 changed files with 688 additions and 188 deletions
-17
View File
@@ -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>
+2 -2
View File
@@ -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>
+10 -1
View File
@@ -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>
+10 -1
View File
@@ -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>