Archived
StwParser.kt now collects all its data before adding everything to the database all at once, fixing problems where LiveData would update before all transactions were finished; ViewPagers have been updated to use Adapters extending FragmentStateAdapter instead of RecyclerView.Adapter
This commit is contained in:
+1
-1
@@ -8,10 +8,10 @@
|
|||||||
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="AndroidLintStringFormatTrivial" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="AndroidLintStringFormatTrivial" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="AndroidLintTypos" enabled="true" level="TYPO" enabled_by_default="true" />
|
<inspection_tool class="AndroidLintTypos" enabled="true" level="TYPO" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="BooleanParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
|
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
|
||||||
<inspection_tool class="DirectUseOfResultType" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
<inspection_tool class="DirectUseOfResultType" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="IncompleteDestructuring" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="IncompleteDestructuring" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="KDocMissingDocumentation" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="NoConstructorMigration" enabled="true" level="ERROR" enabled_by_default="true" />
|
<inspection_tool class="NoConstructorMigration" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
<inspection_tool class="NonExhaustiveWhenStatementMigration" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="NonExhaustiveWhenStatementMigration" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="PublicApiImplicitType" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
<inspection_tool class="PublicApiImplicitType" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ class CanteenOfferItemAdapter(
|
|||||||
// Retrieve item for current position
|
// Retrieve item for current position
|
||||||
val currentItem = offers[position]
|
val currentItem = offers[position]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Children of the line container MUST be removed before any are added, as otherwise, during
|
||||||
|
* recycling, lines will be duplicated when they are scrolled off the visible screen area
|
||||||
|
* and then scrolled onto the screen again. In other terms, if removeAllViews() wasn't
|
||||||
|
* called and the user was to scroll down a list and then back up again, the items at the
|
||||||
|
* top of the list would be added again, creating duplicates.
|
||||||
|
*/
|
||||||
|
holder.binding.lineContainer.removeAllViews()
|
||||||
|
|
||||||
// If an item has been added to mark that there are no offers available, tell the user
|
// If an item has been added to mark that there are no offers available, tell the user
|
||||||
if (currentItem.category == "NO\$ITEMS") {
|
if (currentItem.category == "NO\$ITEMS") {
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package com.denizk0461.studip.adapter
|
package com.denizk0461.studip.adapter
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import androidx.fragment.app.Fragment
|
||||||
import android.view.ViewGroup
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import com.denizk0461.studip.fragment.CanteenPageFragment
|
||||||
import com.denizk0461.studip.databinding.ItemScrollablePageBinding
|
|
||||||
import com.denizk0461.studip.model.CanteenOfferGroup
|
import com.denizk0461.studip.model.CanteenOfferGroup
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom RecyclerView adapter for managing multiple pages of canteen offers in a RecyclerView or
|
* Custom ViewPager adapter for managing multiple pages of canteen offers.
|
||||||
* ViewPager.
|
|
||||||
*
|
*
|
||||||
|
* @param fragmentActivity parent fragment activity
|
||||||
* @param offers all offers for a given canteen, grouped by category (not filtered by day at
|
* @param offers all offers for a given canteen, grouped by category (not filtered by day at
|
||||||
* this point)
|
* this point)
|
||||||
* @param daysCovered tells for how many days offers are available for.
|
* @param daysCovered tells for how many days offers are available for.
|
||||||
@@ -20,55 +19,24 @@ import com.denizk0461.studip.model.CanteenOfferGroup
|
|||||||
* @param displayAllergens whether the user wants allergens to be marked
|
* @param displayAllergens whether the user wants allergens to be marked
|
||||||
*/
|
*/
|
||||||
class CanteenOfferPageAdapter(
|
class CanteenOfferPageAdapter(
|
||||||
|
fragmentActivity: FragmentActivity,
|
||||||
private var offers: List<CanteenOfferGroup>,
|
private var offers: List<CanteenOfferGroup>,
|
||||||
private var daysCovered: Int,
|
private var daysCovered: Int,
|
||||||
private val onClickListener: CanteenOfferItemAdapter.OnClickListener,
|
private val onClickListener: CanteenOfferItemAdapter.OnClickListener,
|
||||||
private val displayAllergens: Boolean,
|
private val displayAllergens: Boolean,
|
||||||
) : RecyclerView.Adapter<CanteenOfferPageAdapter.CanteenOfferPageViewHolder>() {
|
) : FragmentStateAdapter(fragmentActivity) {
|
||||||
|
|
||||||
/**
|
|
||||||
* View holder class for parent class
|
|
||||||
*
|
|
||||||
* @param binding view binding object
|
|
||||||
*/
|
|
||||||
class CanteenOfferPageViewHolder(val binding: ItemScrollablePageBinding) : RecyclerView.ViewHolder(binding.root)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CanteenOfferPageViewHolder =
|
|
||||||
CanteenOfferPageViewHolder(
|
|
||||||
ItemScrollablePageBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Returns the count of days covered
|
// Returns the count of days covered
|
||||||
override fun getItemCount(): Int = daysCovered
|
override fun getItemCount(): Int = daysCovered
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: CanteenOfferPageViewHolder, position: Int) {
|
override fun createFragment(position: Int): Fragment =
|
||||||
|
CanteenPageFragment(
|
||||||
holder.binding.pageRecyclerView.apply {
|
offers,
|
||||||
// Set page to horizontally scroll
|
|
||||||
layoutManager = LinearLayoutManager(
|
|
||||||
holder.binding.root.context, LinearLayoutManager.VERTICAL, false
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create new adapter for every page. Attribute position denotes day that will be set up
|
|
||||||
* by the newly created adapter (0 = Monday, 4 = Friday)
|
|
||||||
* TODO check if the filtered list is empty, and tell the user if it is
|
|
||||||
*/
|
|
||||||
adapter = CanteenOfferItemAdapter(
|
|
||||||
offers.filter { it.dateId == position },
|
|
||||||
onClickListener,
|
onClickListener,
|
||||||
displayAllergens,
|
displayAllergens,
|
||||||
|
position,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Animate creation of new page
|
|
||||||
scheduleLayoutAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the entire list of items
|
* Update the entire list of items
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,61 +1,31 @@
|
|||||||
package com.denizk0461.studip.adapter
|
package com.denizk0461.studip.adapter
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import androidx.fragment.app.Fragment
|
||||||
import android.view.ViewGroup
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.denizk0461.studip.model.StudIPEvent
|
import com.denizk0461.studip.model.StudIPEvent
|
||||||
import com.denizk0461.studip.databinding.ItemScrollablePageBinding
|
import com.denizk0461.studip.fragment.EventPageFragment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom RecyclerView adapter for managing multiple pages of Stud.IP events in a RecyclerView or
|
* Custom ViewPager adapter for managing multiple pages of Stud.IP events.
|
||||||
* ViewPager.
|
|
||||||
*
|
*
|
||||||
|
* @param fragmentActivity parent fragment activity
|
||||||
* @param events all events (not filtered by day at this point)
|
* @param events all events (not filtered by day at this point)
|
||||||
* @param onClickListener for managing click and long press events
|
* @param onClickListener for managing click and long press events
|
||||||
*/
|
*/
|
||||||
class StudIPEventPageAdapter(
|
class StudIPEventPageAdapter(
|
||||||
|
fragmentActivity: FragmentActivity,
|
||||||
private var events: List<StudIPEvent>,
|
private var events: List<StudIPEvent>,
|
||||||
private val onClickListener: StudIPEventItemAdapter.OnClickListener,
|
private val onClickListener: StudIPEventItemAdapter.OnClickListener,
|
||||||
) : RecyclerView.Adapter<StudIPEventPageAdapter.EventPageViewHolder>() {
|
) : FragmentStateAdapter(fragmentActivity) {
|
||||||
|
|
||||||
/**
|
|
||||||
* View holder class for parent class
|
|
||||||
*
|
|
||||||
* @param binding view binding object
|
|
||||||
*/
|
|
||||||
class EventPageViewHolder(val binding: ItemScrollablePageBinding) : RecyclerView.ViewHolder(binding.root)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventPageViewHolder = EventPageViewHolder(
|
|
||||||
ItemScrollablePageBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Assume that there will always be 5 pages, for 5 days - Monday through Friday.
|
* Assume that there will always be 7 pages, for 7 days - Monday through Sunday.
|
||||||
* TODO is it right to assume that the user only has events on weekdays?
|
|
||||||
*/
|
*/
|
||||||
override fun getItemCount(): Int = 5
|
override fun getItemCount(): Int = 7
|
||||||
|
|
||||||
// Set up page
|
override fun createFragment(position: Int): Fragment =
|
||||||
override fun onBindViewHolder(holder: EventPageViewHolder, position: Int) {
|
EventPageFragment(events, position, onClickListener)
|
||||||
holder.binding.pageRecyclerView.apply {
|
|
||||||
// Set page to horizontally scroll
|
|
||||||
layoutManager = LinearLayoutManager(holder.binding.root.context, LinearLayoutManager.VERTICAL, false)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create new adapter for every page. Attribute position denotes day that will be set up
|
|
||||||
* by the newly created adapter (0 = Monday, 4 = Friday)
|
|
||||||
*/
|
|
||||||
adapter = StudIPEventItemAdapter(events, currentDay = position, onClickListener) // TODO check if empty
|
|
||||||
|
|
||||||
// Animate creation of new page
|
|
||||||
scheduleLayoutAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the entire list of items
|
* Update the entire list of items
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.denizk0461.studip.data
|
package com.denizk0461.studip.data
|
||||||
|
|
||||||
import com.denizk0461.studip.db.AppRepository
|
|
||||||
import com.denizk0461.studip.model.*
|
import com.denizk0461.studip.model.*
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
@@ -63,8 +62,15 @@ class StwParser {
|
|||||||
Dependencies.repo.nukeOffers()
|
Dependencies.repo.nukeOffers()
|
||||||
|
|
||||||
// Fetch offers from the chosen canteen
|
// Fetch offers from the chosen canteen
|
||||||
parseFromPage(link, Dependencies.repo)
|
val (dates, canteens, categories, items) = parseFromPage(link)
|
||||||
|
|
||||||
|
// Save everything all at once into the database
|
||||||
|
with (Dependencies.repo) {
|
||||||
|
insertDates(dates)
|
||||||
|
insertCanteens(canteens)
|
||||||
|
insertCategories(categories)
|
||||||
|
insertItems(items)
|
||||||
|
}
|
||||||
// TODO implement onRefresh(Int)
|
// TODO implement onRefresh(Int)
|
||||||
|
|
||||||
// Action call once all fetching activities have finished
|
// Action call once all fetching activities have finished
|
||||||
@@ -75,9 +81,14 @@ class StwParser {
|
|||||||
* Fetch and parse the plan of a specific canteen.
|
* Fetch and parse the plan of a specific canteen.
|
||||||
*
|
*
|
||||||
* @param url link to the canteen to be scraped. Must be a subpage of stw-bremen.de
|
* @param url link to the canteen to be scraped. Must be a subpage of stw-bremen.de
|
||||||
* @param repo reference to the app repository used to save the items to persistent storage
|
|
||||||
*/
|
*/
|
||||||
private fun parseFromPage(url: String, repo: AppRepository) {
|
private fun parseFromPage(url: String): StwResults {
|
||||||
|
|
||||||
|
val dates = mutableListOf<OfferDate>()
|
||||||
|
val canteens = mutableListOf<OfferCanteen>()
|
||||||
|
val categories = mutableListOf<OfferCategory>()
|
||||||
|
val items = mutableListOf<OfferItem>()
|
||||||
|
|
||||||
// Used to store a parsed date string without creating a new variable on every loop
|
// Used to store a parsed date string without creating a new variable on every loop
|
||||||
var date: String
|
var date: String
|
||||||
|
|
||||||
@@ -120,8 +131,8 @@ class StwParser {
|
|||||||
// Trim off excess line breaks
|
// Trim off excess line breaks
|
||||||
openingHours = openingHours.trim()
|
openingHours = openingHours.trim()
|
||||||
|
|
||||||
// Save the canteen to persistent storage
|
// Save the canteen to its list
|
||||||
repo.insert(
|
canteens.add(
|
||||||
OfferCanteen(
|
OfferCanteen(
|
||||||
canteenId,
|
canteenId,
|
||||||
doc.getElementsByClass("pane-title")[1].text(),
|
doc.getElementsByClass("pane-title")[1].text(),
|
||||||
@@ -148,12 +159,8 @@ class StwParser {
|
|||||||
*/
|
*/
|
||||||
date = "${rawDate[0]}${rawDate[1].monthToNumber()}."
|
date = "${rawDate[0]}${rawDate[1].monthToNumber()}."
|
||||||
|
|
||||||
/*
|
// Save the date to its list
|
||||||
* Save the date to persistent storage.
|
dates.add(OfferDate(dateId, date))
|
||||||
* TODO this will duplicate date entries for every canteen that is fetched. This is
|
|
||||||
* inefficient.
|
|
||||||
*/
|
|
||||||
repo.insert(OfferDate(dateId, date))
|
|
||||||
|
|
||||||
dateHasItems = false
|
dateHasItems = false
|
||||||
|
|
||||||
@@ -165,8 +172,8 @@ class StwParser {
|
|||||||
// Retrieve the category text
|
// Retrieve the category text
|
||||||
val categoryTitle = category.getElementsByClass("category-name")[0].text()
|
val categoryTitle = category.getElementsByClass("category-name")[0].text()
|
||||||
|
|
||||||
// Save the category to persistent storage
|
// Save the category to its list
|
||||||
repo.insert(OfferCategory(categoryId, dateId, canteenId, categoryTitle))
|
categories.add(OfferCategory(categoryId, dateId, canteenId, categoryTitle))
|
||||||
|
|
||||||
// Iterate through all items in a category
|
// Iterate through all items in a category
|
||||||
category
|
category
|
||||||
@@ -195,8 +202,8 @@ class StwParser {
|
|||||||
|
|
||||||
val filteredText = tableRows[1].getFilteredText()
|
val filteredText = tableRows[1].getFilteredText()
|
||||||
|
|
||||||
// Save the item to persistent storage
|
// Save the item to its list
|
||||||
repo.insert(
|
items.add(
|
||||||
OfferItem(
|
OfferItem(
|
||||||
itemId,
|
itemId,
|
||||||
categoryId,
|
categoryId,
|
||||||
@@ -219,8 +226,8 @@ class StwParser {
|
|||||||
* is available. Text will then be handled in the adapter class.
|
* is available. Text will then be handled in the adapter class.
|
||||||
*/
|
*/
|
||||||
if (!dateHasItems) {
|
if (!dateHasItems) {
|
||||||
repo.insert(OfferCategory(categoryId, dateId, canteenId, "NO\$ITEMS"))
|
categories.add(OfferCategory(categoryId, dateId, canteenId, "NO\$ITEMS"))
|
||||||
repo.insert(
|
items.add(
|
||||||
OfferItem(
|
OfferItem(
|
||||||
itemId,
|
itemId,
|
||||||
categoryId,
|
categoryId,
|
||||||
@@ -240,6 +247,8 @@ class StwParser {
|
|||||||
}
|
}
|
||||||
// Increment the canteen ID to avoid conflict
|
// Increment the canteen ID to avoid conflict
|
||||||
canteenId += 1
|
canteenId += 1
|
||||||
|
|
||||||
|
return StwResults(dates, canteens, categories, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,6 +375,13 @@ class StwParser {
|
|||||||
// .replace("<p>", "")
|
// .replace("<p>", "")
|
||||||
// .replace("</p>", "")
|
// .replace("</p>", "")
|
||||||
|
|
||||||
|
private data class StwResults(
|
||||||
|
val dates: List<OfferDate>,
|
||||||
|
val canteens: List<OfferCanteen>,
|
||||||
|
val categories: List<OfferCategory>,
|
||||||
|
val items: List<OfferItem>,
|
||||||
|
)
|
||||||
|
|
||||||
// Image links to all dietary preferences used for checking whether a preference is met
|
// Image links to all dietary preferences used for checking whether a preference is met
|
||||||
private val imageLinkPrefFair = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
private val imageLinkPrefFair = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
||||||
private val imageLinkPrefFish = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
private val imageLinkPrefFish = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ interface AppDAO {
|
|||||||
*
|
*
|
||||||
* @param events objects to be saved to the database
|
* @param events objects to be saved to the database
|
||||||
*/
|
*/
|
||||||
@Insert fun insert(events: List<StudIPEvent>)
|
@Insert fun insertEvents(events: List<StudIPEvent>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all Stud.IP events from the database.
|
* Deletes all Stud.IP events from the database.
|
||||||
@@ -128,6 +128,13 @@ interface AppDAO {
|
|||||||
*/
|
*/
|
||||||
@Insert fun insert(date: OfferDate)
|
@Insert fun insert(date: OfferDate)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of dates into the database.
|
||||||
|
*
|
||||||
|
* @param dates objects to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insertDates(dates: List<OfferDate>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a canteen into the database.
|
* Inserts a canteen into the database.
|
||||||
*
|
*
|
||||||
@@ -135,6 +142,13 @@ interface AppDAO {
|
|||||||
*/
|
*/
|
||||||
@Insert fun insert(canteen: OfferCanteen)
|
@Insert fun insert(canteen: OfferCanteen)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of canteens into the database.
|
||||||
|
*
|
||||||
|
* @param canteens objects to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insertCanteens(canteens: List<OfferCanteen>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a canteen offer category into the database.
|
* Inserts a canteen offer category into the database.
|
||||||
*
|
*
|
||||||
@@ -142,6 +156,13 @@ interface AppDAO {
|
|||||||
*/
|
*/
|
||||||
@Insert fun insert(category: OfferCategory)
|
@Insert fun insert(category: OfferCategory)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of categories into the database.
|
||||||
|
*
|
||||||
|
* @param categories objects to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insertCategories(categories: List<OfferCategory>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a canteen offer item into the database.
|
* Inserts a canteen offer item into the database.
|
||||||
*
|
*
|
||||||
@@ -149,6 +170,13 @@ interface AppDAO {
|
|||||||
*/
|
*/
|
||||||
@Insert fun insert(item: OfferItem)
|
@Insert fun insert(item: OfferItem)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of items into the database.
|
||||||
|
*
|
||||||
|
* @param items objects to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insertItems(items: List<OfferItem>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the opening hours of the canteen as a string.
|
* Retrieves the opening hours of the canteen as a string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class AppRepository(app: Application) {
|
|||||||
*
|
*
|
||||||
* @param events objects to be saved to the database
|
* @param events objects to be saved to the database
|
||||||
*/
|
*/
|
||||||
fun insertEvents(events: List<StudIPEvent>) { dao.insert(events) }
|
fun insertEvents(events: List<StudIPEvent>) { dao.insertEvents(events) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all Stud.IP events from the database.
|
* Deletes all Stud.IP events from the database.
|
||||||
@@ -173,6 +173,13 @@ class AppRepository(app: Application) {
|
|||||||
*/
|
*/
|
||||||
fun insert(date: OfferDate) { dao.insert(date) }
|
fun insert(date: OfferDate) { dao.insert(date) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of dates into the database.
|
||||||
|
*
|
||||||
|
* @param dates objects to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insertDates(dates: List<OfferDate>) { dao.insertDates(dates) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a canteen into the database.
|
* Inserts a canteen into the database.
|
||||||
*
|
*
|
||||||
@@ -180,6 +187,13 @@ class AppRepository(app: Application) {
|
|||||||
*/
|
*/
|
||||||
fun insert(canteen: OfferCanteen) { dao.insert(canteen) }
|
fun insert(canteen: OfferCanteen) { dao.insert(canteen) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of canteens into the database.
|
||||||
|
*
|
||||||
|
* @param canteens objects to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insertCanteens(canteens: List<OfferCanteen>) { dao.insertCanteens(canteens) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a canteen offer category into the database.
|
* Inserts a canteen offer category into the database.
|
||||||
*
|
*
|
||||||
@@ -187,6 +201,13 @@ class AppRepository(app: Application) {
|
|||||||
*/
|
*/
|
||||||
fun insert(category: OfferCategory) { dao.insert(category) }
|
fun insert(category: OfferCategory) { dao.insert(category) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of categories into the database.
|
||||||
|
*
|
||||||
|
* @param categories objects to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insertCategories(categories: List<OfferCategory>) { dao.insertCategories(categories) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a canteen offer item into the database.
|
* Inserts a canteen offer item into the database.
|
||||||
*
|
*
|
||||||
@@ -194,6 +215,13 @@ class AppRepository(app: Application) {
|
|||||||
*/
|
*/
|
||||||
fun insert(item: OfferItem) { dao.insert(item) }
|
fun insert(item: OfferItem) { dao.insert(item) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of items into the database.
|
||||||
|
*
|
||||||
|
* @param items objects to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insertItems(items: List<OfferItem>) { dao.insertItems(items) }
|
||||||
|
|
||||||
// --- settings preferences --- //
|
// --- settings preferences --- //
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.denizk0461.studip.R
|
import com.denizk0461.studip.R
|
||||||
import com.denizk0461.studip.adapter.CanteenOfferItemAdapter
|
import com.denizk0461.studip.adapter.CanteenOfferItemAdapter
|
||||||
@@ -145,7 +146,11 @@ class CanteenFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListener {
|
|||||||
|
|
||||||
// Set up the view pager's adapter
|
// Set up the view pager's adapter
|
||||||
viewPagerAdapter = CanteenOfferPageAdapter(
|
viewPagerAdapter = CanteenOfferPageAdapter(
|
||||||
listOf(), 0, this, viewModel.preferenceAllergen
|
activity as FragmentActivity,
|
||||||
|
listOf(),
|
||||||
|
0,
|
||||||
|
this,
|
||||||
|
viewModel.preferenceAllergen,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assign the adapter to the view pager
|
// Assign the adapter to the view pager
|
||||||
@@ -163,7 +168,9 @@ class CanteenFragment : AppFragment(), CanteenOfferItemAdapter.OnClickListener {
|
|||||||
val groupedElements = offers.groupElements().distinct()
|
val groupedElements = offers.groupElements().distinct()
|
||||||
|
|
||||||
// Find all dates stored in the database
|
// Find all dates stored in the database
|
||||||
val newDates = /*groupedElements.map { it.date }.distinct() TODO */viewModel.getDates()
|
// val newDates = /*groupedElements.map { it.date }.distinct() TODO */viewModel.getDates()
|
||||||
|
|
||||||
|
val newDates = groupedElements.map { it.date }.distinct()
|
||||||
|
|
||||||
// Update the date count stored in this fragment
|
// Update the date count stored in this fragment
|
||||||
dateSize = newDates.size
|
dateSize = newDates.size
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.denizk0461.studip.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.denizk0461.studip.adapter.CanteenOfferItemAdapter
|
||||||
|
import com.denizk0461.studip.adapter.StudIPEventPageAdapter
|
||||||
|
import com.denizk0461.studip.databinding.RecyclerViewBinding
|
||||||
|
import com.denizk0461.studip.model.CanteenOfferGroup
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment that is instantiated by [StudIPEventPageAdapter] to display individual days' pages and
|
||||||
|
* their events.
|
||||||
|
*
|
||||||
|
* @param offers all offers for a given canteen, grouped by category (not filtered by day at
|
||||||
|
* this point)
|
||||||
|
* @param onClickListener for managing click and long press events
|
||||||
|
* @param displayAllergens whether the user wants allergens to be marked
|
||||||
|
* @param currentDay current day
|
||||||
|
*/
|
||||||
|
class CanteenPageFragment(
|
||||||
|
private var offers: List<CanteenOfferGroup>,
|
||||||
|
private val onClickListener: CanteenOfferItemAdapter.OnClickListener,
|
||||||
|
private val displayAllergens: Boolean,
|
||||||
|
private val currentDay: Int,
|
||||||
|
) : AppFragment() {
|
||||||
|
|
||||||
|
// Nullable view binding reference
|
||||||
|
private var _binding: RecyclerViewBinding? = null
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Non-null reference to the view binding. This property is only valid between onCreateView and
|
||||||
|
* onDestroyView.
|
||||||
|
*/
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
// Instantiate the view binding
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = RecyclerViewBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.recyclerView.apply {
|
||||||
|
// Set page to scroll vertically
|
||||||
|
layoutManager = LinearLayoutManager(
|
||||||
|
context, LinearLayoutManager.VERTICAL, false
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create new adapter for every page. Attribute position denotes day that will be set up
|
||||||
|
* by the newly created adapter (0 = Monday, 4 = Friday)
|
||||||
|
* TODO check if the filtered list is empty, and tell the user if it is
|
||||||
|
*/
|
||||||
|
val o = offers.filter { it.dateId == currentDay }
|
||||||
|
|
||||||
|
adapter = CanteenOfferItemAdapter(
|
||||||
|
o,
|
||||||
|
onClickListener,
|
||||||
|
displayAllergens,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Animate creation of new page
|
||||||
|
scheduleLayoutAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.denizk0461.studip.fragment
|
package com.denizk0461.studip.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
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.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.denizk0461.studip.R
|
import com.denizk0461.studip.R
|
||||||
import com.denizk0461.studip.adapter.StudIPEventItemAdapter
|
import com.denizk0461.studip.adapter.StudIPEventItemAdapter
|
||||||
@@ -39,13 +42,12 @@ class EventFragment : AppFragment() {
|
|||||||
private var dayOfWeek: Int = 0
|
private var dayOfWeek: Int = 0
|
||||||
|
|
||||||
// Titles for the view pager's tabs
|
// Titles for the view pager's tabs
|
||||||
private val dayStrings = listOf(
|
private lateinit var weekdays: Array<String>
|
||||||
R.string.monday,
|
|
||||||
R.string.tuesday,
|
// private var viewPagerScrollPosition: Parcelable? = null
|
||||||
R.string.wednesday,
|
private var viewPagerPosition = -1
|
||||||
R.string.thursday,
|
|
||||||
R.string.friday,
|
private var hasFragmentStarted = false
|
||||||
)
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -56,24 +58,29 @@ class EventFragment : AppFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
/*
|
weekdays = resources.getStringArray(R.array.weekdays)
|
||||||
* Determine the current day.
|
|
||||||
* TODO expand to weekend
|
// Determine the current day
|
||||||
*/
|
|
||||||
dayOfWeek = when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
dayOfWeek = when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
||||||
Calendar.TUESDAY -> 1
|
Calendar.TUESDAY -> 1
|
||||||
Calendar.WEDNESDAY -> 2
|
Calendar.WEDNESDAY -> 2
|
||||||
Calendar.THURSDAY -> 3
|
Calendar.THURSDAY -> 3
|
||||||
Calendar.FRIDAY -> 4
|
Calendar.FRIDAY -> 4
|
||||||
else -> 0
|
Calendar.SATURDAY -> 5
|
||||||
|
Calendar.SUNDAY -> 6
|
||||||
|
else -> 0 // assume Monday
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the view pager's adapter
|
// Set up the view pager's adapter
|
||||||
viewPagerAdapter = StudIPEventPageAdapter(listOf(), object : StudIPEventItemAdapter.OnClickListener {
|
viewPagerAdapter =
|
||||||
|
StudIPEventPageAdapter(activity as FragmentActivity, listOf(), object : StudIPEventItemAdapter.OnClickListener {
|
||||||
override fun onClick(event: StudIPEvent) {
|
override fun onClick(event: StudIPEvent) {
|
||||||
// TODO implement functionality or delete
|
// TODO implement functionality or delete
|
||||||
}
|
}
|
||||||
override fun onLongClick(event: StudIPEvent): Boolean {
|
override fun onLongClick(event: StudIPEvent): Boolean {
|
||||||
|
// Save the current page of the ViewPager
|
||||||
|
viewPagerPosition = binding.viewPager.currentItem
|
||||||
|
|
||||||
// Open a bottom sheet to edit the event
|
// Open a bottom sheet to edit the event
|
||||||
openBottomSheet(ScheduleUpdateSheet(event, onUpdate = { eventToUpdate ->
|
openBottomSheet(ScheduleUpdateSheet(event, onUpdate = { eventToUpdate ->
|
||||||
viewModel.update(eventToUpdate)
|
viewModel.update(eventToUpdate)
|
||||||
@@ -89,7 +96,7 @@ class EventFragment : AppFragment() {
|
|||||||
|
|
||||||
// Create and attach the object mediating the tabs for the view pager
|
// Create and attach the object mediating the tabs for the view pager
|
||||||
TabLayoutMediator(binding.dayTabLayout, binding.viewPager) { tab, position ->
|
TabLayoutMediator(binding.dayTabLayout, binding.viewPager) { tab, position ->
|
||||||
tab.text = getString(dayStrings[position])
|
tab.text = weekdays[position]
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
// Set up LiveData observer to refresh the view on update
|
// Set up LiveData observer to refresh the view on update
|
||||||
@@ -97,16 +104,53 @@ class EventFragment : AppFragment() {
|
|||||||
// Update the item list in the view pager's adapter
|
// Update the item list in the view pager's adapter
|
||||||
viewPagerAdapter.setNewItems(events)
|
viewPagerAdapter.setNewItems(events)
|
||||||
|
|
||||||
// Scroll to the current day
|
if (!hasFragmentStarted) {
|
||||||
|
// Scroll to the current day, if no page has been stored to be scrolled to
|
||||||
switchToCurrentDayView()
|
switchToCurrentDayView()
|
||||||
|
hasFragmentStarted = true
|
||||||
|
// } else {
|
||||||
|
// /*
|
||||||
|
// * If a page was previously saved, scroll to that one instead. This is meant to
|
||||||
|
// * prevent jumping from
|
||||||
|
// */
|
||||||
|
// binding.viewPager.currentItem = viewPagerPosition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// viewPagerPosition = binding.viewPager.currentItem
|
||||||
|
// Log.d("AAA?", "onpause: $viewPagerPosition")
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
binding.viewPager.currentItem = viewPagerPosition
|
||||||
|
Log.d("AAA?", "onresume: pos: $viewPagerPosition; vpp: ${binding.viewPager.currentItem}")
|
||||||
|
|
||||||
// Scroll to the current day
|
// Scroll to the current day
|
||||||
switchToCurrentDayView()
|
// switchToCurrentDayView()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the layout changes (e.g. device rotation) but NOT when the fragment is selected
|
||||||
|
* from the bottom navigation view.
|
||||||
|
*/
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
Log.d("AAA?", "${binding.viewPager.currentItem}")
|
||||||
|
outState.putInt(
|
||||||
|
"viewPagerCurrentPage",
|
||||||
|
binding.viewPager.currentItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||||
|
super.onViewStateRestored(savedInstanceState)
|
||||||
|
val restoredPage = savedInstanceState?.getInt("viewPagerCurrentPage") ?: 0
|
||||||
|
viewPagerPosition = restoredPage
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate the view binding
|
// Invalidate the view binding
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.denizk0461.studip.fragment
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.denizk0461.studip.adapter.StudIPEventPageAdapter
|
||||||
|
import com.denizk0461.studip.adapter.StudIPEventItemAdapter
|
||||||
|
import com.denizk0461.studip.databinding.RecyclerViewBinding
|
||||||
|
import com.denizk0461.studip.model.StudIPEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment that is instantiated by [StudIPEventPageAdapter] to display individual days' pages and
|
||||||
|
* their events.
|
||||||
|
*
|
||||||
|
* @param events list of all events (still not filtered by day at this point)
|
||||||
|
* @param currentDay current day that is used to show only events of a given day (0 = Monday,
|
||||||
|
* 4 = Friday)
|
||||||
|
* @param onClickListener used for listening to clicks and long presses
|
||||||
|
*/
|
||||||
|
class EventPageFragment(
|
||||||
|
private val events: List<StudIPEvent>,
|
||||||
|
private val currentDay: Int,
|
||||||
|
private val onClickListener: StudIPEventItemAdapter.OnClickListener,
|
||||||
|
) : AppFragment() {
|
||||||
|
|
||||||
|
// Nullable view binding reference
|
||||||
|
private var _binding: RecyclerViewBinding? = null
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Non-null reference to the view binding. This property is only valid between onCreateView and
|
||||||
|
* onDestroyView.
|
||||||
|
*/
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
// Instantiate the view binding
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
_binding = RecyclerViewBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.recyclerView.apply {
|
||||||
|
// Set page to scroll vertically
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create new adapter for every page. Attribute position denotes day that will be set up
|
||||||
|
* by the newly created adapter (0 = Monday, 4 = Friday)
|
||||||
|
*/
|
||||||
|
adapter = StudIPEventItemAdapter(events, currentDay, onClickListener) // TODO check if empty
|
||||||
|
|
||||||
|
// Animate creation of new page
|
||||||
|
scheduleLayoutAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scrollbars="vertical">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/page_recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layoutAnimation="@anim/layout_animation_fall_down">
|
|
||||||
|
|
||||||
</androidx.recyclerview.widget.RecyclerView>
|
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:layoutAnimation="@anim/layout_animation_fall_down"/>
|
||||||
@@ -9,6 +9,16 @@
|
|||||||
<string name="saturday">Samstag</string>
|
<string name="saturday">Samstag</string>
|
||||||
<string name="sunday">Sonntag</string>
|
<string name="sunday">Sonntag</string>
|
||||||
|
|
||||||
|
<string-array name="weekdays">
|
||||||
|
<item>Montag</item>
|
||||||
|
<item>Dienstag</item>
|
||||||
|
<item>Mittwoch</item>
|
||||||
|
<item>Donnerstag</item>
|
||||||
|
<item>Freitag</item>
|
||||||
|
<item>Samstag</item>
|
||||||
|
<item>Sonntag</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string>
|
<string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string>
|
||||||
|
|
||||||
<string name="save">Speichern</string>
|
<string name="save">Speichern</string>
|
||||||
@@ -121,9 +131,6 @@
|
|||||||
<string name="settings_highlight_title">Markiere meinen nächsten Kurs</string>
|
<string name="settings_highlight_title">Markiere meinen nächsten Kurs</string>
|
||||||
<string name="settings_highlight_subtitle">Funktioniert nur wenn Kurse sich nicht überschneiden!</string>
|
<string name="settings_highlight_subtitle">Funktioniert nur wenn Kurse sich nicht überschneiden!</string>
|
||||||
|
|
||||||
<string name="settings_quarter_title">Nutze akademisches Viertel</string>
|
|
||||||
<string name="settings_quarter_subtitle">Wo es passt</string>
|
|
||||||
|
|
||||||
<string name="settings_allergens_title">Markiere Angebote mit Allergenen mit einem Stern*</string>
|
<string name="settings_allergens_title">Markiere Angebote mit Allergenen mit einem Stern*</string>
|
||||||
<string name="settings_allergens_subtitle">Wird auch für Zusatzstoffe genutzt</string>
|
<string name="settings_allergens_subtitle">Wird auch für Zusatzstoffe genutzt</string>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,16 @@
|
|||||||
<string name="saturday">Saturday</string>
|
<string name="saturday">Saturday</string>
|
||||||
<string name="sunday">Sunday</string>
|
<string name="sunday">Sunday</string>
|
||||||
|
|
||||||
|
<string-array name="weekdays">
|
||||||
|
<item>Monday</item>
|
||||||
|
<item>Tuesday</item>
|
||||||
|
<item>Wednesday</item>
|
||||||
|
<item>Thursday</item>
|
||||||
|
<item>Friday</item>
|
||||||
|
<item>Saturday</item>
|
||||||
|
<item>Sunday</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string name="fetch_error_snack">Something went wrong!</string>
|
<string name="fetch_error_snack">Something went wrong!</string>
|
||||||
|
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user