Archived
Fully documented all Kotlin files and added TODOs to functions that need to be updated in some way
This commit is contained in:
@@ -9,7 +9,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import com.denizk0461.studip.R
|
import com.denizk0461.studip.R
|
||||||
import com.denizk0461.studip.data.Dependencies
|
import com.denizk0461.studip.data.Dependencies
|
||||||
import com.denizk0461.studip.databinding.ActivityMainBinding
|
import com.denizk0461.studip.databinding.ActivityMainBinding
|
||||||
import com.denizk0461.studip.db.EventRepository
|
import com.denizk0461.studip.db.AppRepository
|
||||||
import com.denizk0461.studip.fragment.EventFragment
|
import com.denizk0461.studip.fragment.EventFragment
|
||||||
import com.denizk0461.studip.fragment.CanteenFragment
|
import com.denizk0461.studip.fragment.CanteenFragment
|
||||||
import com.denizk0461.studip.fragment.SettingsFragment
|
import com.denizk0461.studip.fragment.SettingsFragment
|
||||||
@@ -38,7 +38,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
* Instantiate repository object that is accessed by the fragments' view models to retrieve
|
* Instantiate repository object that is accessed by the fragments' view models to retrieve
|
||||||
* data.
|
* data.
|
||||||
*/
|
*/
|
||||||
Dependencies.repo = EventRepository(application)
|
Dependencies.repo = AppRepository(application)
|
||||||
|
|
||||||
// Inflate view binding and bind to this activity
|
// Inflate view binding and bind to this activity
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
|||||||
@@ -2,18 +2,27 @@ package com.denizk0461.studip.adapter
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.denizk0461.studip.data.Misc
|
import com.denizk0461.studip.R
|
||||||
import com.denizk0461.studip.databinding.ItemCanteenBinding
|
import com.denizk0461.studip.databinding.ItemCanteenBinding
|
||||||
import com.denizk0461.studip.databinding.ItemCanteenLineBinding
|
import com.denizk0461.studip.databinding.ItemCanteenLineBinding
|
||||||
import com.denizk0461.studip.databinding.ItemIconBinding
|
import com.denizk0461.studip.databinding.ItemIconBinding
|
||||||
import com.denizk0461.studip.model.CanteenOffer
|
|
||||||
import com.denizk0461.studip.model.CanteenOfferGroup
|
import com.denizk0461.studip.model.CanteenOfferGroup
|
||||||
|
|
||||||
class CanteenOfferItemAdapter(private val offers: List<CanteenOfferGroup>) : RecyclerView.Adapter<CanteenOfferItemAdapter.OfferViewHolder>() {
|
/**
|
||||||
|
* Custom RecyclerView adapter that lays out the offers of a given canteen on a given day.
|
||||||
|
*
|
||||||
|
* @param offers list of all offers filtered by canteen and day, grouped by category
|
||||||
|
*/
|
||||||
|
class CanteenOfferItemAdapter(
|
||||||
|
private val offers: List<CanteenOfferGroup>
|
||||||
|
) : RecyclerView.Adapter<CanteenOfferItemAdapter.OfferViewHolder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View holder class for parent class
|
||||||
|
*
|
||||||
|
* @param binding view binding object
|
||||||
|
*/
|
||||||
class OfferViewHolder(val binding: ItemCanteenBinding) : RecyclerView.ViewHolder(binding.root)
|
class OfferViewHolder(val binding: ItemCanteenBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OfferViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OfferViewHolder {
|
||||||
@@ -26,20 +35,32 @@ class CanteenOfferItemAdapter(private val offers: List<CanteenOfferGroup>) : Rec
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the amount of offers for a given canteen and day
|
||||||
override fun getItemCount(): Int = offers.size
|
override fun getItemCount(): Int = offers.size
|
||||||
|
|
||||||
|
// TODO inflate a view saying "no events" if none are available for a given day
|
||||||
override fun onBindViewHolder(holder: OfferViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: OfferViewHolder, position: Int) {
|
||||||
|
// Retrieve item for current position
|
||||||
val currentItem = offers[position]
|
val currentItem = offers[position]
|
||||||
|
|
||||||
|
// Set category text / header of the item
|
||||||
holder.binding.textCategory.text = currentItem.category
|
holder.binding.textCategory.text = currentItem.category
|
||||||
|
|
||||||
|
// Create a new view (line) for each item displayed within one category
|
||||||
currentItem.offers.forEach { offer ->
|
currentItem.offers.forEach { offer ->
|
||||||
|
// Inflate the new line without binding it to the line container
|
||||||
val line = ItemCanteenLineBinding.inflate(
|
val line = ItemCanteenLineBinding.inflate(
|
||||||
LayoutInflater.from(holder.binding.root.context),
|
LayoutInflater.from(holder.binding.root.context),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set text values
|
||||||
line.textContent.text = offer.title
|
line.textContent.text = offer.title
|
||||||
line.textPrice.text = offer.price
|
line.textPrice.text = offer.price
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check which dietary preferences are met by this item. This is used to set visual
|
||||||
|
* icons on each line. Example: a leaf on vegan items.
|
||||||
|
*/
|
||||||
val indices = arrayListOf<Int>()
|
val indices = arrayListOf<Int>()
|
||||||
offer.dietaryPreferences.filterIndexed { index, char ->
|
offer.dietaryPreferences.filterIndexed { index, char ->
|
||||||
if (char == 't') {
|
if (char == 't') {
|
||||||
@@ -47,25 +68,44 @@ class CanteenOfferItemAdapter(private val offers: List<CanteenOfferGroup>) : Rec
|
|||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no dietary preferences are met, set the view to display a neutral icon
|
||||||
if (indices.isEmpty()) indices.add(10)
|
if (indices.isEmpty()) indices.add(10)
|
||||||
|
|
||||||
|
// Iterate through all icons that need to be displayed
|
||||||
indices.forEach { index ->
|
indices.forEach { index ->
|
||||||
|
// Inflate a new icon holder
|
||||||
val img = ItemIconBinding.inflate(LayoutInflater.from(holder.binding.root.context))
|
val img = ItemIconBinding.inflate(LayoutInflater.from(holder.binding.root.context))
|
||||||
// holder.binding.root.resources.getDrawable(Misc.indexToDrawable[index]!!, holder.binding.root.context.theme)
|
|
||||||
img.imageView.setImageDrawable(holder.binding.root.context.getDrawable(Misc.indexToDrawable[index]!!))
|
// Set the appropriate icon
|
||||||
|
img.imageView.setImageDrawable(
|
||||||
|
holder.binding.root.context.getDrawable(indexToDrawable[index]!!)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bind the new icon to the line view
|
||||||
line.imageViewContainer.addView(img.root)
|
line.imageViewContainer.addView(img.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind the new line to the line container
|
||||||
holder.binding.lineContainer.addView(line.root)
|
holder.binding.lineContainer.addView(line.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// holder.binding.textCategory.text = currentItem.category
|
|
||||||
// holder.binding.tempContent.text = "${currentItem.title} • ${currentItem.price}"
|
|
||||||
|
|
||||||
// TODO inflate view saying "no offers!" or sth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface OnClickListener {
|
/**
|
||||||
// fun onClick(event: StudIPEvent)
|
* Provides a converting function between the index of a dietary preference and its according
|
||||||
// fun onLongClick(event: StudIPEvent)
|
* icon.
|
||||||
// }
|
*/
|
||||||
|
private val indexToDrawable: Map<Int, Int> = mapOf(
|
||||||
|
0 to R.drawable.handshake,
|
||||||
|
1 to R.drawable.fish,
|
||||||
|
2 to R.drawable.chicken,
|
||||||
|
3 to R.drawable.sheep,
|
||||||
|
4 to R.drawable.yoga,
|
||||||
|
5 to R.drawable.cow,
|
||||||
|
6 to R.drawable.pig,
|
||||||
|
7 to R.drawable.leaf,
|
||||||
|
8 to R.drawable.carrot,
|
||||||
|
9 to R.drawable.deer,
|
||||||
|
10 to R.drawable.circle,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,26 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.denizk0461.studip.databinding.ItemScrollablePageBinding
|
import com.denizk0461.studip.databinding.ItemScrollablePageBinding
|
||||||
import com.denizk0461.studip.model.CanteenOfferGroup
|
import com.denizk0461.studip.model.CanteenOfferGroup
|
||||||
|
|
||||||
class CanteenOfferPageAdapter(private var offers: List<CanteenOfferGroup>, private var daysCovered: Int, private var prefsRegex: Regex) : RecyclerView.Adapter<CanteenOfferPageAdapter.CanteenOfferPageViewHolder>() {
|
/**
|
||||||
|
* Custom RecyclerView adapter for managing multiple pages of canteen offers in a RecyclerView or
|
||||||
|
* ViewPager.
|
||||||
|
*
|
||||||
|
* @param offers all offers for a given canteen, grouped by category (not filtered by day at
|
||||||
|
* this point)
|
||||||
|
* @param daysCovered tells for how many days offers are available for.
|
||||||
|
* Example: if the next two weeks are available, Monday through Friday and
|
||||||
|
* excluding weekends, this value should be 10.
|
||||||
|
*/
|
||||||
|
class CanteenOfferPageAdapter(
|
||||||
|
private var offers: List<CanteenOfferGroup>,
|
||||||
|
private var daysCovered: Int,
|
||||||
|
) : RecyclerView.Adapter<CanteenOfferPageAdapter.CanteenOfferPageViewHolder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View holder class for parent class
|
||||||
|
*
|
||||||
|
* @param binding view binding object
|
||||||
|
*/
|
||||||
class CanteenOfferPageViewHolder(val binding: ItemScrollablePageBinding) : RecyclerView.ViewHolder(binding.root)
|
class CanteenOfferPageViewHolder(val binding: ItemScrollablePageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CanteenOfferPageViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CanteenOfferPageViewHolder =
|
||||||
@@ -20,22 +38,39 @@ class CanteenOfferPageAdapter(private var offers: List<CanteenOfferGroup>, priva
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 onBindViewHolder(holder: CanteenOfferPageViewHolder, position: Int) {
|
||||||
|
|
||||||
holder.binding.pageRecyclerView.apply {
|
holder.binding.pageRecyclerView.apply {
|
||||||
|
// Set page to horizontally scroll
|
||||||
layoutManager = LinearLayoutManager(holder.binding.root.context, LinearLayoutManager.VERTICAL, false)
|
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 = CanteenOfferItemAdapter(offers.filter { it.dateId == position }) // TODO check if empty
|
adapter = CanteenOfferItemAdapter(offers.filter { it.dateId == position }) // TODO check if empty
|
||||||
|
|
||||||
|
// Animate creation of new page
|
||||||
scheduleLayoutAnimation()
|
scheduleLayoutAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the entire list of items
|
||||||
|
*
|
||||||
|
* @param items the new set of items
|
||||||
|
*/
|
||||||
fun setNewItems(items: List<CanteenOfferGroup>, daysCovered: Int) {
|
fun setNewItems(items: List<CanteenOfferGroup>, daysCovered: Int) {
|
||||||
|
// Update the count of days covered
|
||||||
this.daysCovered = daysCovered
|
this.daysCovered = daysCovered
|
||||||
|
|
||||||
|
// Update items
|
||||||
offers = items
|
offers = items
|
||||||
|
|
||||||
|
// Force an update of the view. TODO replace with individual updates, as this is inefficient
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,14 +10,40 @@ import com.denizk0461.studip.model.StudIPEvent
|
|||||||
import com.denizk0461.studip.databinding.ItemEventBinding
|
import com.denizk0461.studip.databinding.ItemEventBinding
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom RecyclerView adapter that lays out the Stud.IP events of a given day.
|
||||||
|
*
|
||||||
|
* @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 StudIPEventItemAdapter(
|
class StudIPEventItemAdapter(
|
||||||
events: List<StudIPEvent>, private val currentDay: Int, private val onClickListener: OnClickListener
|
events: List<StudIPEvent>,
|
||||||
|
private val currentDay: Int,
|
||||||
|
private val onClickListener: OnClickListener,
|
||||||
) : RecyclerView.Adapter<StudIPEventItemAdapter.EventViewHolder>() {
|
) : RecyclerView.Adapter<StudIPEventItemAdapter.EventViewHolder>() {
|
||||||
|
|
||||||
|
// Filter events to only show those of a given day
|
||||||
private val filteredEvents: List<StudIPEvent> = events.filter { it.day == currentDay }
|
private val filteredEvents: List<StudIPEvent> = events.filter { it.day == currentDay }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieve an instance of Calendar to check whether the adapter's day matches the current day.
|
||||||
|
* TODO this can be optimised by checking in StudIPEventPageAdapter.kt and delivering a boolean
|
||||||
|
*/
|
||||||
private val currentCalendar = Calendar.getInstance()
|
private val currentCalendar = Calendar.getInstance()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Used to disallow highlighting more than one course at a time, since only a single course
|
||||||
|
* could be coming up at a given time.
|
||||||
|
*/
|
||||||
private var isAnyCourseHighlighted = false
|
private var isAnyCourseHighlighted = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View holder class for parent class
|
||||||
|
*
|
||||||
|
* @param binding view binding object
|
||||||
|
*/
|
||||||
class EventViewHolder(val binding: ItemEventBinding) : RecyclerView.ViewHolder(binding.root)
|
class EventViewHolder(val binding: ItemEventBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
|
||||||
@@ -30,57 +56,92 @@ class StudIPEventItemAdapter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the amount of items to the size of the filtered list, so only events of the current day
|
||||||
override fun getItemCount(): Int = filteredEvents.size
|
override fun getItemCount(): Int = filteredEvents.size
|
||||||
|
|
||||||
|
// TODO inflate a view saying "no events" if none are available for a given day
|
||||||
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
|
||||||
|
// Retrieve item for current position
|
||||||
val currentItem = filteredEvents[position]
|
val currentItem = filteredEvents[position]
|
||||||
|
|
||||||
|
// Set up text fields with corresponding values
|
||||||
holder.binding.textTitle.text = currentItem.title
|
holder.binding.textTitle.text = currentItem.title
|
||||||
holder.binding.textLecturers.text = currentItem.lecturer
|
holder.binding.textLecturers.text = currentItem.lecturer
|
||||||
holder.binding.textRoom.text = currentItem.room
|
holder.binding.textRoom.text = currentItem.room
|
||||||
|
|
||||||
|
// Use parsed timeslot string as defined in StudIPEvent#timeslot()
|
||||||
holder.binding.textTimeslot.text = currentItem.timeslot()
|
holder.binding.textTimeslot.text = currentItem.timeslot()
|
||||||
|
|
||||||
|
// Set up colours used for highlighting and un-highlighting an item.
|
||||||
val colorPrimaryTranslucent = TypedValue()
|
val colorPrimaryTranslucent = TypedValue()
|
||||||
val colorCardBackground = TypedValue()
|
val colorCardBackground = TypedValue()
|
||||||
val colorTextHintLighter = TypedValue()
|
val colorTextHintLighter = TypedValue()
|
||||||
val theme = holder.binding.root.context.theme
|
|
||||||
|
|
||||||
theme.apply {
|
// Resolve themed attributes to get the right values for day and night modes
|
||||||
|
holder.binding.root.context.theme.apply {
|
||||||
resolveAttribute(R.attr.colorPrimaryTranslucent, colorPrimaryTranslucent, true)
|
resolveAttribute(R.attr.colorPrimaryTranslucent, colorPrimaryTranslucent, true)
|
||||||
resolveAttribute(R.attr.colorCardBackground, colorCardBackground, true)
|
resolveAttribute(R.attr.colorCardBackground, colorCardBackground, true)
|
||||||
resolveAttribute(R.attr.colorTextHintLighter, colorTextHintLighter, true)
|
resolveAttribute(R.attr.colorTextHintLighter, colorTextHintLighter, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAnyCourseHighlighted && currentItem.isCurrentCourse(currentCalendar)) { // highlight
|
/* Highlight the next upcoming course of the day if the day of the adapter matches the
|
||||||
|
* current day, and if no other course has been highlighted, to avoid double highlighting.
|
||||||
|
*/
|
||||||
|
if (!isAnyCourseHighlighted && currentItem.isCurrentCourse(currentCalendar)) {
|
||||||
|
// Ensure that no other course will be highlighted
|
||||||
isAnyCourseHighlighted = true
|
isAnyCourseHighlighted = true
|
||||||
|
|
||||||
holder.binding.cardBackground.apply {
|
holder.binding.cardBackground.apply {
|
||||||
|
// Set the card's background colour to a desaturated shade of the primary colour
|
||||||
backgroundTintList = ColorStateList.valueOf(colorPrimaryTranslucent.data)
|
backgroundTintList = ColorStateList.valueOf(colorPrimaryTranslucent.data)
|
||||||
|
|
||||||
|
// Hide card stroke
|
||||||
strokeColor = context.getColor(android.R.color.transparent)
|
strokeColor = context.getColor(android.R.color.transparent)
|
||||||
}
|
}
|
||||||
} else { // un-highlight
|
// Otherwise, apply colours to ensure that the item will not be highlighted
|
||||||
|
} else {
|
||||||
holder.binding.cardBackground.apply {
|
holder.binding.cardBackground.apply {
|
||||||
|
// Set the card's background colour to its default value
|
||||||
backgroundTintList = ColorStateList.valueOf(colorCardBackground.data)
|
backgroundTintList = ColorStateList.valueOf(colorCardBackground.data)
|
||||||
|
|
||||||
|
// Set the card's stroke to its default colour
|
||||||
strokeColor = colorTextHintLighter.data
|
strokeColor = colorTextHintLighter.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up single click listener
|
||||||
holder.binding.cardBackground.setOnClickListener {
|
holder.binding.cardBackground.setOnClickListener {
|
||||||
onClickListener.onClick(currentItem)
|
onClickListener.onClick(currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up long press listener
|
||||||
holder.binding.cardBackground.setOnLongClickListener {
|
holder.binding.cardBackground.setOnLongClickListener {
|
||||||
onClickListener.onLongClick(currentItem)
|
onClickListener.onLongClick(currentItem)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO inflate view saying "no events!" or sth
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given course is the next course coming up. This is done by checking whether the
|
||||||
|
* day of the course matches the current real world day, as well as by evaluating if the end
|
||||||
|
* time stamp of the event has already passed. As the course list is ordered chronologically,
|
||||||
|
* this should always find the 'next' event, since without ordering the data origin, this could
|
||||||
|
* be used to highlight any event of the day that hasn't already passed.
|
||||||
|
*
|
||||||
|
* @param calendar instance of the current calendar
|
||||||
|
* @return whether the course is coming up next
|
||||||
|
*/
|
||||||
private fun StudIPEvent.isCurrentCourse(calendar: Calendar): Boolean =
|
private fun StudIPEvent.isCurrentCourse(calendar: Calendar): Boolean =
|
||||||
(calendar.get(Calendar.DAY_OF_WEEK) == this.day.toCalendarDay()) &&
|
(calendar.get(Calendar.DAY_OF_WEEK) == this.day.toCalendarDay()) &&
|
||||||
((calendar.get(Calendar.MINUTE) + calendar.get(Calendar.HOUR_OF_DAY) * 60) < this.timeslotEnd.parseToMinutes())
|
((calendar.get(Calendar.MINUTE) + calendar.get(Calendar.HOUR_OF_DAY) * 60) < this.timeslotEnd.parseToMinutes())
|
||||||
|
|
||||||
// Converts day as defined in StudIPEvent.kt to day from Calendar
|
// Converts day as defined in StudIPEvent.kt to day from Calendar
|
||||||
|
/**
|
||||||
|
* Converts a numeric value as defined in StudIPEvent.kt to a calendar day. Used to convert from
|
||||||
|
* American convention (where Sunday is the first day, 1 = Sunday, 2 = Monday, 7 = Saturday) to
|
||||||
|
* European convention and zero-based indexing (0 = Monday, 6 = Sunday).
|
||||||
|
*
|
||||||
|
* @return calendar day as numeric value
|
||||||
|
*/
|
||||||
private fun Int.toCalendarDay(): Int = when (this) {
|
private fun Int.toCalendarDay(): Int = when (this) {
|
||||||
1 -> Calendar.TUESDAY
|
1 -> Calendar.TUESDAY
|
||||||
2 -> Calendar.WEDNESDAY
|
2 -> Calendar.WEDNESDAY
|
||||||
@@ -91,13 +152,36 @@ class StudIPEventItemAdapter(
|
|||||||
else -> Calendar.MONDAY
|
else -> Calendar.MONDAY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a time stamp string to its numeric value in minutes.
|
||||||
|
* Example: 13:20. (20 minutes) + (13 hours * 60 minutes) = 800 minutes.
|
||||||
|
*
|
||||||
|
* @return minute value of the string
|
||||||
|
*/
|
||||||
private fun String.parseToMinutes(): Int {
|
private fun String.parseToMinutes(): Int {
|
||||||
val parts = split(":")
|
val parts = split(":")
|
||||||
return (parts[0].toInt() * 60) + parts[1].toInt()
|
return (parts[0].toInt() * 60) + parts[1].toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to evaluate clicks and long presses on a given item.
|
||||||
|
*/
|
||||||
interface OnClickListener {
|
interface OnClickListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed when an item has been clicked.
|
||||||
|
*
|
||||||
|
* @param event item that has been clicked
|
||||||
|
*/
|
||||||
fun onClick(event: StudIPEvent)
|
fun onClick(event: StudIPEvent)
|
||||||
fun onLongClick(event: StudIPEvent)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed when an item has been long-pressed.
|
||||||
|
*
|
||||||
|
* @param event item that has been long-pressed
|
||||||
|
* @return whether the long press was successful
|
||||||
|
*/
|
||||||
|
fun onLongClick(event: StudIPEvent): Boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,23 @@ 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.databinding.ItemScrollablePageBinding
|
||||||
|
|
||||||
class StudIPEventPageAdapter(private var events: List<StudIPEvent>, private val onClickListener: StudIPEventItemAdapter.OnClickListener) : RecyclerView.Adapter<StudIPEventPageAdapter.EventPageViewHolder>() {
|
/**
|
||||||
|
* Custom RecyclerView adapter for managing multiple pages of Stud.IP events in a RecyclerView or
|
||||||
|
* ViewPager.
|
||||||
|
*
|
||||||
|
* @param events all events (not filtered by day at this point)
|
||||||
|
* @param onClickListener for managing click and long press events
|
||||||
|
*/
|
||||||
|
class StudIPEventPageAdapter(
|
||||||
|
private var events: List<StudIPEvent>,
|
||||||
|
private val onClickListener: StudIPEventItemAdapter.OnClickListener
|
||||||
|
) : RecyclerView.Adapter<StudIPEventPageAdapter.EventPageViewHolder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View holder class for parent class
|
||||||
|
*
|
||||||
|
* @param binding view binding object
|
||||||
|
*/
|
||||||
class EventPageViewHolder(val binding: ItemScrollablePageBinding) : RecyclerView.ViewHolder(binding.root)
|
class EventPageViewHolder(val binding: ItemScrollablePageBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventPageViewHolder = EventPageViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventPageViewHolder = EventPageViewHolder(
|
||||||
@@ -19,19 +34,39 @@ class StudIPEventPageAdapter(private var events: List<StudIPEvent>, private val
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Assume that there will always be 5 pages, for 5 days - Monday through Friday.
|
||||||
|
* TODO is it right to assume that the user only has events on weekdays?
|
||||||
|
*/
|
||||||
override fun getItemCount(): Int = 5
|
override fun getItemCount(): Int = 5
|
||||||
|
|
||||||
|
// Set up page
|
||||||
override fun onBindViewHolder(holder: EventPageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: EventPageViewHolder, position: Int) {
|
||||||
|
|
||||||
holder.binding.pageRecyclerView.apply {
|
holder.binding.pageRecyclerView.apply {
|
||||||
|
// Set page to horizontally scroll
|
||||||
layoutManager = LinearLayoutManager(holder.binding.root.context, LinearLayoutManager.VERTICAL, false)
|
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
|
adapter = StudIPEventItemAdapter(events, currentDay = position, onClickListener) // TODO check if empty
|
||||||
|
|
||||||
|
// Animate creation of new page
|
||||||
scheduleLayoutAnimation()
|
scheduleLayoutAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the entire list of items
|
||||||
|
*
|
||||||
|
* @param items the new set of items
|
||||||
|
*/
|
||||||
fun setNewItems(items: List<StudIPEvent>) {
|
fun setNewItems(items: List<StudIPEvent>) {
|
||||||
|
// Update items
|
||||||
events = items
|
events = items
|
||||||
|
|
||||||
|
// Force an update of the view. TODO replace with individual updates, as this is inefficient
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
package com.denizk0461.studip.data
|
package com.denizk0461.studip.data
|
||||||
|
|
||||||
import com.denizk0461.studip.db.EventRepository
|
import com.denizk0461.studip.db.AppRepository
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static object that holds a reference to the app's repository to provide abstracted access to the
|
||||||
|
* app database.
|
||||||
|
*/
|
||||||
object Dependencies {
|
object Dependencies {
|
||||||
lateinit var repo: EventRepository
|
/**
|
||||||
|
* Reference to the app's repository. Since it instantiated once by the main activity, it can be
|
||||||
|
* accessed statically by all other classes. Used for database transactions.
|
||||||
|
*/
|
||||||
|
lateinit var repo: AppRepository
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,26 @@
|
|||||||
package com.denizk0461.studip.data
|
package com.denizk0461.studip.data
|
||||||
|
|
||||||
import com.denizk0461.studip.R
|
/**
|
||||||
|
* Miscellaneous functions and variables that don't belong into any specific class.
|
||||||
|
*/
|
||||||
object Misc {
|
object Misc {
|
||||||
|
|
||||||
private val timeslotAcademicQuarter = mapOf(
|
// private val timeslotAcademicQuarter = mapOf(
|
||||||
"6:00" to "6:15",
|
// "6:00" to "6:15",
|
||||||
"8:00" to "7:45",
|
// "8:00" to "7:45",
|
||||||
"8:00" to "8:15",
|
// "8:00" to "8:15",
|
||||||
"10:00" to "9:45",
|
// "10:00" to "9:45",
|
||||||
"10:00" to "10:15",
|
// "10:00" to "10:15",
|
||||||
"12:00" to "11:45",
|
// "12:00" to "11:45",
|
||||||
"12:00" to "12:15",
|
// "12:00" to "12:15",
|
||||||
"14:00" to "13:45",
|
// "14:00" to "13:45",
|
||||||
"14:00" to "14:15",
|
// "14:00" to "14:15",
|
||||||
"16:00" to "15:45",
|
// "16:00" to "15:45",
|
||||||
"16:00" to "16:15",
|
// "16:00" to "16:15",
|
||||||
"18:00" to "17:45",
|
// "18:00" to "17:45",
|
||||||
"18:00" to "18:15",
|
// "18:00" to "18:15",
|
||||||
"20:00" to "19:45",
|
// "20:00" to "19:45",
|
||||||
"20:00" to "20:15",
|
// "20:00" to "20:15",
|
||||||
"22:00" to "21:45",
|
// "22:00" to "21:45",
|
||||||
)
|
// )
|
||||||
|
|
||||||
val indexToDrawable: Map<Int, Int> = mapOf(
|
|
||||||
0 to R.drawable.handshake,
|
|
||||||
1 to R.drawable.fish,
|
|
||||||
2 to R.drawable.chicken,
|
|
||||||
3 to R.drawable.sheep,
|
|
||||||
4 to R.drawable.yoga,
|
|
||||||
5 to R.drawable.cow,
|
|
||||||
6 to R.drawable.pig,
|
|
||||||
7 to R.drawable.leaf,
|
|
||||||
8 to R.drawable.carrot,
|
|
||||||
9 to R.drawable.deer,
|
|
||||||
10 to R.drawable.circle,
|
|
||||||
)
|
|
||||||
|
|
||||||
const val mysteryLink = "https://www.youtube.com/watch?v=nhIQMCXJzLI"
|
|
||||||
}
|
}
|
||||||
@@ -3,43 +3,99 @@ package com.denizk0461.studip.data
|
|||||||
import com.denizk0461.studip.model.StudIPEvent
|
import com.denizk0461.studip.model.StudIPEvent
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser class used for fetching and collecting scheduled events from a Stud.IP timetable.
|
||||||
|
*/
|
||||||
class StudIPParser {
|
class StudIPParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a given HTML string. HTML must be of a Stud.IP timetable.
|
||||||
|
*
|
||||||
|
* @param html website content of the Stud.IP timetable
|
||||||
|
* @param insert action call to save the contents to persistent storage
|
||||||
|
*/
|
||||||
fun parse(html: String, insert: (events: List<StudIPEvent>) -> Unit) {
|
fun parse(html: String, insert: (events: List<StudIPEvent>) -> Unit) {
|
||||||
|
// Primary key value to uniquely identify entries in the database
|
||||||
var id = 0
|
var id = 0
|
||||||
|
|
||||||
|
// Parse the HTML using Jsoup to traverse the document
|
||||||
val doc = Jsoup.parse(html)
|
val doc = Jsoup.parse(html)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create the list that all events will be added to temporarily before saving them to
|
||||||
|
* persistent storage.
|
||||||
|
*/
|
||||||
val newEvents = mutableListOf<StudIPEvent>()
|
val newEvents = mutableListOf<StudIPEvent>()
|
||||||
val columns = arrayOf( // TODO THIS NEEDS TO BE DYNAMIC!
|
|
||||||
|
/*
|
||||||
|
* Fetch all columns from Monday through Friday to parse and assign the events of each day
|
||||||
|
* individually and accordingly.
|
||||||
|
* TODO this must be dynamic. As it is, it assumes that the first day is always Monday,
|
||||||
|
* which needs not be the case. The timetable can be customised by the user by removing
|
||||||
|
* days. Also, Sunday is treated as the first day in Stud.IP FOR SOME REASON, so the app
|
||||||
|
* needs to handle this appropriately.
|
||||||
|
*/
|
||||||
|
val columns = arrayOf(
|
||||||
doc.getElementById("calendar_view_1_column_0"),
|
doc.getElementById("calendar_view_1_column_0"),
|
||||||
doc.getElementById("calendar_view_1_column_1"),
|
doc.getElementById("calendar_view_1_column_1"),
|
||||||
doc.getElementById("calendar_view_1_column_2"),
|
doc.getElementById("calendar_view_1_column_2"),
|
||||||
doc.getElementById("calendar_view_1_column_3"),
|
doc.getElementById("calendar_view_1_column_3"),
|
||||||
doc.getElementById("calendar_view_1_column_4"),
|
doc.getElementById("calendar_view_1_column_4"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Iterate through each day
|
||||||
columns.forEachIndexed { index, element ->
|
columns.forEachIndexed { index, element ->
|
||||||
element?.getElementsByClass("schedule_entry")?.forEachIndexed { entryIndex, entry ->
|
|
||||||
|
// Iterate through all events scheduled for a given day
|
||||||
|
element?.getElementsByClass("schedule_entry")?.forEach { entry ->
|
||||||
|
/*
|
||||||
|
* Retrieve the event's header. This will contain the event's title as well as the
|
||||||
|
* lecturers holding the event.
|
||||||
|
*/
|
||||||
val entryHeader = entry.getElementsByTag("dt")[0].text()
|
val entryHeader = entry.getElementsByTag("dt")[0].text()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Find out the index before which the title ends and after which the lecturer
|
||||||
|
* name(s) begin.
|
||||||
|
*/
|
||||||
val delimiter = entryHeader.lastIndexOf('(')
|
val delimiter = entryHeader.lastIndexOf('(')
|
||||||
val parsedTitle = entryHeader.substring(0 until delimiter)
|
|
||||||
val parsedLecturers = entryHeader.substring(delimiter + 1 until entryHeader.length - 1)
|
|
||||||
|
|
||||||
val entryInfos = entry.getElementsByTag("dd")[0].text().split(", ", limit = 2)
|
// Retrieve the title of the event from the header string
|
||||||
// val timeSlot = Misc.parseTimeslot(entryInfos[0])
|
val parsedTitle = entryHeader.substring(0 until delimiter).trim()
|
||||||
val timeSlot = entryInfos[0].split(" - ")
|
|
||||||
|
|
||||||
|
// Retrieve the lecturer name(s) from the header string
|
||||||
|
val parsedLecturers = entryHeader.substring(delimiter + 1 until entryHeader.length - 1).trim()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieve further event information. This will contain both the time slot the
|
||||||
|
* event is assigned to (index 0), as well as the room the event will primarily take
|
||||||
|
* place in (index 1).
|
||||||
|
*/
|
||||||
|
val entryInfo = entry.getElementsByTag("dd")[0].text().split(", ", limit = 2)
|
||||||
|
|
||||||
|
// Retrieve the time slot and split it into start and end time stamps
|
||||||
|
val timeSlot = entryInfo[0].split(" - ")
|
||||||
|
|
||||||
|
// Construct the newly scraped Stud.IP event
|
||||||
val event = StudIPEvent(
|
val event = StudIPEvent(
|
||||||
id = id,
|
id = id,
|
||||||
title = parsedTitle,
|
title = parsedTitle,
|
||||||
lecturer = parsedLecturers,
|
lecturer = parsedLecturers,
|
||||||
room = entryInfos[1],
|
room = entryInfo[1],
|
||||||
day = index,
|
day = index,
|
||||||
timeslotStart = timeSlot[0],
|
timeslotStart = timeSlot[0],
|
||||||
timeslotEnd = timeSlot[1],
|
timeslotEnd = timeSlot[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add the new element to the temporary list
|
||||||
newEvents.add(event)
|
newEvents.add(event)
|
||||||
|
|
||||||
|
// Raise the primary key value by 1 to avoid not fulfilling the unique constraint
|
||||||
id += 1
|
id += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After fetching has finished, save the list of new items into persistent storage
|
||||||
insert(newEvents)
|
insert(newEvents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,50 @@
|
|||||||
package com.denizk0461.studip.data
|
package com.denizk0461.studip.data
|
||||||
|
|
||||||
import android.util.Log
|
import com.denizk0461.studip.db.AppRepository
|
||||||
import com.denizk0461.studip.db.EventRepository
|
|
||||||
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
|
||||||
import org.jsoup.select.Elements
|
import org.jsoup.select.Elements
|
||||||
|
|
||||||
// Parser class for the Studierendenwerk cafeteria plans
|
/**
|
||||||
|
* Parser class used for fetching and collecting canteen offers from the website of the
|
||||||
|
* Studierendenwerk Bremen.
|
||||||
|
*/
|
||||||
class StwParser {
|
class StwParser {
|
||||||
|
|
||||||
|
// Unique primary key value for the date elements
|
||||||
private var dateId = 0
|
private var dateId = 0
|
||||||
|
|
||||||
|
// Unique primary key value for the canteen elements
|
||||||
private var canteenId = 0
|
private var canteenId = 0
|
||||||
|
|
||||||
|
// Unique primary key value for the category elements
|
||||||
private var categoryId = 0
|
private var categoryId = 0
|
||||||
|
|
||||||
|
// Unique primary key value for the item elements
|
||||||
private var itemId = 0
|
private var itemId = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses through a list of canteen plans and saves them to persistent storage.
|
||||||
|
*
|
||||||
|
* @param onRefreshUpdate action call for when an update on the status of the fetch is
|
||||||
|
* available
|
||||||
|
* @param onFinish action call for when the fetch has finished
|
||||||
|
*/
|
||||||
fun parse(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
|
fun parse(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
|
||||||
|
/*
|
||||||
|
* Reset primary key values. This needs to be done in case this method is called multiple
|
||||||
|
* times within the app's lifespan
|
||||||
|
*/
|
||||||
dateId = 0
|
dateId = 0
|
||||||
canteenId = 0
|
canteenId = 0
|
||||||
categoryId = 0
|
categoryId = 0
|
||||||
itemId = 0
|
itemId = 0
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create and populate a list of all the canteen plans that will be fetched.
|
||||||
|
* TODO implement functionality that will allow for fetching multiple plans, if necessary
|
||||||
|
*/
|
||||||
val links = mutableListOf<String>()
|
val links = mutableListOf<String>()
|
||||||
if (true) { links.add(urlUniMensa) }
|
if (true) { links.add(urlUniMensa) }
|
||||||
// if (true) { links.add(urlCafeCentral) }
|
// if (true) { links.add(urlCafeCentral) }
|
||||||
@@ -32,66 +57,106 @@ class StwParser {
|
|||||||
// if (true) { links.add(urlMensaBHV) }
|
// if (true) { links.add(urlMensaBHV) }
|
||||||
// if (true) { links.add(urlCafeBHV) }
|
// if (true) { links.add(urlCafeBHV) }
|
||||||
|
|
||||||
|
// Delete all previous entries and start afresh
|
||||||
Dependencies.repo.nukeOffers()
|
Dependencies.repo.nukeOffers()
|
||||||
|
|
||||||
// STEP: fetch every canteen
|
/*
|
||||||
|
* Fetch offers from every canteen individually. This is done in sequence for
|
||||||
|
* simplicity's sake.
|
||||||
|
* TODO perhaps it can be assumed here that the next two weeks will always be available?
|
||||||
|
* Hence, the date fetch might be superfluous and a database insert call could be
|
||||||
|
* implemented here instead. Another option would be to set the conflict policy to IGNORE
|
||||||
|
* and move the dateId reset from parseFromPage() to parse().
|
||||||
|
*/
|
||||||
links.forEach { link ->
|
links.forEach { link ->
|
||||||
// TODO implement onRefresh(Int)
|
|
||||||
parseFromPage(link, Dependencies.repo)
|
parseFromPage(link, Dependencies.repo)
|
||||||
|
|
||||||
|
// TODO implement onRefresh(Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action call once all fetching activities have finished
|
||||||
onFinish()
|
onFinish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseFromPage(url: String, repo: EventRepository): List<CanteenOffer> {
|
/**
|
||||||
val items = mutableListOf<CanteenOffer>()
|
* 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 repo reference to the app repository used to save the items to persistent storage
|
||||||
|
*/
|
||||||
|
private fun parseFromPage(url: String, repo: AppRepository) {
|
||||||
|
// Used to store a parsed date string without creating a new variable on every loop
|
||||||
var date: String
|
var date: String
|
||||||
|
|
||||||
|
// Reset the primary key value for the date objects
|
||||||
dateId = 0
|
dateId = 0
|
||||||
|
|
||||||
|
// Parse the HTML using Jsoup to traverse the document
|
||||||
val doc = Jsoup.connect(url).get()
|
val doc = Jsoup.connect(url).get()
|
||||||
|
|
||||||
|
// Save the canteen to persistent storage
|
||||||
repo.insert(OfferCanteen(canteenId, doc.getElementsByClass("pane-title")[1].text()))
|
repo.insert(OfferCanteen(canteenId, doc.getElementsByClass("pane-title")[1].text()))
|
||||||
|
|
||||||
// STEP: get each day
|
// Iterate through each day for which offers are available
|
||||||
doc.getElementsByClass("food-plan").forEach { dayPlan ->
|
doc.getElementsByClass("food-plan").forEach { dayPlan ->
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fetch the date for a given day. It will be in the following format:
|
||||||
|
* 24. Apr
|
||||||
|
* This value will be split between the day (24.) and the month (Apr)
|
||||||
|
*/
|
||||||
val rawDate = doc.getElementsByClass("tabs")[0]
|
val rawDate = doc.getElementsByClass("tabs")[0]
|
||||||
.getElementsByClass("tab-date")[dateId].text().split(" ")
|
.getElementsByClass("tab-date")[dateId].text().split(" ")
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parse the two elements into a single date. The month will be converted to a numeric
|
||||||
|
* value. Example:
|
||||||
|
* Input: 24. Apr
|
||||||
|
* Output: 24.04.
|
||||||
|
*/
|
||||||
date = "${rawDate[0]}${rawDate[1].monthToNumber()}."
|
date = "${rawDate[0]}${rawDate[1].monthToNumber()}."
|
||||||
|
|
||||||
val d = OfferDate(dateId, date)
|
/*
|
||||||
Log.d("eek!8", d.toString())
|
* Save the date to persistent storage.
|
||||||
repo.insert(d)
|
* TODO this will duplicate date entries for every canteen that is fetched. This is
|
||||||
|
* inefficient.
|
||||||
|
*/
|
||||||
|
repo.insert(OfferDate(dateId, date))
|
||||||
|
|
||||||
// STEP: get each category in a day
|
// Iterate through all categories offered on a given day
|
||||||
dayPlan.getElementsByClass("food-category").forEach { category ->
|
dayPlan.getElementsByClass("food-category").forEach { category ->
|
||||||
|
// 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
|
||||||
repo.insert(OfferCategory(categoryId, dateId, canteenId, categoryTitle))
|
repo.insert(OfferCategory(categoryId, dateId, canteenId, categoryTitle))
|
||||||
|
|
||||||
// STEP: get each item in a category
|
// Iterate through all items in a category
|
||||||
category
|
category
|
||||||
.getElementsByTag("tbody")[0]
|
.getElementsByTag("tbody")[0]
|
||||||
.getElementsByTag("tr").forEach { element ->
|
.getElementsByTag("tr").forEach { element ->
|
||||||
|
// Retrieve the parent element holding the item's title and price
|
||||||
val tableRows = element.getElementsByTag("td")
|
val tableRows = element.getElementsByTag("td")
|
||||||
|
|
||||||
val prefs =
|
// Retrieve the dietary preferences the item meets
|
||||||
element.getElementsByClass("field field-name-field-food-types")[0]
|
val prefs = element
|
||||||
|
.getElementsByClass("field field-name-field-food-types")[0]
|
||||||
|
|
||||||
|
// Parse the preferences into a string for the database
|
||||||
val prefString = DietaryPrefObject(
|
val prefString = DietaryPrefObject(
|
||||||
isFair = prefs.isDietaryPreferenceMet(PREFERENCE_FAIR),
|
isFair = prefs.isDietaryPreferenceMet(imageLinkPrefFair),
|
||||||
isFish = prefs.isDietaryPreferenceMet(PREFERENCE_FISH),
|
isFish = prefs.isDietaryPreferenceMet(imageLinkPrefFish),
|
||||||
isPoultry = prefs.isDietaryPreferenceMet(PREFERENCE_POULTRY),
|
isPoultry = prefs.isDietaryPreferenceMet(imageLinkPrefPoultry),
|
||||||
isLamb = prefs.isDietaryPreferenceMet(PREFERENCE_LAMB),
|
isLamb = prefs.isDietaryPreferenceMet(imageLinkPrefLamb),
|
||||||
isVital = prefs.isDietaryPreferenceMet(PREFERENCE_VITAL),
|
isVital = prefs.isDietaryPreferenceMet(imageLinkPrefVital),
|
||||||
isBeef = prefs.isDietaryPreferenceMet(PREFERENCE_BEEF),
|
isBeef = prefs.isDietaryPreferenceMet(imageLinkPrefBeef),
|
||||||
isPork = prefs.isDietaryPreferenceMet(PREFERENCE_PORK),
|
isPork = prefs.isDietaryPreferenceMet(imageLinkPrefPork),
|
||||||
isVegan = prefs.isDietaryPreferenceMet(PREFERENCE_VEGAN),
|
isVegan = prefs.isDietaryPreferenceMet(imageLinkPrefVegan),
|
||||||
isVegetarian = prefs.isDietaryPreferenceMet(PREFERENCE_VEGETARIAN),
|
isVegetarian = prefs.isDietaryPreferenceMet(imageLinkPrefVegetarian),
|
||||||
isGame = prefs.isDietaryPreferenceMet(PREFERENCE_GAME),
|
isGame = prefs.isDietaryPreferenceMet(imageLinkPrefGame),
|
||||||
).deconstruct()
|
).deconstruct()
|
||||||
|
|
||||||
|
// Save the item to persistent storage
|
||||||
repo.insert(
|
repo.insert(
|
||||||
OfferItem(
|
OfferItem(
|
||||||
itemId,
|
itemId,
|
||||||
@@ -101,39 +166,77 @@ class StwParser {
|
|||||||
prefString,
|
prefString,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Increment the item ID to avoid overriding
|
||||||
itemId += 1
|
itemId += 1
|
||||||
}
|
}
|
||||||
|
// Increment the category ID to avoid overriding
|
||||||
categoryId += 1
|
categoryId += 1
|
||||||
}
|
}
|
||||||
|
// Increment the date ID to avoid overriding
|
||||||
dateId += 1
|
dateId += 1
|
||||||
}
|
}
|
||||||
|
// Increment the canteen ID to avoid overriding
|
||||||
canteenId += 1
|
canteenId += 1
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether a dietary preference is met by checking if the link to an image can be
|
||||||
|
* found in the HTML.
|
||||||
|
*
|
||||||
|
* @param preference constraint that needs to be met
|
||||||
|
* @return whether it is met
|
||||||
|
*/
|
||||||
private fun Element.isDietaryPreferenceMet(preference: String): Boolean =
|
private fun Element.isDietaryPreferenceMet(preference: String): Boolean =
|
||||||
getElementsByAttributeValue("src", preference).isNotEmpty()
|
getElementsByAttributeValue("src", preference).isNotEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes certain character references into human-readable characters. Since the fetched HTML
|
||||||
|
* contains unresolved symbols such as &, they need to be replaced with their counterpart
|
||||||
|
* (in this case, &).
|
||||||
|
*
|
||||||
|
* @return the filtered string
|
||||||
|
*/
|
||||||
private fun Element.getFilteredText(): String {
|
private fun Element.getFilteredText(): String {
|
||||||
|
// Retrieve the element's inner HTML and replace faulty characters
|
||||||
var text = html()
|
var text = html()
|
||||||
.replace("&", "&")
|
.replace("&", "&")
|
||||||
.replace(">", ">")
|
.replace(">", ">")
|
||||||
.replace("<", "<")
|
.replace("<", "<")
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The Studierendenwerk's website lists allergens, but they are invisible, rendering them
|
||||||
|
* pointless to the website user. As of now, this information is discarded in the app.
|
||||||
|
* TODO implement allergen functionality
|
||||||
|
*/
|
||||||
while (text.contains("<sup>")) {
|
while (text.contains("<sup>")) {
|
||||||
text = text.substring(0 until text.indexOf("<sup>")) + text.substring(text.indexOf("</sup>")+6 until text.length)
|
text = text.substring(0 until text.indexOf("<sup>")) + text.substring(text.indexOf("</sup>")+6 until text.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the stripped and updated HTML string
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the text of an element in a certain position if the element can be found. Else,
|
||||||
|
* return an empty string.
|
||||||
|
*
|
||||||
|
* @param index position at which a child
|
||||||
|
* @return text content or, if no element is found, an empty string
|
||||||
|
*/
|
||||||
private fun Elements.getTextOrEmpty(index: Int): String = try {
|
private fun Elements.getTextOrEmpty(index: Int): String = try {
|
||||||
get(index).text()
|
get(index).text()
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a month text string to its numeric value. Example:
|
||||||
|
* Input: Apr
|
||||||
|
* Output: 04
|
||||||
|
*
|
||||||
|
* @return the numeric value of the month
|
||||||
|
*/
|
||||||
private fun String.monthToNumber(): String = when (this) {
|
private fun String.monthToNumber(): String = when (this) {
|
||||||
"Jan" -> "01"
|
"Jan" -> "01"
|
||||||
"Feb" -> "02"
|
"Feb" -> "02"
|
||||||
@@ -150,22 +253,24 @@ class StwParser {
|
|||||||
else -> "00" // shouldn't occur
|
else -> "00" // shouldn't occur
|
||||||
}
|
}
|
||||||
|
|
||||||
private val PREFERENCE_FAIR = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
// Image links to all dietary preferences used for checking whether a preference is met
|
||||||
private val PREFERENCE_FISH = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
private val imageLinkPrefFair = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
||||||
private val PREFERENCE_POULTRY = "https://www.stw-bremen.de/sites/default/files/images/pictograms/geflugel.png"
|
private val imageLinkPrefFish = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
||||||
private val PREFERENCE_LAMB = "https://www.stw-bremen.de/sites/default/files/images/pictograms/lamm.png"
|
private val imageLinkPrefPoultry = "https://www.stw-bremen.de/sites/default/files/images/pictograms/geflugel.png"
|
||||||
private val PREFERENCE_VITAL = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vital.png"
|
private val imageLinkPrefLamb = "https://www.stw-bremen.de/sites/default/files/images/pictograms/lamm.png"
|
||||||
private val PREFERENCE_BEEF = "https://www.stw-bremen.de/sites/default/files/images/pictograms/rindfleisch.png"
|
private val imageLinkPrefVital = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vital.png"
|
||||||
private val PREFERENCE_PORK = "https://www.stw-bremen.de/sites/default/files/images/pictograms/schwein.png"
|
private val imageLinkPrefBeef = "https://www.stw-bremen.de/sites/default/files/images/pictograms/rindfleisch.png"
|
||||||
private val PREFERENCE_VEGAN = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vegan.png"
|
private val imageLinkPrefPork = "https://www.stw-bremen.de/sites/default/files/images/pictograms/schwein.png"
|
||||||
private val PREFERENCE_VEGETARIAN = "https://www.stw-bremen.de/sites/default/files/images/pictograms/vegetarisch.png"
|
private val imageLinkPrefVegan = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vegan.png"
|
||||||
private val PREFERENCE_GAME = "https://www.stw-bremen.de/sites/default/files/images/pictograms/wild.png"
|
private val imageLinkPrefVegetarian = "https://www.stw-bremen.de/sites/default/files/images/pictograms/vegetarisch.png"
|
||||||
|
private val imageLinkPrefGame = "https://www.stw-bremen.de/sites/default/files/images/pictograms/wild.png"
|
||||||
|
|
||||||
|
// URLs to all canteens in Bremen and Bremerhaven managed by the Studierendenwerk Bremen
|
||||||
private val urlUniMensa = "https://www.stw-bremen.de/de/mensa/uni-mensa"
|
private val urlUniMensa = "https://www.stw-bremen.de/de/mensa/uni-mensa"
|
||||||
private val urlCafeCentral = "https://www.stw-bremen.de/de/mensa/cafe-central"
|
private val urlCafeCentral = "https://www.stw-bremen.de/de/mensa/cafe-central"
|
||||||
private val urlNW1 = "https://www.stw-bremen.de/de/mensa/nw-1"
|
private val urlNW1 = "https://www.stw-bremen.de/de/mensa/nw-1"
|
||||||
private val urlGW2 = "https://www.stw-bremen.de/de/cafeteria/gw2"
|
private val urlGW2 = "https://www.stw-bremen.de/de/cafeteria/gw2"
|
||||||
// private val urlGraz = ""
|
// private val urlGraz = "" // No link since there are only snacks on offer that are not listed online
|
||||||
private val urlHSBNeustadt = "https://www.stw-bremen.de/de/mensa/neustadtswall"
|
private val urlHSBNeustadt = "https://www.stw-bremen.de/de/mensa/neustadtswall"
|
||||||
private val urlHSBWerder = "https://www.stw-bremen.de/de/mensa/werderstra%C3%9Fe"
|
private val urlHSBWerder = "https://www.stw-bremen.de/de/mensa/werderstra%C3%9Fe"
|
||||||
private val urlHSBAirport = "https://www.stw-bremen.de/de/mensa/airport"
|
private val urlHSBAirport = "https://www.stw-bremen.de/de/mensa/airport"
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package com.denizk0461.studip.db
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.denizk0461.studip.model.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database access object used for all database transactions in the app. Can be accessed through an
|
||||||
|
* instance of AppRepository.kt.
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface AppDAO {
|
||||||
|
|
||||||
|
/* --- Stud.IP schedule events --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all Stud.IP events.
|
||||||
|
*
|
||||||
|
* @return all Stud.IP events exposed through a LiveData object
|
||||||
|
*/
|
||||||
|
@get:Query("SELECT * FROM events ORDER BY id")
|
||||||
|
val allEvents: LiveData<List<StudIPEvent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a Stud.IP event into the database.
|
||||||
|
*
|
||||||
|
* @param event object to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insert(event: StudIPEvent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of Stud.IP events into the database.
|
||||||
|
*
|
||||||
|
* @param events objects to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insert(events: List<StudIPEvent>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all Stud.IP events from the database.
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM events")
|
||||||
|
fun nukeEvents()
|
||||||
|
|
||||||
|
/* --- canteen offers --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offers. Objects will be joined through their primary/foreign keys from
|
||||||
|
* instances of OfferDate.kt, OfferCanteen.kt, OfferCategory.kt, and OfferItem.kt.
|
||||||
|
*
|
||||||
|
* @return all canteen offers exposed through a LiveData object
|
||||||
|
*/
|
||||||
|
@get:Query(
|
||||||
|
"SELECT * FROM offer_item " +
|
||||||
|
"JOIN offer_category ON offer_item.categoryId = offer_category.id " +
|
||||||
|
"JOIN offer_canteen ON offer_category.canteenId = offer_canteen.id " +
|
||||||
|
"JOIN offer_date ON offer_category.dateId = offer_date.id"
|
||||||
|
)
|
||||||
|
val allOffers: LiveData<List<CanteenOffer>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offers that match given dietary preferences. Objects will be joined
|
||||||
|
* through their primary/foreign keys from instances of OfferDate.kt, OfferCanteen.kt,
|
||||||
|
* OfferCategory.kt, and OfferItem.kt.
|
||||||
|
*
|
||||||
|
* @return all canteen offers matching the given preference exposed through a LiveData object
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM offer_item " +
|
||||||
|
"JOIN offer_category ON offer_item.categoryId = offer_category.id " +
|
||||||
|
"JOIN offer_canteen ON offer_category.canteenId = offer_canteen.id " +
|
||||||
|
"JOIN offer_date ON offer_category.dateId = offer_date.id " +
|
||||||
|
"WHERE dietary_preferences = :prefs "
|
||||||
|
)
|
||||||
|
fun getOffersByPreference(prefs: String): LiveData<List<CanteenOffer>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offer date objects.
|
||||||
|
*
|
||||||
|
* @return a list of instances of canteen offer dates
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM offer_date ORDER BY id")
|
||||||
|
fun getDates(): List<OfferDate>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all dates from the database.
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM offer_date")
|
||||||
|
fun nukeOfferDates()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all canteens from the database.
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM offer_canteen")
|
||||||
|
fun nukeOfferCanteens()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all canteen offer categories from the database.
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM offer_category")
|
||||||
|
fun nukeOfferCategories()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all canteen offer items from the database.
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM offer_item")
|
||||||
|
fun nukeOfferItems()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a date into the database.
|
||||||
|
*
|
||||||
|
* @param date object to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insert(date: OfferDate)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a canteen into the database.
|
||||||
|
*
|
||||||
|
* @param canteen object to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insert(canteen: OfferCanteen)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a canteen offer category into the database.
|
||||||
|
*
|
||||||
|
* @param category object to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insert(category: OfferCategory)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a canteen offer item into the database.
|
||||||
|
*
|
||||||
|
* @param item object to be saved to the database
|
||||||
|
*/
|
||||||
|
@Insert fun insert(item: OfferItem)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.denizk0461.studip.db
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import com.denizk0461.studip.model.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room database that holds all objects that need to be saved to persistent storage.
|
||||||
|
*/
|
||||||
|
@Database(
|
||||||
|
entities = [
|
||||||
|
StudIPEvent::class,
|
||||||
|
OfferDate::class,
|
||||||
|
OfferCanteen::class,
|
||||||
|
OfferCategory::class,
|
||||||
|
OfferItem::class
|
||||||
|
],
|
||||||
|
version = 10,
|
||||||
|
)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
// The database access object
|
||||||
|
abstract fun dao(): AppDAO
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// The singular instance of this database
|
||||||
|
private var instance: AppDatabase? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the instance of the database.
|
||||||
|
*
|
||||||
|
* @param context used to resolve the object
|
||||||
|
* @return the database
|
||||||
|
*/
|
||||||
|
fun getInstance(context: Context): AppDatabase {
|
||||||
|
/*
|
||||||
|
* Check if an object has already been instantiated. Only create a new instance if none
|
||||||
|
* exists.
|
||||||
|
*/
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized(AppDatabase::class) {
|
||||||
|
instance = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
"event_db"
|
||||||
|
).fallbackToDestructiveMigration().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Instance can never be null at this point
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package com.denizk0461.studip.db
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.denizk0461.studip.model.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository objects that acts as a mediator between view models and the database to retrieve data
|
||||||
|
* without being bound to a view lifecycle.
|
||||||
|
*
|
||||||
|
* @param app reference to the application
|
||||||
|
*/
|
||||||
|
class AppRepository(app: Application) {
|
||||||
|
|
||||||
|
// Reference to the database access object used to handle database transactions
|
||||||
|
private val dao: AppDAO = AppDatabase.getInstance(app.applicationContext).dao()
|
||||||
|
|
||||||
|
// Shared preferences used to store small values
|
||||||
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(app)
|
||||||
|
|
||||||
|
// Tag used to store the user's dietary preferences in persistent storage (shared preferences)
|
||||||
|
private val dietaryPrefString = "prefs_obj"
|
||||||
|
|
||||||
|
// Blank dietary preference regular expression
|
||||||
|
private val blankDietaryPrefs: String = DietaryPrefObject.C_FALSE.toString().repeat(10)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all Stud.IP events.
|
||||||
|
*
|
||||||
|
* @return all Stud.IP events exposed through a LiveData object
|
||||||
|
*/
|
||||||
|
val allEvents: LiveData<List<StudIPEvent>> = dao.allEvents
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of Stud.IP events into the database.
|
||||||
|
*
|
||||||
|
* @param events objects to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insertEvents(events: List<StudIPEvent>) { dao.insert(events) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all Stud.IP events from the database.
|
||||||
|
*/
|
||||||
|
fun nukeEvents() { dao.nukeEvents() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offers.
|
||||||
|
*
|
||||||
|
* @return all canteen offers exposed through a LiveData object
|
||||||
|
*/
|
||||||
|
val allOffers: LiveData<List<CanteenOffer>> = dao.allOffers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offer date objects.
|
||||||
|
*
|
||||||
|
* @return a list of instances of canteen offer dates
|
||||||
|
*/
|
||||||
|
fun getDates(): List<OfferDate> = dao.getDates()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the regex string used to determine the user's dietary preferences, or, if no
|
||||||
|
* preference has been set, a 'blank' expression denoting such.
|
||||||
|
*
|
||||||
|
* @return dietary preferences as a regular expression string
|
||||||
|
*/
|
||||||
|
private fun getDietaryPrefs(): String {
|
||||||
|
return prefs.getString(dietaryPrefString, blankDietaryPrefs) ?: blankDietaryPrefs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the user's dietary preferences as a DietaryPrefObject.
|
||||||
|
*
|
||||||
|
* @return dietary preferences as an instance of DietaryPrefObject.kt
|
||||||
|
*/
|
||||||
|
fun getDietaryPrefsAsObj(): DietaryPrefObject = DietaryPrefObject.construct(getDietaryPrefs())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a given dietary preference to a new value.
|
||||||
|
*
|
||||||
|
* @param pref preference to be updated
|
||||||
|
* @param newValue value to set the preference to
|
||||||
|
*/
|
||||||
|
fun setPreference(pref: DietaryPreferences, newValue: Boolean) {
|
||||||
|
val oldString = getDietaryPrefs().toMutableList()
|
||||||
|
|
||||||
|
oldString[pref.ordinal] = newValue.toChar()
|
||||||
|
// when (pref) {
|
||||||
|
// DietaryPreferences.FAIR -> oldString[0] = newValue.toChar()
|
||||||
|
// DietaryPreferences.FISH -> oldString[1] = newValue.toChar()
|
||||||
|
// DietaryPreferences.POULTRY -> oldString[2] = newValue.toChar()
|
||||||
|
// DietaryPreferences.LAMB -> oldString[3] = newValue.toChar()
|
||||||
|
// DietaryPreferences.VITAL -> oldString[4] = newValue.toChar()
|
||||||
|
// DietaryPreferences.BEEF -> oldString[5] = newValue.toChar()
|
||||||
|
// DietaryPreferences.PORK -> oldString[6] = newValue.toChar()
|
||||||
|
// DietaryPreferences.VEGAN -> oldString[7] = newValue.toChar()
|
||||||
|
// DietaryPreferences.VEGETARIAN -> oldString[8] = newValue.toChar()
|
||||||
|
// DietaryPreferences.GAME -> oldString[9] = newValue.toChar()
|
||||||
|
// }
|
||||||
|
val newString = oldString.joinToString("")
|
||||||
|
prefs.edit().putString(dietaryPrefString, newString).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a boolean value to the corresponding value used to construct a regular expression
|
||||||
|
* for determining whether a dietary preference is met.
|
||||||
|
*
|
||||||
|
* @return char denoting whether a preference needs to be met
|
||||||
|
*/
|
||||||
|
private fun Boolean.toChar() = if (this) DietaryPrefObject.C_TRUE else DietaryPrefObject.C_FALSE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single user-specified dietary preference.
|
||||||
|
*
|
||||||
|
* @param pref the dietary preference that should be retrieved
|
||||||
|
* @return whether the preference needs to be met1
|
||||||
|
*/
|
||||||
|
fun getPreference(pref: DietaryPreferences): Boolean =
|
||||||
|
getDietaryPrefs()[pref.ordinal] == DietaryPrefObject.C_TRUE
|
||||||
|
// val prefString = getDietaryPrefs().toList()
|
||||||
|
// return when (pref) {
|
||||||
|
// DietaryPreferences.FAIR -> prefString[0] == 't'
|
||||||
|
// DietaryPreferences.FISH -> prefString[1] == 't'
|
||||||
|
// DietaryPreferences.POULTRY -> prefString[2] == 't'
|
||||||
|
// DietaryPreferences.LAMB -> prefString[3] == 't'
|
||||||
|
// DietaryPreferences.VITAL -> prefString[4] == 't'
|
||||||
|
// DietaryPreferences.BEEF -> prefString[5] == 't'
|
||||||
|
// DietaryPreferences.PORK -> prefString[6] == 't'
|
||||||
|
// DietaryPreferences.VEGAN -> prefString[7] == 't'
|
||||||
|
// DietaryPreferences.VEGETARIAN -> prefString[8] == 't'
|
||||||
|
// DietaryPreferences.GAME -> prefString[9] == 't'
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all canteen offers from the database.
|
||||||
|
*/
|
||||||
|
fun nukeOffers() {
|
||||||
|
dao.nukeOfferItems()
|
||||||
|
dao.nukeOfferCategories()
|
||||||
|
dao.nukeOfferCanteens()
|
||||||
|
dao.nukeOfferDates()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a date into the database.
|
||||||
|
*
|
||||||
|
* @param date object to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insert(date: OfferDate) { dao.insert(date) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a canteen into the database.
|
||||||
|
*
|
||||||
|
* @param canteen object to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insert(canteen: OfferCanteen) { dao.insert(canteen) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a canteen offer category into the database.
|
||||||
|
*
|
||||||
|
* @param category object to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insert(category: OfferCategory) { dao.insert(category) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a canteen offer item into the database.
|
||||||
|
*
|
||||||
|
* @param item object to be saved to the database
|
||||||
|
*/
|
||||||
|
fun insert(item: OfferItem) { dao.insert(item) }
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.denizk0461.studip.db
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.Query
|
|
||||||
import com.denizk0461.studip.model.*
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface EventDAO {
|
|
||||||
|
|
||||||
// Stud.IP schedule events
|
|
||||||
@get:Query("SELECT * FROM events ORDER BY id")
|
|
||||||
val allEvents: LiveData<List<StudIPEvent>>
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
fun insertEvent(event: StudIPEvent)
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
fun insertEvents(event: List<StudIPEvent>)
|
|
||||||
|
|
||||||
@Query("DELETE FROM events")
|
|
||||||
fun nukeEvents()
|
|
||||||
|
|
||||||
// @get:Query("SELECT * FROM offers ORDER BY id")
|
|
||||||
// val allOffers: LiveData<List<CanteenOffer>>
|
|
||||||
//
|
|
||||||
// @Insert
|
|
||||||
// fun insertOffers(offers: List<CanteenOffer>)
|
|
||||||
//
|
|
||||||
// @Query("DELETE from offers")
|
|
||||||
// fun nukeOffers()
|
|
||||||
|
|
||||||
// canteen offers
|
|
||||||
// get all offers
|
|
||||||
@get:Query(
|
|
||||||
"SELECT * FROM offer_item " +
|
|
||||||
"JOIN offer_category ON offer_item.categoryId = offer_category.id " +
|
|
||||||
"JOIN offer_canteen ON offer_category.canteenId = offer_canteen.id " +
|
|
||||||
"JOIN offer_date ON offer_category.dateId = offer_date.id"
|
|
||||||
)
|
|
||||||
val allOffers: LiveData<List<CanteenOffer>>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
"SELECT * FROM offer_item " +
|
|
||||||
"JOIN offer_category ON offer_item.categoryId = offer_category.id " +
|
|
||||||
"JOIN offer_canteen ON offer_category.canteenId = offer_canteen.id " +
|
|
||||||
"JOIN offer_date ON offer_category.dateId = offer_date.id " +
|
|
||||||
"WHERE dietary_preferences = :prefs "
|
|
||||||
// "WHERE isFair = :isFair " +
|
|
||||||
// "OR isFish = :isFish " +
|
|
||||||
// "OR isPoultry = :isPoultry " +
|
|
||||||
// "OR isLamb = :isLamb " +
|
|
||||||
// "OR isVital = :isVital " +
|
|
||||||
// "OR isBeef = :isBeef " +
|
|
||||||
// "OR isPork = :isPork " +
|
|
||||||
// "OR isVegan = :isVegan " +
|
|
||||||
// "OR isVegetarian = :isVegetarian " +
|
|
||||||
// "OR isGame = :isGame "
|
|
||||||
)
|
|
||||||
fun getOffersByPreference(prefs: String
|
|
||||||
// isFair: Boolean, isFish: Boolean, isPoultry: Boolean, isLamb: Boolean, isVital: Boolean,
|
|
||||||
// isBeef: Boolean, isPork: Boolean, isVegan: Boolean, isVegetarian: Boolean, isGame: Boolean
|
|
||||||
): LiveData<List<CanteenOffer>>
|
|
||||||
|
|
||||||
@get:Query("SELECT id FROM offer_date")
|
|
||||||
val updateObserver: LiveData<List<Int>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM offer_date ORDER BY id")
|
|
||||||
fun getDates(): List<OfferDate>
|
|
||||||
|
|
||||||
@Query("DELETE FROM offer_date")
|
|
||||||
fun nukeOfferDates()
|
|
||||||
|
|
||||||
@Query("DELETE FROM offer_category")
|
|
||||||
fun nukeOfferCategories()
|
|
||||||
|
|
||||||
@Query("DELETE FROM offer_canteen")
|
|
||||||
fun nukeOfferCanteens()
|
|
||||||
|
|
||||||
@Query("DELETE FROM offer_item")
|
|
||||||
fun nukeOfferItems()
|
|
||||||
|
|
||||||
@Insert fun insert(date: OfferDate)
|
|
||||||
@Insert fun insert(canteen: OfferCanteen)
|
|
||||||
@Insert fun insert(category: OfferCategory)
|
|
||||||
@Insert fun insert(item: OfferItem)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.denizk0461.studip.db
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import com.denizk0461.studip.model.*
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
entities = [
|
|
||||||
StudIPEvent::class,
|
|
||||||
OfferDate::class,
|
|
||||||
OfferCanteen::class,
|
|
||||||
OfferCategory::class,
|
|
||||||
OfferItem::class
|
|
||||||
],
|
|
||||||
version = 10,
|
|
||||||
)
|
|
||||||
abstract class EventDatabase : RoomDatabase() {
|
|
||||||
|
|
||||||
abstract fun dao(): EventDAO
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private var instance: EventDatabase? = null
|
|
||||||
|
|
||||||
fun getInstance(context: Context): EventDatabase {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized(EventDatabase::class) {
|
|
||||||
instance = Room.databaseBuilder(
|
|
||||||
context.applicationContext,
|
|
||||||
EventDatabase::class.java,
|
|
||||||
"event_db"
|
|
||||||
).fallbackToDestructiveMigration().build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package com.denizk0461.studip.db
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.denizk0461.studip.model.*
|
|
||||||
|
|
||||||
class EventRepository(app: Application) {
|
|
||||||
|
|
||||||
private val dao: EventDAO = EventDatabase.getInstance(app.applicationContext).dao()
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(app)
|
|
||||||
|
|
||||||
val allEvents: LiveData<List<StudIPEvent>> = dao.allEvents
|
|
||||||
|
|
||||||
private val dietaryPrefString = "prefs_obj"
|
|
||||||
|
|
||||||
fun insertEvent(event: StudIPEvent) { dao.insertEvent(event) }
|
|
||||||
fun insertEvents(events: List<StudIPEvent>) { dao.insertEvents(events) }
|
|
||||||
fun nukeEvents() { dao.nukeEvents() }
|
|
||||||
|
|
||||||
val allOffers: LiveData<List<CanteenOffer>> = dao.allOffers
|
|
||||||
|
|
||||||
fun getDates(): List<OfferDate> = dao.getDates()
|
|
||||||
|
|
||||||
fun getDietaryPrefs(): String {
|
|
||||||
return prefs.getString(dietaryPrefString, "..........") ?: ".........."
|
|
||||||
// return DietaryPrefObject(
|
|
||||||
// isFair = prefs.getBoolean(DietaryPreferences.FAIR.value, false),
|
|
||||||
// isFish = prefs.getBoolean(DietaryPreferences.FISH.value, false),
|
|
||||||
// isPoultry = prefs.getBoolean(DietaryPreferences.POULTRY.value, false),
|
|
||||||
// isLamb = prefs.getBoolean(DietaryPreferences.LAMB.value, false),
|
|
||||||
// isVital = prefs.getBoolean(DietaryPreferences.VITAL.value, false),
|
|
||||||
// isBeef = prefs.getBoolean(DietaryPreferences.BEEF.value, false),
|
|
||||||
// isPork = prefs.getBoolean(DietaryPreferences.PORK.value, false),
|
|
||||||
// isVegan = prefs.getBoolean(DietaryPreferences.VEGAN.value, false),
|
|
||||||
// isVegetarian = prefs.getBoolean(DietaryPreferences.VEGETARIAN.value, false),
|
|
||||||
// isGame = prefs.getBoolean(DietaryPreferences.GAME.value, false),
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDietaryPrefsAsObj(): DietaryPrefObject = DietaryPrefObject.construct(getDietaryPrefs())
|
|
||||||
|
|
||||||
// private fun isPreferenceSet(): Boolean {
|
|
||||||
// getDietaryPrefs().also { p ->
|
|
||||||
// if (p.isFair) return true
|
|
||||||
// if (p.isFish) return true
|
|
||||||
// if (p.isPoultry) return true
|
|
||||||
// if (p.isLamb) return true
|
|
||||||
// if (p.isVital) return true
|
|
||||||
// if (p.isBeef) return true
|
|
||||||
// if (p.isPork) return true
|
|
||||||
// if (p.isVegan) return true
|
|
||||||
// if (p.isVegetarian) return true
|
|
||||||
// if (p.isGame) return true
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun setPreference(pref: DietaryPreferences, newValue: Boolean) {
|
|
||||||
// prefs.edit().putBoolean(pref.value, newValue).apply()
|
|
||||||
val oldString = getDietaryPrefs().toMutableList()
|
|
||||||
when (pref) {
|
|
||||||
DietaryPreferences.FAIR -> oldString[0] = newValue.toChar()
|
|
||||||
DietaryPreferences.FISH -> oldString[1] = newValue.toChar()
|
|
||||||
DietaryPreferences.POULTRY -> oldString[2] = newValue.toChar()
|
|
||||||
DietaryPreferences.LAMB -> oldString[3] = newValue.toChar()
|
|
||||||
DietaryPreferences.VITAL -> oldString[4] = newValue.toChar()
|
|
||||||
DietaryPreferences.BEEF -> oldString[5] = newValue.toChar()
|
|
||||||
DietaryPreferences.PORK -> oldString[6] = newValue.toChar()
|
|
||||||
DietaryPreferences.VEGAN -> oldString[7] = newValue.toChar()
|
|
||||||
DietaryPreferences.VEGETARIAN -> oldString[8] = newValue.toChar()
|
|
||||||
DietaryPreferences.GAME -> oldString[9] = newValue.toChar()
|
|
||||||
}
|
|
||||||
val newString = oldString.joinToString("")
|
|
||||||
Log.d("eek!4", newString)
|
|
||||||
prefs.edit().putString(dietaryPrefString, newString).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Boolean.toChar() = if (this) 't' else '.'
|
|
||||||
|
|
||||||
fun getPreference(pref: DietaryPreferences): Boolean {
|
|
||||||
val prefString = getDietaryPrefs().toList()
|
|
||||||
return when (pref) {
|
|
||||||
DietaryPreferences.FAIR -> prefString[0] == 't'
|
|
||||||
DietaryPreferences.FISH -> prefString[1] == 't'
|
|
||||||
DietaryPreferences.POULTRY -> prefString[2] == 't'
|
|
||||||
DietaryPreferences.LAMB -> prefString[3] == 't'
|
|
||||||
DietaryPreferences.VITAL -> prefString[4] == 't'
|
|
||||||
DietaryPreferences.BEEF -> prefString[5] == 't'
|
|
||||||
DietaryPreferences.PORK -> prefString[6] == 't'
|
|
||||||
DietaryPreferences.VEGAN -> prefString[7] == 't'
|
|
||||||
DietaryPreferences.VEGETARIAN -> prefString[8] == 't'
|
|
||||||
DietaryPreferences.GAME -> prefString[9] == 't'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// prefs.getBoolean(pref.value, false)
|
|
||||||
|
|
||||||
// fun insertOffers(offers: List<CanteenOffer>) { dao.insertOffers(offers) }
|
|
||||||
fun nukeOffers() {
|
|
||||||
dao.nukeOfferItems()
|
|
||||||
dao.nukeOfferCategories()
|
|
||||||
dao.nukeOfferCanteens()
|
|
||||||
dao.nukeOfferDates()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun insert(date: OfferDate) { dao.insert(date) }
|
|
||||||
fun insert(canteen: OfferCanteen) { dao.insert(canteen) }
|
|
||||||
fun insert(category: OfferCategory) { dao.insert(category) }
|
|
||||||
fun insert(item: OfferItem) { dao.insert(item) }
|
|
||||||
}
|
|
||||||
@@ -7,29 +7,44 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import com.denizk0461.studip.adapter.CanteenOfferPageAdapter
|
import com.denizk0461.studip.adapter.CanteenOfferPageAdapter
|
||||||
import com.denizk0461.studip.databinding.FragmentCanteenBinding
|
import com.denizk0461.studip.databinding.FragmentCanteenBinding
|
||||||
import com.denizk0461.studip.model.*
|
import com.denizk0461.studip.model.*
|
||||||
import com.denizk0461.studip.viewmodel.CanteenViewModel
|
import com.denizk0461.studip.viewmodel.CanteenViewModel
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-facing fragment view that displays the canteen offers from the website of the
|
||||||
|
* Studierendenwerk Bremen.
|
||||||
|
*/
|
||||||
class CanteenFragment : Fragment() {
|
class CanteenFragment : Fragment() {
|
||||||
|
|
||||||
|
// Nullable view binding reference
|
||||||
private var _binding: FragmentCanteenBinding? = null
|
private var _binding: FragmentCanteenBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
/*
|
||||||
// onDestroyView.
|
* Non-null reference to the view binding. This property is only valid between onCreateView and
|
||||||
// BUG: if the fragment is changed before the refresh is finished, the app crashes with a NPE
|
* onDestroyView.
|
||||||
|
* BUG if the fragment is changed before the refresh is finished, the app crashes with a NPE
|
||||||
|
*/
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
// Adapter for the view pager displaying all offers
|
||||||
private lateinit var viewPagerAdapter: CanteenOfferPageAdapter
|
private lateinit var viewPagerAdapter: CanteenOfferPageAdapter
|
||||||
|
|
||||||
|
// View model reference for providing access to the database
|
||||||
private val viewModel: CanteenViewModel by viewModels()
|
private val viewModel: CanteenViewModel by viewModels()
|
||||||
|
|
||||||
|
// Elements in the canteen plan
|
||||||
private var elements: List<CanteenOffer> = listOf()
|
private var elements: List<CanteenOffer> = listOf()
|
||||||
private var dateSize = 0
|
|
||||||
|
|
||||||
|
// Count of days displayed in the canteen plan
|
||||||
|
private var dateSize: Int = 0
|
||||||
|
|
||||||
|
// Used if no preference has been set or none can be found
|
||||||
|
private val emptyPreferenceRegex: String = ".........."
|
||||||
|
|
||||||
|
// Instantiate the view binding
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentCanteenBinding.inflate(inflater, container, false)
|
_binding = FragmentCanteenBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -38,6 +53,7 @@ class CanteenFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Assign a preference value to every button to filter for dietary preferences
|
||||||
val chipMap = mapOf(
|
val chipMap = mapOf(
|
||||||
binding.chipPrefFair to DietaryPreferences.FAIR,
|
binding.chipPrefFair to DietaryPreferences.FAIR,
|
||||||
binding.chipPrefFish to DietaryPreferences.FISH,
|
binding.chipPrefFish to DietaryPreferences.FISH,
|
||||||
@@ -51,15 +67,21 @@ class CanteenFragment : Fragment() {
|
|||||||
binding.chipPrefGame to DietaryPreferences.GAME,
|
binding.chipPrefGame to DietaryPreferences.GAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set up every chip
|
||||||
chipMap.forEach { (chip, pref) ->
|
chipMap.forEach { (chip, pref) ->
|
||||||
|
// Set chip to be checked if user has set this preference before
|
||||||
chip.isChecked = getPreference(pref)
|
chip.isChecked = getPreference(pref)
|
||||||
chip.setOnCheckedChangeListener { buttonView, newValue ->
|
|
||||||
setPreference(pref, newValue)
|
|
||||||
Log.d("eek!4", "object as : ${viewModel.getDietaryPrefs().deconstruct()}")
|
|
||||||
|
|
||||||
/* Since this removes the view before immediately adding it back, this method causes a
|
// Set a listener for when the user clicks the chip
|
||||||
* flicker. Note for the future: implement a more efficient / better-looking method.
|
chip.setOnCheckedChangeListener { buttonView, newValue ->
|
||||||
*
|
// Save the preference change to persistent storage
|
||||||
|
setPreference(pref, newValue)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Force an animation on click. This is a subpar method from StackOverflow. Since
|
||||||
|
* it removes the view before immediately adding it back, this method causes a
|
||||||
|
* flicker.
|
||||||
|
* TODO implement a more efficient / better-looking method
|
||||||
* TODO order the chips alphabetically
|
* TODO order the chips alphabetically
|
||||||
*/
|
*/
|
||||||
val index = binding.chipsPreference.indexOfChild(buttonView)
|
val index = binding.chipsPreference.indexOfChild(buttonView)
|
||||||
@@ -68,29 +90,42 @@ class CanteenFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPagerAdapter = CanteenOfferPageAdapter(listOf(), 0, getPrefRegex())
|
// Set up the view pager's adapter
|
||||||
|
viewPagerAdapter = CanteenOfferPageAdapter(listOf(), 0)
|
||||||
|
|
||||||
|
// Assign the adapter to the view pager
|
||||||
binding.viewPager.adapter = viewPagerAdapter
|
binding.viewPager.adapter = viewPagerAdapter
|
||||||
|
|
||||||
|
// Create and attach the tab mediator for the view pager
|
||||||
createTabLayoutMediator()
|
createTabLayoutMediator()
|
||||||
|
|
||||||
|
// Set up LiveData observer to refresh the view on update
|
||||||
viewModel.allOffers.observe(viewLifecycleOwner) { offers ->
|
viewModel.allOffers.observe(viewLifecycleOwner) { offers ->
|
||||||
|
// Update the element list stored in this fragment
|
||||||
elements = offers
|
elements = offers
|
||||||
|
|
||||||
|
// Group elements by category
|
||||||
val groupedElements = offers.groupElements().distinct()
|
val groupedElements = offers.groupElements().distinct()
|
||||||
|
|
||||||
val newDates = groupedElements.map { it.date }.distinct()//.map { item -> OfferDate(item.) }
|
// Find all dates stored in the database
|
||||||
// val newDates = viewModel.getDates()
|
val newDates = groupedElements.map { it.date }.distinct() // TODO viewModel.getDates()
|
||||||
|
|
||||||
|
// Update the date count stored in this fragment
|
||||||
dateSize = newDates.size
|
dateSize = newDates.size
|
||||||
|
|
||||||
|
// Update the item list in the view pager's adapter
|
||||||
viewPagerAdapter.setNewItems(groupedElements, dateSize)
|
viewPagerAdapter.setNewItems(groupedElements, dateSize)
|
||||||
// createTabLayoutMediator(newDates)
|
|
||||||
|
|
||||||
// TODO this must be changed. if an exception is raised, then this will not fire
|
/*
|
||||||
|
* Tell the swipe refresh layout to stop refreshing.
|
||||||
|
* TODO if an exception is raised, this will not be fired. This must be changed
|
||||||
|
*/
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up functions for when the user swipes to refresh the view
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
|
// Retrieve new offers from the website(s)
|
||||||
viewModel.fetchOffers(onRefreshUpdate = { status ->
|
viewModel.fetchOffers(onRefreshUpdate = { status ->
|
||||||
// TODO refresh updates
|
// TODO refresh updates
|
||||||
}, onFinish = {
|
}, onFinish = {
|
||||||
@@ -100,7 +135,11 @@ class CanteenFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTabLayoutMediator(){//List<OfferDate>) {
|
/**
|
||||||
|
* Create and attach the object mediating the tabs for the view pager.
|
||||||
|
*/
|
||||||
|
private fun createTabLayoutMediator() {
|
||||||
|
// Fetch all dates from the database
|
||||||
val dates = viewModel.getDates()
|
val dates = viewModel.getDates()
|
||||||
if (dates.isNotEmpty()) {
|
if (dates.isNotEmpty()) {
|
||||||
Log.d("eek!7", "dates: ${dates}, viewpager pages: ${binding.viewPager.adapter?.itemCount}")
|
Log.d("eek!7", "dates: ${dates}, viewpager pages: ${binding.viewPager.adapter?.itemCount}")
|
||||||
@@ -111,57 +150,106 @@ class CanteenFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate the view binding
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a given preference and refreshes the view.
|
||||||
|
*
|
||||||
|
* @param pref preference to be updated
|
||||||
|
* @param newValue value to set the preference to
|
||||||
|
*/
|
||||||
private fun setPreference(pref: DietaryPreferences, newValue: Boolean) {
|
private fun setPreference(pref: DietaryPreferences, newValue: Boolean) {
|
||||||
|
// Update the preference
|
||||||
viewModel.setPreference(pref, newValue)
|
viewModel.setPreference(pref, newValue)
|
||||||
|
|
||||||
|
// Update the view
|
||||||
viewPagerAdapter.setNewItems(elements.groupElements().distinct(), dateSize)
|
viewPagerAdapter.setNewItems(elements.groupElements().distinct(), dateSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single user-specified dietary preference.
|
||||||
|
*
|
||||||
|
* @param pref the dietary preference that should be retrieved
|
||||||
|
* @return whether the preference needs to be met
|
||||||
|
*/
|
||||||
private fun getPreference(pref: DietaryPreferences): Boolean =
|
private fun getPreference(pref: DietaryPreferences): Boolean =
|
||||||
viewModel.getPreference(pref)
|
viewModel.getPreference(pref)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
private fun getPrefRegex(): Regex {
|
||||||
val prefs = viewModel.getDietaryPrefs().deconstruct()//.replace('f', '.')
|
// Retrieve the user's dietary preference as a string
|
||||||
|
val prefs = viewModel.getDietaryPrefs().deconstruct()
|
||||||
|
|
||||||
val template = ".........."
|
// Return the 'empty' template string if no preference has been set or found
|
||||||
|
return if (prefs == emptyPreferenceRegex) {
|
||||||
return if (prefs == "..........") {
|
Regex(emptyPreferenceRegex)
|
||||||
Regex(template)
|
|
||||||
} else {
|
} else {
|
||||||
|
// Create the variable to insert the regular expression into
|
||||||
var regexString = ""
|
var regexString = ""
|
||||||
|
|
||||||
|
// Create the object to store which preferences need to be met
|
||||||
val indices = mutableListOf<Int>()
|
val indices = mutableListOf<Int>()
|
||||||
|
|
||||||
|
// Find all preferences that need to be met
|
||||||
prefs.forEachIndexed { index, c ->
|
prefs.forEachIndexed { index, c ->
|
||||||
if (c == 't') indices.add(index)
|
if (c == DietaryPrefObject.C_TRUE) indices.add(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needs to be set to correctly assemble the regular expression
|
||||||
var isFirst = true
|
var isFirst = true
|
||||||
|
|
||||||
|
// Iterate through every preference that has been set
|
||||||
indices.forEach { index ->
|
indices.forEach { index ->
|
||||||
|
// Set an OR if more than one preference needs to be met
|
||||||
if (!isFirst) regexString += '|'
|
if (!isFirst) regexString += '|'
|
||||||
regexString += template.substring(0 until index) + 't' + template.substring(index+1)
|
|
||||||
|
// Add the expression looking for the single preference to the entire string
|
||||||
|
regexString += emptyPreferenceRegex.substring(0 until index) +
|
||||||
|
DietaryPrefObject.C_TRUE +
|
||||||
|
emptyPreferenceRegex.substring(index+1)
|
||||||
|
|
||||||
|
// Ensure that OR statements will be placed between the individual expressions
|
||||||
isFirst = false
|
isFirst = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compile the string to a regular expression object
|
||||||
Regex(regexString)
|
Regex(regexString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
private fun List<CanteenOffer>.groupElements(): List<CanteenOfferGroup> {
|
||||||
|
// Get dietary preference regex
|
||||||
val prefsRegex = getPrefRegex()
|
val prefsRegex = getPrefRegex()
|
||||||
|
|
||||||
val filteredForPreferences = if (prefsRegex.toString() == "ffffffffff") {
|
val filteredForPreferences = if (prefsRegex.toString() == emptyPreferenceRegex) {
|
||||||
// show all elements and skip filtering
|
// Show all elements and skip filtering if no preference is set
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
|
// Filter for dietary preferences
|
||||||
this.filter {
|
this.filter {
|
||||||
prefsRegex.matches(it.dietaryPreferences)
|
prefsRegex.matches(it.dietaryPreferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val b = filteredForPreferences.map { offer ->
|
return filteredForPreferences.map { offer ->
|
||||||
|
// Create new group elements to group the already filtered elements by their categories
|
||||||
CanteenOfferGroup(
|
CanteenOfferGroup(
|
||||||
offer.date,
|
offer.date,
|
||||||
offer.dateId,
|
offer.dateId,
|
||||||
@@ -170,6 +258,7 @@ class CanteenFragment : Fragment() {
|
|||||||
filteredForPreferences.filter {
|
filteredForPreferences.filter {
|
||||||
it.category == offer.category && it.date == offer.date && it.canteen == offer.canteen
|
it.category == offer.category && it.date == offer.date && it.canteen == offer.canteen
|
||||||
}.map {
|
}.map {
|
||||||
|
// Map the individual items to their respective groups
|
||||||
CanteenOfferGroupElement(
|
CanteenOfferGroupElement(
|
||||||
it.title,
|
it.title,
|
||||||
it.price,
|
it.price,
|
||||||
@@ -178,6 +267,5 @@ class CanteenFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,17 +16,29 @@ import com.google.android.material.tabs.TabLayoutMediator
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple [Fragment] subclass as the default destination in the navigation.
|
* User-facing fragment view that displays the user's Stud.IP schedule.
|
||||||
*/
|
*/
|
||||||
class EventFragment : Fragment() {
|
class EventFragment : Fragment() {
|
||||||
|
|
||||||
|
// Nullable view binding reference
|
||||||
private var _binding: FragmentEventBinding? = null
|
private var _binding: FragmentEventBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
/*
|
||||||
// onDestroyView.
|
* Non-null reference to the view binding. This property is only valid between onCreateView and
|
||||||
|
* onDestroyView.
|
||||||
|
*/
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
// Adapter for the view pager displaying all events
|
||||||
|
private lateinit var viewPagerAdapter: StudIPEventPageAdapter
|
||||||
|
|
||||||
|
// View model reference for providing access to the database
|
||||||
|
private val viewModel: EventViewModel by viewModels()
|
||||||
|
|
||||||
|
// Used to determine the current day
|
||||||
private var dayOfWeek: Int = 0
|
private var dayOfWeek: Int = 0
|
||||||
|
|
||||||
|
// Titles for the view pager's tabs
|
||||||
private val dayStrings = listOf(
|
private val dayStrings = listOf(
|
||||||
R.string.monday,
|
R.string.monday,
|
||||||
R.string.tuesday,
|
R.string.tuesday,
|
||||||
@@ -35,9 +47,7 @@ class EventFragment : Fragment() {
|
|||||||
R.string.friday,
|
R.string.friday,
|
||||||
)
|
)
|
||||||
|
|
||||||
private lateinit var viewPagerAdapter: StudIPEventPageAdapter
|
// Instantiate the view binding
|
||||||
private val viewModel: EventViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentEventBinding.inflate(inflater, container, false)
|
_binding = FragmentEventBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -46,6 +56,10 @@ class EventFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determine the current day.
|
||||||
|
* TODO expand to weekend
|
||||||
|
*/
|
||||||
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
|
||||||
@@ -54,42 +68,53 @@ class EventFragment : Fragment() {
|
|||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up the view pager's adapter
|
||||||
viewPagerAdapter = StudIPEventPageAdapter(listOf(), object : StudIPEventItemAdapter.OnClickListener {
|
viewPagerAdapter = StudIPEventPageAdapter(listOf(), object : StudIPEventItemAdapter.OnClickListener {
|
||||||
override fun onClick(event: StudIPEvent) {
|
override fun onClick(event: StudIPEvent) {
|
||||||
|
// TODO implement functionality or delete
|
||||||
}
|
}
|
||||||
override fun onLongClick(event: StudIPEvent) {
|
override fun onLongClick(event: StudIPEvent): Boolean {
|
||||||
|
// TODO implement functionality or delete
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
})//DummyData.events.toList())
|
})
|
||||||
|
|
||||||
|
// Assign the adapter to the view pager
|
||||||
binding.viewPager.adapter = viewPagerAdapter
|
binding.viewPager.adapter = viewPagerAdapter
|
||||||
|
|
||||||
|
// 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 = getString(dayStrings[position])
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
|
// Set up LiveData observer to refresh the view on update
|
||||||
viewModel.allEvents.observe(viewLifecycleOwner) { events ->
|
viewModel.allEvents.observe(viewLifecycleOwner) { events ->
|
||||||
|
// Update the item list in the view pager's adapter
|
||||||
viewPagerAdapter.setNewItems(events)
|
viewPagerAdapter.setNewItems(events)
|
||||||
|
|
||||||
|
// Scroll to the current day
|
||||||
switchToCurrentDayView()
|
switchToCurrentDayView()
|
||||||
}
|
}
|
||||||
|
|
||||||
// binding.buttonFirst.setOnClickListener {
|
|
||||||
// findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Scroll to the current day
|
||||||
switchToCurrentDayView()
|
switchToCurrentDayView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate the view binding
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to the current day. Current day is determined after the fragment's view has been
|
||||||
|
* created.
|
||||||
|
*/
|
||||||
private fun switchToCurrentDayView() {
|
private fun switchToCurrentDayView() {
|
||||||
binding.viewPager.currentItem = dayOfWeek
|
binding.viewPager.currentItem = dayOfWeek
|
||||||
// binding.dayTabLayout.getTabAt(dayOfWeek)?.select()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package com.denizk0461.studip.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import com.denizk0461.studip.data.StudIPParser
|
|
||||||
import com.denizk0461.studip.databinding.FragmentParserBinding
|
|
||||||
import com.denizk0461.studip.viewmodel.FetcherViewModel
|
|
||||||
import java.net.URLDecoder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple [Fragment] subclass as the second destination in the navigation.
|
|
||||||
*/
|
|
||||||
class ParserFragment : Fragment() {
|
|
||||||
|
|
||||||
private var html = "" // temporary storage for the website HTML
|
|
||||||
|
|
||||||
private var _binding: FragmentParserBinding? = null
|
|
||||||
// This property is only valid between onCreateView and
|
|
||||||
// onDestroyView.
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val viewModel: FetcherViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
|
|
||||||
_binding = FragmentParserBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
// binding.buttonSecond.setOnClickListener {
|
|
||||||
// findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
|
|
||||||
// }
|
|
||||||
|
|
||||||
binding.webview.settings.apply {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
}
|
|
||||||
binding.webview.webViewClient = object : WebViewClient() {
|
|
||||||
override fun shouldOverrideUrlLoading(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest
|
|
||||||
): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
|
||||||
binding.webview.loadUrl(
|
|
||||||
"javascript:window.HtmlViewer.showHTML" +
|
|
||||||
"('<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>');"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes")
|
|
||||||
|
|
||||||
binding.fab.setOnClickListener { view ->
|
|
||||||
binding.webview.evaluateJavascript(
|
|
||||||
"(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();"
|
|
||||||
) { p0 ->
|
|
||||||
viewModel.nukeEvents()
|
|
||||||
StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events ->
|
|
||||||
viewModel.insertEvents(events)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// html.chunked(3000).forEach {
|
|
||||||
// Log.d("HELLO", it)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,23 +7,32 @@ 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 android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.denizk0461.studip.BuildConfig
|
import com.denizk0461.studip.BuildConfig
|
||||||
import com.denizk0461.studip.activity.FetcherActivity
|
import com.denizk0461.studip.activity.FetcherActivity
|
||||||
import com.denizk0461.studip.data.Misc
|
|
||||||
import com.denizk0461.studip.databinding.FragmentSettingsBinding
|
import com.denizk0461.studip.databinding.FragmentSettingsBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-facing fragment view that is used to change app settings.
|
||||||
|
*/
|
||||||
class SettingsFragment : Fragment() {
|
class SettingsFragment : Fragment() {
|
||||||
|
|
||||||
|
// Nullable view binding reference
|
||||||
private var _binding: FragmentSettingsBinding? = null
|
private var _binding: FragmentSettingsBinding? = null
|
||||||
// This property is only valid between onCreateView and
|
|
||||||
// onDestroyView.
|
/*
|
||||||
|
* Non-null reference to the view binding. This property is only valid between onCreateView and
|
||||||
|
* onDestroyView.
|
||||||
|
*/
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
// Click counter on the app version button
|
||||||
private var appVersionClick = 0
|
private var appVersionClick = 0
|
||||||
|
|
||||||
private var isQuarterChecked = false
|
// 222
|
||||||
|
private val mysteryLink = "https://www.youtube.com/watch?v=nhIQMCXJzLI"
|
||||||
|
|
||||||
|
// Instantiate the view binding
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -32,47 +41,39 @@ class SettingsFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// binding.layoutQuarter.setOnClickListener {
|
// Launch the Stud.IP schedule fetcher activity
|
||||||
// isQuarterChecked = !isQuarterChecked
|
|
||||||
// binding.switchQuarter.isChecked = isQuarterChecked
|
|
||||||
// }
|
|
||||||
|
|
||||||
binding.buttonRefreshSchedule.setOnClickListener {
|
binding.buttonRefreshSchedule.setOnClickListener {
|
||||||
launchWebView()
|
launchWebView()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.switchQuarter.setOnClickListener {
|
// Set click listener for the app version button
|
||||||
isQuarterChecked = !isQuarterChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonAppVersion.setOnClickListener {
|
binding.buttonAppVersion.setOnClickListener {
|
||||||
when (appVersionClick) {
|
when (appVersionClick) {
|
||||||
19 -> {
|
|
||||||
appVersionClick += 1
|
|
||||||
}
|
|
||||||
20 -> {
|
|
||||||
appVersionClick += 1
|
|
||||||
}
|
|
||||||
21 -> {
|
|
||||||
appVersionClick += 1
|
|
||||||
}
|
|
||||||
22 -> {
|
22 -> {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Misc.mysteryLink)))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(mysteryLink)))
|
||||||
appVersionClick = 0
|
appVersionClick = 0
|
||||||
}
|
}
|
||||||
else -> appVersionClick += 1
|
else -> appVersionClick += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Display the app's version as set in the build.gradle. Also display if the app is a
|
||||||
|
* development version.
|
||||||
|
*/
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
binding.appVersionText.text = "${BuildConfig.VERSION_NAME}-${if (BuildConfig.DEBUG) "debug" else "release"}"
|
binding.appVersionText.text = "${BuildConfig.VERSION_NAME}-${if (BuildConfig.DEBUG) "dev" else "release"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate the view binding
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the Stud.IP schedule fetcher activity.
|
||||||
|
*/
|
||||||
private fun launchWebView() {
|
private fun launchWebView() {
|
||||||
startActivity(Intent(context, FetcherActivity::class.java))
|
startActivity(Intent(context, FetcherActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,25 @@ package com.denizk0461.studip.model
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
|
|
||||||
//@Entity(tableName = "offers")
|
/**
|
||||||
|
* Entity that is constructed by combining instances of OfferItem.kt (itemId, title, price,
|
||||||
|
* dietaryPreferences), OfferCategory.kt (category, categoryId), OfferCanteen.kt (canteen,
|
||||||
|
* canteenId), and OfferDate.kt (date, dateId) to be used as a data type for a custom RecyclerView
|
||||||
|
* adapter class.
|
||||||
|
*
|
||||||
|
* @param itemId unique identifier of the item
|
||||||
|
* @param date date formatted as dd.MM.
|
||||||
|
* @param dateId ID of the corresponding OfferDate.kt instance
|
||||||
|
* @param category category text content of the item
|
||||||
|
* @param categoryId ID of the corresponding OfferCategory.kt instance
|
||||||
|
* @param canteen name of the canteen
|
||||||
|
* @param canteenId ID of the corresponding OfferCanteen.kt instance
|
||||||
|
* @param title text content of the individual canteen offer
|
||||||
|
* @param price students' price for the individual canteen offer
|
||||||
|
* @param dietaryPreferences dietary preferences used to filter for the user's needs - see
|
||||||
|
* DietaryPrefObject.kt
|
||||||
|
*/
|
||||||
data class CanteenOffer(
|
data class CanteenOffer(
|
||||||
// @PrimaryKey val id: Int,
|
|
||||||
val itemId: Int,
|
val itemId: Int,
|
||||||
val date: String,
|
val date: String,
|
||||||
val dateId: Int,
|
val dateId: Int,
|
||||||
@@ -13,6 +29,6 @@ data class CanteenOffer(
|
|||||||
val canteen: String,
|
val canteen: String,
|
||||||
val canteenId: Int,
|
val canteenId: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val price: String, // price for students
|
val price: String,
|
||||||
@ColumnInfo(name = "dietary_preferences") val dietaryPreferences: String,
|
@ColumnInfo(name = "dietary_preferences") val dietaryPreferences: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
package com.denizk0461.studip.model
|
package com.denizk0461.studip.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grouping element that holds instances of CanteenOfferGroupElement.kt that share date, category,
|
||||||
|
* and canteen values. Used to filter more easily
|
||||||
|
*
|
||||||
|
* @param date date formatted as dd.MM.
|
||||||
|
* @param dateId ID of the corresponding OfferDate.kt instance
|
||||||
|
* @param category category text content of the item
|
||||||
|
* @param canteen name of the canteen
|
||||||
|
* @param offers elements of CanteenOfferGroupElement.kt with common values
|
||||||
|
*/
|
||||||
data class CanteenOfferGroup(
|
data class CanteenOfferGroup(
|
||||||
val date: String,
|
val date: String,
|
||||||
val dateId: Int,
|
val dateId: Int,
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
package com.denizk0461.studip.model
|
package com.denizk0461.studip.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stripped version of CanteenOffer.kt that only carries essential info. Assumes that instances of
|
||||||
|
* this are assigned to corresponding date, canteen, and category via an instance of
|
||||||
|
* CanteenOfferGroup.kt.
|
||||||
|
*
|
||||||
|
* @param title text content of the individual canteen offer
|
||||||
|
* @param price students' price for the individual canteen offer
|
||||||
|
* @param dietaryPreferences dietary preferences used to filter for the user's needs - see
|
||||||
|
* DietaryPrefObject.kt
|
||||||
|
*/
|
||||||
data class CanteenOfferGroupElement(
|
data class CanteenOfferGroupElement(
|
||||||
val title: String,
|
val title: String,
|
||||||
val price: String,
|
val price: String,
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
package com.denizk0461.studip.model
|
package com.denizk0461.studip.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object that holds dietary preferences. Can be used to hold both the user-set values as well as
|
||||||
|
* the values of an individual canteen item.
|
||||||
|
*
|
||||||
|
* @param isFair item contains meat from fairly-treated animals (lol sure)
|
||||||
|
* @param isFish item contains fish
|
||||||
|
* @param isPoultry item contains chicken
|
||||||
|
* @param isLamb item contains lamb
|
||||||
|
* @param isVital i don't actually know
|
||||||
|
* @param isBeef item contains beef
|
||||||
|
* @param isPork item contains pork
|
||||||
|
* @param isVegan item is plant-based; contains no animal-derived ingredients
|
||||||
|
* @param isVegetarian item is vegetarian; contains no animal parts
|
||||||
|
* @param isGame item contains game meat (e.g. deer)
|
||||||
|
*/
|
||||||
data class DietaryPrefObject(
|
data class DietaryPrefObject(
|
||||||
val isFair: Boolean,
|
val isFair: Boolean,
|
||||||
val isFish: Boolean,
|
val isFish: Boolean,
|
||||||
@@ -13,11 +28,18 @@ data class DietaryPrefObject(
|
|||||||
val isGame: Boolean,
|
val isGame: Boolean,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val C_TRUE = 't'
|
// Char used to construct a regex to denote that a preference is met
|
||||||
private const val C_FALSE = '.'
|
const val C_TRUE = 't'
|
||||||
/* assume that the values must be parsed as follows:
|
// Char used to construct a regex to denote that a preference is not met
|
||||||
* "xxxxxxxxxx"
|
const val C_FALSE = '.'
|
||||||
* t = true, . = false
|
|
||||||
|
/**
|
||||||
|
* Constructs a DietaryPrefObject from a regex string. String must be 10 characters long.
|
||||||
|
* A char equal to C_TRUE is treated as a true boolean value. Any other char is treated as a
|
||||||
|
* false boolean value.
|
||||||
|
*
|
||||||
|
* @param values the regex string
|
||||||
|
* @return an instance of DietaryPrefObject
|
||||||
*/
|
*/
|
||||||
fun construct(values: String) = DietaryPrefObject(
|
fun construct(values: String) = DietaryPrefObject(
|
||||||
isFair = values[0] == C_TRUE,
|
isFair = values[0] == C_TRUE,
|
||||||
@@ -33,6 +55,15 @@ data class DietaryPrefObject(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a regular expression string from a DietaryPrefObject. This can be used to filter for specific
|
||||||
|
* preferences. Example:
|
||||||
|
* .......t..
|
||||||
|
* This expression filters for items that are vegan ('t' in position 8) and ignores all other
|
||||||
|
* preference options ('.' in all other positions).
|
||||||
|
*
|
||||||
|
* @return a 10-character long regular expression
|
||||||
|
*/
|
||||||
fun deconstruct() = String(
|
fun deconstruct() = String(
|
||||||
charArrayOf(
|
charArrayOf(
|
||||||
if (this.isFair) C_TRUE else C_FALSE,
|
if (this.isFair) C_TRUE else C_FALSE,
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
package com.denizk0461.studip.model
|
package com.denizk0461.studip.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration that can be used in conjunction with DietaryPrefObject.kt to more concisely look up,
|
||||||
|
* specify, and iterate through dietary preferences. Order of these elements matches the order used
|
||||||
|
* on the website of the Studierendenwerk Bremen.
|
||||||
|
*
|
||||||
|
* @param value string value that can be used where the enum value itself cannot be used (e.g.
|
||||||
|
* SharedPreferences)
|
||||||
|
*/
|
||||||
enum class DietaryPreferences(val value: String) {
|
enum class DietaryPreferences(val value: String) {
|
||||||
FAIR("isFair"),
|
FAIR("isFair"),
|
||||||
FISH("isFish"),
|
FISH("isFish"),
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ package com.denizk0461.studip.model
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity for binding several canteen items to a specific date. Registered in the app database.
|
||||||
|
*
|
||||||
|
* @param id primary key that uniquely identifies the item
|
||||||
|
* @param canteen name of the canteen
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "offer_canteen",
|
tableName = "offer_canteen",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ import androidx.room.Entity
|
|||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity for storing categories/headers grouping individual instances of OfferItem.kt. Registered
|
||||||
|
* in the app database.
|
||||||
|
*
|
||||||
|
* @param id key that uniquely identifies the item
|
||||||
|
* @param dateId foreign key binding the item to an instance of OfferDate.kt
|
||||||
|
* @param canteenId foreign key binding the item to an instance of OfferCanteen.kt
|
||||||
|
* @param category text content of the item
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "offer_category",
|
tableName = "offer_category",
|
||||||
foreignKeys = [
|
foreignKeys = [
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ package com.denizk0461.studip.model
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity for binding several canteen items to a specific date. Registered in the app database.
|
||||||
|
*
|
||||||
|
* @param id primary key that uniquely identifies the item
|
||||||
|
* @param date date formatted as dd.MM.
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "offer_date",
|
tableName = "offer_date",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ import androidx.room.Entity
|
|||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity for storing individual items of a university canteen. Registered in the app database.
|
||||||
|
*
|
||||||
|
* @param itemId primary key that uniquely identifies the item
|
||||||
|
* @param categoryId foreign key binding the item to an instance of OfferCategory.kt
|
||||||
|
* @param title text content of the individual canteen offer
|
||||||
|
* @param price students' price for the individual canteen offer
|
||||||
|
* @param dietaryPreferences dietary preferences used to filter for the user's needs - see
|
||||||
|
* DietaryPrefObject.kt
|
||||||
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "offer_item",
|
tableName = "offer_item",
|
||||||
foreignKeys = [
|
foreignKeys = [
|
||||||
@@ -22,14 +32,4 @@ data class OfferItem(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val price: String,
|
val price: String,
|
||||||
@ColumnInfo(name = "dietary_preferences") val dietaryPreferences: String,
|
@ColumnInfo(name = "dietary_preferences") val dietaryPreferences: String,
|
||||||
// val isFair: Boolean,
|
|
||||||
// val isFish: Boolean,
|
|
||||||
// val isPoultry: Boolean,
|
|
||||||
// val isLamb: Boolean,
|
|
||||||
// val isVital: Boolean,
|
|
||||||
// val isBeef: Boolean,
|
|
||||||
// val isPork: Boolean,
|
|
||||||
// val isVegan: Boolean,
|
|
||||||
// val isVegetarian: Boolean,
|
|
||||||
// val isGame: Boolean,
|
|
||||||
)
|
)
|
||||||
@@ -8,18 +8,57 @@ import com.denizk0461.studip.model.DietaryPrefObject
|
|||||||
import com.denizk0461.studip.model.DietaryPreferences
|
import com.denizk0461.studip.model.DietaryPreferences
|
||||||
import com.denizk0461.studip.model.OfferDate
|
import com.denizk0461.studip.model.OfferDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model for [com.denizk0461.studip.fragment.CanteenFragment]
|
||||||
|
*
|
||||||
|
* @param app reference to the app
|
||||||
|
*/
|
||||||
class CanteenViewModel(app: Application) : TemplateViewModel(app) {
|
class CanteenViewModel(app: Application) : TemplateViewModel(app) {
|
||||||
|
|
||||||
|
// Instantiate a parser, should the user want to refresh the canteen offers
|
||||||
private val parser = StwParser()
|
private val parser = StwParser()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all Stud.IP events.
|
||||||
|
*
|
||||||
|
* @return all Stud.IP events exposed through a LiveData object
|
||||||
|
*/
|
||||||
val allOffers: LiveData<List<CanteenOffer>> = repo.allOffers
|
val allOffers: LiveData<List<CanteenOffer>> = repo.allOffers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the user's dietary preferences.
|
||||||
|
*
|
||||||
|
* @return dietary preferences
|
||||||
|
*/
|
||||||
fun getDietaryPrefs(): DietaryPrefObject = repo.getDietaryPrefsAsObj()
|
fun getDietaryPrefs(): DietaryPrefObject = repo.getDietaryPrefsAsObj()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all canteen offer date objects.
|
||||||
|
*
|
||||||
|
* @return a list of instances of canteen offer dates
|
||||||
|
*/
|
||||||
fun getDates(): List<OfferDate> = returnBlocking { repo.getDates() }
|
fun getDates(): List<OfferDate> = returnBlocking { repo.getDates() }
|
||||||
|
|
||||||
fun fetchOffers(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) { doAsync { parser.parse(onRefreshUpdate, onFinish) }}
|
// Fetch the canteen offers asynchronously. Updates will be provided through a LiveData object.
|
||||||
|
fun fetchOffers(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
|
||||||
|
doAsync { parser.parse(onRefreshUpdate, onFinish) }
|
||||||
|
}
|
||||||
|
|
||||||
fun setPreference(pref: DietaryPreferences, newValue: Boolean) { repo.setPreference(pref, newValue) }
|
/**
|
||||||
|
* Updates a given dietary preference to a new value.
|
||||||
|
*
|
||||||
|
* @param pref preference to be updated
|
||||||
|
* @param newValue value to set the preference to
|
||||||
|
*/
|
||||||
|
fun setPreference(pref: DietaryPreferences, newValue: Boolean) {
|
||||||
|
repo.setPreference(pref, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single user-specified dietary preference.
|
||||||
|
*
|
||||||
|
* @param pref the dietary preference that should be retrieved
|
||||||
|
* @return whether the preference needs to be met
|
||||||
|
*/
|
||||||
fun getPreference(pref: DietaryPreferences): Boolean = repo.getPreference(pref)
|
fun getPreference(pref: DietaryPreferences): Boolean = repo.getPreference(pref)
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,17 @@ import android.app.Application
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.denizk0461.studip.model.StudIPEvent
|
import com.denizk0461.studip.model.StudIPEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model for [com.denizk0461.studip.fragment.EventFragment]
|
||||||
|
*
|
||||||
|
* @param app reference to the app
|
||||||
|
*/
|
||||||
class EventViewModel(app: Application) : TemplateViewModel(app) {
|
class EventViewModel(app: Application) : TemplateViewModel(app) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all Stud.IP events.
|
||||||
|
*
|
||||||
|
* @return all Stud.IP events exposed through a LiveData object
|
||||||
|
*/
|
||||||
val allEvents: LiveData<List<StudIPEvent>> = repo.allEvents
|
val allEvents: LiveData<List<StudIPEvent>> = repo.allEvents
|
||||||
|
|
||||||
fun insertEvent(event: StudIPEvent) { repo.insertEvent(event) }
|
|
||||||
fun nukeEvents() { repo.nukeEvents() }
|
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,22 @@ package com.denizk0461.studip.viewmodel
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.denizk0461.studip.model.StudIPEvent
|
import com.denizk0461.studip.model.StudIPEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model for [com.denizk0461.studip.activity.FetcherActivity]
|
||||||
|
*
|
||||||
|
* @param app reference to the app
|
||||||
|
*/
|
||||||
class FetcherViewModel(app: Application) : TemplateViewModel(app) {
|
class FetcherViewModel(app: Application) : TemplateViewModel(app) {
|
||||||
|
|
||||||
fun insertEvent(event: StudIPEvent) { repo.insertEvent(event) }
|
/**
|
||||||
|
* Save a list of Stud.IP events to persistent storage asynchronously.
|
||||||
|
*
|
||||||
|
* @param events list of events to be saved
|
||||||
|
*/
|
||||||
fun insertEvents(events: List<StudIPEvent>) { doAsync { repo.insertEvents(events) } }
|
fun insertEvents(events: List<StudIPEvent>) { doAsync { repo.insertEvents(events) } }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all Stud.IP events from the database asynchronously.
|
||||||
|
*/
|
||||||
fun nukeEvents() { doAsync { repo.nukeEvents() } }
|
fun nukeEvents() { doAsync { repo.nukeEvents() } }
|
||||||
}
|
}
|
||||||
@@ -4,21 +4,43 @@ import android.app.Application
|
|||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.denizk0461.studip.data.Dependencies
|
import com.denizk0461.studip.data.Dependencies
|
||||||
import com.denizk0461.studip.db.EventRepository
|
import com.denizk0461.studip.db.AppRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View model super class providing common functionality. View model is used to provide an
|
||||||
|
* abstraction between the view classes and the data providers. All view models should inherit from
|
||||||
|
* this.
|
||||||
|
*
|
||||||
|
* @param app reference to the app
|
||||||
|
*/
|
||||||
open class TemplateViewModel(app: Application) : AndroidViewModel(app) {
|
open class TemplateViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
|
|
||||||
protected val repo: EventRepository = Dependencies.repo
|
// Reference to the app's repository for database transactions
|
||||||
|
protected val repo: AppRepository = Dependencies.repo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function asynchronously on the I/O thread. Be careful not to execute UI commands
|
||||||
|
* with this.
|
||||||
|
*
|
||||||
|
* @param function the action that will be executed asynchronously
|
||||||
|
*/
|
||||||
fun doAsync(function: () -> Unit) {
|
fun doAsync(function: () -> Unit) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
function()
|
function()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function that needs to be ran on an I/O thread and return a value. This is a simple
|
||||||
|
* but inefficient solution to retrieving data from the database without the need for a LiveData
|
||||||
|
* object, which should always be preferred.
|
||||||
|
*
|
||||||
|
* @param function the action that will be executed on the I/O thread
|
||||||
|
* @return any value returned by the executing function
|
||||||
|
*/
|
||||||
fun <T> returnBlocking(function: () -> T): T = runBlocking(Dispatchers.IO) {
|
fun <T> returnBlocking(function: () -> T): T = runBlocking(Dispatchers.IO) {
|
||||||
function()
|
function()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".fragment.ParserFragment">
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:id="@+id/webview"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin"
|
|
||||||
app:srcCompat="@drawable/check" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
Reference in New Issue
Block a user