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.data.Dependencies
|
||||
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.CanteenFragment
|
||||
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
|
||||
* data.
|
||||
*/
|
||||
Dependencies.repo = EventRepository(application)
|
||||
Dependencies.repo = AppRepository(application)
|
||||
|
||||
// Inflate view binding and bind to this activity
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
||||
@@ -2,18 +2,27 @@ package com.denizk0461.studip.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
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.ItemCanteenLineBinding
|
||||
import com.denizk0461.studip.databinding.ItemIconBinding
|
||||
import com.denizk0461.studip.model.CanteenOffer
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
// TODO inflate a view saying "no events" if none are available for a given day
|
||||
override fun onBindViewHolder(holder: OfferViewHolder, position: Int) {
|
||||
// Retrieve item for current position
|
||||
val currentItem = offers[position]
|
||||
|
||||
// Set category text / header of the item
|
||||
holder.binding.textCategory.text = currentItem.category
|
||||
|
||||
// Create a new view (line) for each item displayed within one category
|
||||
currentItem.offers.forEach { offer ->
|
||||
// Inflate the new line without binding it to the line container
|
||||
val line = ItemCanteenLineBinding.inflate(
|
||||
LayoutInflater.from(holder.binding.root.context),
|
||||
)
|
||||
|
||||
// Set text values
|
||||
line.textContent.text = offer.title
|
||||
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>()
|
||||
offer.dietaryPreferences.filterIndexed { index, char ->
|
||||
if (char == 't') {
|
||||
@@ -47,25 +68,44 @@ class CanteenOfferItemAdapter(private val offers: List<CanteenOfferGroup>) : Rec
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
// If no dietary preferences are met, set the view to display a neutral icon
|
||||
if (indices.isEmpty()) indices.add(10)
|
||||
|
||||
// Iterate through all icons that need to be displayed
|
||||
indices.forEach { index ->
|
||||
// Inflate a new icon holder
|
||||
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)
|
||||
}
|
||||
|
||||
// Bind the new line to the line container
|
||||
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)
|
||||
// fun onLongClick(event: StudIPEvent)
|
||||
// }
|
||||
/**
|
||||
* Provides a converting function between the index of a dietary preference and its according
|
||||
* 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.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)
|
||||
|
||||
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 onBindViewHolder(holder: CanteenOfferPageViewHolder, position: Int) {
|
||||
|
||||
holder.binding.pageRecyclerView.apply {
|
||||
|
||||
// Set page to horizontally scroll
|
||||
layoutManager = LinearLayoutManager(holder.binding.root.context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
/*
|
||||
* Create new adapter for every page. Attribute position denotes day that will be set up
|
||||
* by the newly created adapter (0 = Monday, 4 = Friday)
|
||||
*/
|
||||
adapter = CanteenOfferItemAdapter(offers.filter { it.dateId == position }) // TODO check if empty
|
||||
|
||||
// Animate creation of new page
|
||||
scheduleLayoutAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the entire list of items
|
||||
*
|
||||
* @param items the new set of items
|
||||
*/
|
||||
fun setNewItems(items: List<CanteenOfferGroup>, daysCovered: Int) {
|
||||
// Update the count of days covered
|
||||
this.daysCovered = daysCovered
|
||||
|
||||
// Update items
|
||||
offers = items
|
||||
|
||||
// Force an update of the view. TODO replace with individual updates, as this is inefficient
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,40 @@ import com.denizk0461.studip.model.StudIPEvent
|
||||
import com.denizk0461.studip.databinding.ItemEventBinding
|
||||
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(
|
||||
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>() {
|
||||
|
||||
// Filter events to only show those of a given day
|
||||
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()
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* View holder class for parent class
|
||||
*
|
||||
* @param binding view binding object
|
||||
*/
|
||||
class EventViewHolder(val binding: ItemEventBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
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
|
||||
|
||||
// TODO inflate a view saying "no events" if none are available for a given day
|
||||
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
|
||||
// Retrieve item for current position
|
||||
val currentItem = filteredEvents[position]
|
||||
|
||||
// Set up text fields with corresponding values
|
||||
holder.binding.textTitle.text = currentItem.title
|
||||
holder.binding.textLecturers.text = currentItem.lecturer
|
||||
holder.binding.textRoom.text = currentItem.room
|
||||
|
||||
// Use parsed timeslot string as defined in StudIPEvent#timeslot()
|
||||
holder.binding.textTimeslot.text = currentItem.timeslot()
|
||||
|
||||
// Set up colours used for highlighting and un-highlighting an item.
|
||||
val colorPrimaryTranslucent = TypedValue()
|
||||
val colorCardBackground = 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.colorCardBackground, colorCardBackground, 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
|
||||
|
||||
holder.binding.cardBackground.apply {
|
||||
// Set the card's background colour to a desaturated shade of the primary colour
|
||||
backgroundTintList = ColorStateList.valueOf(colorPrimaryTranslucent.data)
|
||||
|
||||
// Hide card stroke
|
||||
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 {
|
||||
// Set the card's background colour to its default value
|
||||
backgroundTintList = ColorStateList.valueOf(colorCardBackground.data)
|
||||
|
||||
// Set the card's stroke to its default colour
|
||||
strokeColor = colorTextHintLighter.data
|
||||
}
|
||||
}
|
||||
|
||||
// Set up single click listener
|
||||
holder.binding.cardBackground.setOnClickListener {
|
||||
onClickListener.onClick(currentItem)
|
||||
}
|
||||
|
||||
// Set up long press listener
|
||||
holder.binding.cardBackground.setOnLongClickListener {
|
||||
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 =
|
||||
(calendar.get(Calendar.DAY_OF_WEEK) == this.day.toCalendarDay()) &&
|
||||
((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 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) {
|
||||
1 -> Calendar.TUESDAY
|
||||
2 -> Calendar.WEDNESDAY
|
||||
@@ -91,13 +152,36 @@ class StudIPEventItemAdapter(
|
||||
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 {
|
||||
val parts = split(":")
|
||||
return (parts[0].toInt() * 60) + parts[1].toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to evaluate clicks and long presses on a given item.
|
||||
*/
|
||||
interface OnClickListener {
|
||||
|
||||
/**
|
||||
* Executed when an item has been clicked.
|
||||
*
|
||||
* @param event item that has been clicked
|
||||
*/
|
||||
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.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)
|
||||
|
||||
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
|
||||
|
||||
// Set up page
|
||||
override fun onBindViewHolder(holder: EventPageViewHolder, position: Int) {
|
||||
|
||||
holder.binding.pageRecyclerView.apply {
|
||||
// Set page to horizontally scroll
|
||||
layoutManager = LinearLayoutManager(holder.binding.root.context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
/*
|
||||
* Create new adapter for every page. Attribute position denotes day that will be set up
|
||||
* by the newly created adapter (0 = Monday, 4 = Friday)
|
||||
*/
|
||||
adapter = StudIPEventItemAdapter(events, currentDay = position, onClickListener) // TODO check if empty
|
||||
|
||||
// Animate creation of new page
|
||||
scheduleLayoutAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the entire list of items
|
||||
*
|
||||
* @param items the new set of items
|
||||
*/
|
||||
fun setNewItems(items: List<StudIPEvent>) {
|
||||
// Update items
|
||||
events = items
|
||||
|
||||
// Force an update of the view. TODO replace with individual updates, as this is inefficient
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
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 {
|
||||
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
|
||||
|
||||
import com.denizk0461.studip.R
|
||||
|
||||
/**
|
||||
* Miscellaneous functions and variables that don't belong into any specific class.
|
||||
*/
|
||||
object Misc {
|
||||
|
||||
private val timeslotAcademicQuarter = mapOf(
|
||||
"6:00" to "6:15",
|
||||
"8:00" to "7:45",
|
||||
"8:00" to "8:15",
|
||||
"10:00" to "9:45",
|
||||
"10:00" to "10:15",
|
||||
"12:00" to "11:45",
|
||||
"12:00" to "12:15",
|
||||
"14:00" to "13:45",
|
||||
"14:00" to "14:15",
|
||||
"16:00" to "15:45",
|
||||
"16:00" to "16:15",
|
||||
"18:00" to "17:45",
|
||||
"18:00" to "18:15",
|
||||
"20:00" to "19:45",
|
||||
"20:00" to "20:15",
|
||||
"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"
|
||||
// private val timeslotAcademicQuarter = mapOf(
|
||||
// "6:00" to "6:15",
|
||||
// "8:00" to "7:45",
|
||||
// "8:00" to "8:15",
|
||||
// "10:00" to "9:45",
|
||||
// "10:00" to "10:15",
|
||||
// "12:00" to "11:45",
|
||||
// "12:00" to "12:15",
|
||||
// "14:00" to "13:45",
|
||||
// "14:00" to "14:15",
|
||||
// "16:00" to "15:45",
|
||||
// "16:00" to "16:15",
|
||||
// "18:00" to "17:45",
|
||||
// "18:00" to "18:15",
|
||||
// "20:00" to "19:45",
|
||||
// "20:00" to "20:15",
|
||||
// "22:00" to "21:45",
|
||||
// )
|
||||
}
|
||||
@@ -3,43 +3,99 @@ package com.denizk0461.studip.data
|
||||
import com.denizk0461.studip.model.StudIPEvent
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
/**
|
||||
* Parser class used for fetching and collecting scheduled events from a Stud.IP timetable.
|
||||
*/
|
||||
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) {
|
||||
// Primary key value to uniquely identify entries in the database
|
||||
var id = 0
|
||||
|
||||
// Parse the HTML using Jsoup to traverse the document
|
||||
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 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_1"),
|
||||
doc.getElementById("calendar_view_1_column_2"),
|
||||
doc.getElementById("calendar_view_1_column_3"),
|
||||
doc.getElementById("calendar_view_1_column_4"),
|
||||
)
|
||||
|
||||
// Iterate through each day
|
||||
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()
|
||||
|
||||
/*
|
||||
* Find out the index before which the title ends and after which the lecturer
|
||||
* name(s) begin.
|
||||
*/
|
||||
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)
|
||||
// val timeSlot = Misc.parseTimeslot(entryInfos[0])
|
||||
val timeSlot = entryInfos[0].split(" - ")
|
||||
// Retrieve the title of the event from the header string
|
||||
val parsedTitle = entryHeader.substring(0 until delimiter).trim()
|
||||
|
||||
// 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(
|
||||
id = id,
|
||||
title = parsedTitle,
|
||||
lecturer = parsedLecturers,
|
||||
room = entryInfos[1],
|
||||
day = index,
|
||||
timeslotStart = timeSlot[0],
|
||||
timeslotEnd = timeSlot[1],
|
||||
id = id,
|
||||
title = parsedTitle,
|
||||
lecturer = parsedLecturers,
|
||||
room = entryInfo[1],
|
||||
day = index,
|
||||
timeslotStart = timeSlot[0],
|
||||
timeslotEnd = timeSlot[1],
|
||||
)
|
||||
|
||||
// Add the new element to the temporary list
|
||||
newEvents.add(event)
|
||||
|
||||
// Raise the primary key value by 1 to avoid not fulfilling the unique constraint
|
||||
id += 1
|
||||
}
|
||||
}
|
||||
|
||||
// After fetching has finished, save the list of new items into persistent storage
|
||||
insert(newEvents)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,50 @@
|
||||
package com.denizk0461.studip.data
|
||||
|
||||
import android.util.Log
|
||||
import com.denizk0461.studip.db.EventRepository
|
||||
import com.denizk0461.studip.db.AppRepository
|
||||
import com.denizk0461.studip.model.*
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
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 {
|
||||
|
||||
// Unique primary key value for the date elements
|
||||
private var dateId = 0
|
||||
|
||||
// Unique primary key value for the canteen elements
|
||||
private var canteenId = 0
|
||||
|
||||
// Unique primary key value for the category elements
|
||||
private var categoryId = 0
|
||||
|
||||
// Unique primary key value for the item elements
|
||||
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) {
|
||||
/*
|
||||
* Reset primary key values. This needs to be done in case this method is called multiple
|
||||
* times within the app's lifespan
|
||||
*/
|
||||
dateId = 0
|
||||
canteenId = 0
|
||||
categoryId = 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>()
|
||||
if (true) { links.add(urlUniMensa) }
|
||||
// if (true) { links.add(urlCafeCentral) }
|
||||
@@ -32,66 +57,106 @@ class StwParser {
|
||||
// if (true) { links.add(urlMensaBHV) }
|
||||
// if (true) { links.add(urlCafeBHV) }
|
||||
|
||||
// Delete all previous entries and start afresh
|
||||
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 ->
|
||||
// TODO implement onRefresh(Int)
|
||||
parseFromPage(link, Dependencies.repo)
|
||||
|
||||
// TODO implement onRefresh(Int)
|
||||
}
|
||||
|
||||
// Action call once all fetching activities have finished
|
||||
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
|
||||
|
||||
// Reset the primary key value for the date objects
|
||||
dateId = 0
|
||||
|
||||
// Parse the HTML using Jsoup to traverse the document
|
||||
val doc = Jsoup.connect(url).get()
|
||||
|
||||
// Save the canteen to persistent storage
|
||||
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 ->
|
||||
|
||||
/*
|
||||
* 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]
|
||||
.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()}."
|
||||
|
||||
val d = OfferDate(dateId, date)
|
||||
Log.d("eek!8", d.toString())
|
||||
repo.insert(d)
|
||||
/*
|
||||
* Save the date to persistent storage.
|
||||
* 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 ->
|
||||
// Retrieve the category text
|
||||
val categoryTitle = category.getElementsByClass("category-name")[0].text()
|
||||
|
||||
// Save the category to persistent storage
|
||||
repo.insert(OfferCategory(categoryId, dateId, canteenId, categoryTitle))
|
||||
|
||||
// STEP: get each item in a category
|
||||
// Iterate through all items in a category
|
||||
category
|
||||
.getElementsByTag("tbody")[0]
|
||||
.getElementsByTag("tr").forEach { element ->
|
||||
// Retrieve the parent element holding the item's title and price
|
||||
val tableRows = element.getElementsByTag("td")
|
||||
|
||||
val prefs =
|
||||
element.getElementsByClass("field field-name-field-food-types")[0]
|
||||
// Retrieve the dietary preferences the item meets
|
||||
val prefs = element
|
||||
.getElementsByClass("field field-name-field-food-types")[0]
|
||||
|
||||
// Parse the preferences into a string for the database
|
||||
val prefString = DietaryPrefObject(
|
||||
isFair = prefs.isDietaryPreferenceMet(PREFERENCE_FAIR),
|
||||
isFish = prefs.isDietaryPreferenceMet(PREFERENCE_FISH),
|
||||
isPoultry = prefs.isDietaryPreferenceMet(PREFERENCE_POULTRY),
|
||||
isLamb = prefs.isDietaryPreferenceMet(PREFERENCE_LAMB),
|
||||
isVital = prefs.isDietaryPreferenceMet(PREFERENCE_VITAL),
|
||||
isBeef = prefs.isDietaryPreferenceMet(PREFERENCE_BEEF),
|
||||
isPork = prefs.isDietaryPreferenceMet(PREFERENCE_PORK),
|
||||
isVegan = prefs.isDietaryPreferenceMet(PREFERENCE_VEGAN),
|
||||
isVegetarian = prefs.isDietaryPreferenceMet(PREFERENCE_VEGETARIAN),
|
||||
isGame = prefs.isDietaryPreferenceMet(PREFERENCE_GAME),
|
||||
isFair = prefs.isDietaryPreferenceMet(imageLinkPrefFair),
|
||||
isFish = prefs.isDietaryPreferenceMet(imageLinkPrefFish),
|
||||
isPoultry = prefs.isDietaryPreferenceMet(imageLinkPrefPoultry),
|
||||
isLamb = prefs.isDietaryPreferenceMet(imageLinkPrefLamb),
|
||||
isVital = prefs.isDietaryPreferenceMet(imageLinkPrefVital),
|
||||
isBeef = prefs.isDietaryPreferenceMet(imageLinkPrefBeef),
|
||||
isPork = prefs.isDietaryPreferenceMet(imageLinkPrefPork),
|
||||
isVegan = prefs.isDietaryPreferenceMet(imageLinkPrefVegan),
|
||||
isVegetarian = prefs.isDietaryPreferenceMet(imageLinkPrefVegetarian),
|
||||
isGame = prefs.isDietaryPreferenceMet(imageLinkPrefGame),
|
||||
).deconstruct()
|
||||
|
||||
// Save the item to persistent storage
|
||||
repo.insert(
|
||||
OfferItem(
|
||||
itemId,
|
||||
@@ -101,39 +166,77 @@ class StwParser {
|
||||
prefString,
|
||||
)
|
||||
)
|
||||
|
||||
// Increment the item ID to avoid overriding
|
||||
itemId += 1
|
||||
}
|
||||
// Increment the category ID to avoid overriding
|
||||
categoryId += 1
|
||||
}
|
||||
// Increment the date ID to avoid overriding
|
||||
dateId += 1
|
||||
}
|
||||
// Increment the canteen ID to avoid overriding
|
||||
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 =
|
||||
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 {
|
||||
// Retrieve the element's inner HTML and replace faulty characters
|
||||
var text = html()
|
||||
.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>")) {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
get(index).text()
|
||||
} 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) {
|
||||
"Jan" -> "01"
|
||||
"Feb" -> "02"
|
||||
@@ -150,22 +253,24 @@ class StwParser {
|
||||
else -> "00" // shouldn't occur
|
||||
}
|
||||
|
||||
private val PREFERENCE_FAIR = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
||||
private val PREFERENCE_FISH = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
||||
private val PREFERENCE_POULTRY = "https://www.stw-bremen.de/sites/default/files/images/pictograms/geflugel.png"
|
||||
private val PREFERENCE_LAMB = "https://www.stw-bremen.de/sites/default/files/images/pictograms/lamm.png"
|
||||
private val PREFERENCE_VITAL = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vital.png"
|
||||
private val PREFERENCE_BEEF = "https://www.stw-bremen.de/sites/default/files/images/pictograms/rindfleisch.png"
|
||||
private val PREFERENCE_PORK = "https://www.stw-bremen.de/sites/default/files/images/pictograms/schwein.png"
|
||||
private val PREFERENCE_VEGAN = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vegan.png"
|
||||
private val PREFERENCE_VEGETARIAN = "https://www.stw-bremen.de/sites/default/files/images/pictograms/vegetarisch.png"
|
||||
private val PREFERENCE_GAME = "https://www.stw-bremen.de/sites/default/files/images/pictograms/wild.png"
|
||||
// Image links to all dietary preferences used for checking whether a preference is met
|
||||
private val imageLinkPrefFair = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
||||
private val imageLinkPrefFish = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
||||
private val imageLinkPrefPoultry = "https://www.stw-bremen.de/sites/default/files/images/pictograms/geflugel.png"
|
||||
private val imageLinkPrefLamb = "https://www.stw-bremen.de/sites/default/files/images/pictograms/lamm.png"
|
||||
private val imageLinkPrefVital = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vital.png"
|
||||
private val imageLinkPrefBeef = "https://www.stw-bremen.de/sites/default/files/images/pictograms/rindfleisch.png"
|
||||
private val imageLinkPrefPork = "https://www.stw-bremen.de/sites/default/files/images/pictograms/schwein.png"
|
||||
private val imageLinkPrefVegan = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vegan.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 urlCafeCentral = "https://www.stw-bremen.de/de/mensa/cafe-central"
|
||||
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 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 urlHSBWerder = "https://www.stw-bremen.de/de/mensa/werderstra%C3%9Fe"
|
||||
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 androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.denizk0461.studip.adapter.CanteenOfferPageAdapter
|
||||
import com.denizk0461.studip.databinding.FragmentCanteenBinding
|
||||
import com.denizk0461.studip.model.*
|
||||
import com.denizk0461.studip.viewmodel.CanteenViewModel
|
||||
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() {
|
||||
|
||||
// Nullable view binding reference
|
||||
private var _binding: FragmentCanteenBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and
|
||||
// onDestroyView.
|
||||
// BUG: if the fragment is changed before the refresh is finished, the app crashes with a NPE
|
||||
/*
|
||||
* Non-null reference to the view binding. This property is only valid between onCreateView and
|
||||
* onDestroyView.
|
||||
* BUG if the fragment is changed before the refresh is finished, the app crashes with a NPE
|
||||
*/
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// Adapter for the view pager displaying all offers
|
||||
private lateinit var viewPagerAdapter: CanteenOfferPageAdapter
|
||||
|
||||
// View model reference for providing access to the database
|
||||
private val viewModel: CanteenViewModel by viewModels()
|
||||
|
||||
|
||||
// Elements in the canteen plan
|
||||
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 {
|
||||
_binding = FragmentCanteenBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@@ -38,6 +53,7 @@ class CanteenFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Assign a preference value to every button to filter for dietary preferences
|
||||
val chipMap = mapOf(
|
||||
binding.chipPrefFair to DietaryPreferences.FAIR,
|
||||
binding.chipPrefFish to DietaryPreferences.FISH,
|
||||
@@ -51,15 +67,21 @@ class CanteenFragment : Fragment() {
|
||||
binding.chipPrefGame to DietaryPreferences.GAME,
|
||||
)
|
||||
|
||||
// Set up every chip
|
||||
chipMap.forEach { (chip, pref) ->
|
||||
// Set chip to be checked if user has set this preference before
|
||||
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
|
||||
* flicker. Note for the future: implement a more efficient / better-looking method.
|
||||
*
|
||||
// Set a listener for when the user clicks the chip
|
||||
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
|
||||
*/
|
||||
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
|
||||
|
||||
// Create and attach the tab mediator for the view pager
|
||||
createTabLayoutMediator()
|
||||
|
||||
// Set up LiveData observer to refresh the view on update
|
||||
viewModel.allOffers.observe(viewLifecycleOwner) { offers ->
|
||||
// Update the element list stored in this fragment
|
||||
elements = offers
|
||||
|
||||
// Group elements by category
|
||||
val groupedElements = offers.groupElements().distinct()
|
||||
|
||||
val newDates = groupedElements.map { it.date }.distinct()//.map { item -> OfferDate(item.) }
|
||||
// val newDates = viewModel.getDates()
|
||||
// Find all dates stored in the database
|
||||
val newDates = groupedElements.map { it.date }.distinct() // TODO viewModel.getDates()
|
||||
|
||||
// Update the date count stored in this fragment
|
||||
dateSize = newDates.size
|
||||
|
||||
// Update the item list in the view pager's adapter
|
||||
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
|
||||
}
|
||||
|
||||
// Set up functions for when the user swipes to refresh the view
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
// Retrieve new offers from the website(s)
|
||||
viewModel.fetchOffers(onRefreshUpdate = { status ->
|
||||
// TODO refresh updates
|
||||
}, 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()
|
||||
if (dates.isNotEmpty()) {
|
||||
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() {
|
||||
super.onDestroyView()
|
||||
_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) {
|
||||
// Update the preference
|
||||
viewModel.setPreference(pref, newValue)
|
||||
|
||||
// Update the view
|
||||
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 =
|
||||
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 {
|
||||
val prefs = viewModel.getDietaryPrefs().deconstruct()//.replace('f', '.')
|
||||
// Retrieve the user's dietary preference as a string
|
||||
val prefs = viewModel.getDietaryPrefs().deconstruct()
|
||||
|
||||
val template = ".........."
|
||||
|
||||
return if (prefs == "..........") {
|
||||
Regex(template)
|
||||
// Return the 'empty' template string if no preference has been set or found
|
||||
return if (prefs == emptyPreferenceRegex) {
|
||||
Regex(emptyPreferenceRegex)
|
||||
} else {
|
||||
// Create the variable to insert the regular expression into
|
||||
var regexString = ""
|
||||
|
||||
// Create the object to store which preferences need to be met
|
||||
val indices = mutableListOf<Int>()
|
||||
|
||||
// Find all preferences that need to be met
|
||||
prefs.forEachIndexed { index, c ->
|
||||
if (c == '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
|
||||
|
||||
// Iterate through every preference that has been set
|
||||
indices.forEach { index ->
|
||||
// Set an OR if more than one preference needs to be met
|
||||
if (!isFirst) regexString += '|'
|
||||
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
|
||||
}
|
||||
|
||||
// Compile the string to a regular expression object
|
||||
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> {
|
||||
|
||||
// Get dietary preference regex
|
||||
val prefsRegex = getPrefRegex()
|
||||
|
||||
val filteredForPreferences = if (prefsRegex.toString() == "ffffffffff") {
|
||||
// show all elements and skip filtering
|
||||
val filteredForPreferences = if (prefsRegex.toString() == emptyPreferenceRegex) {
|
||||
// Show all elements and skip filtering if no preference is set
|
||||
this
|
||||
} else {
|
||||
// Filter for dietary preferences
|
||||
this.filter {
|
||||
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(
|
||||
offer.date,
|
||||
offer.dateId,
|
||||
@@ -170,6 +258,7 @@ class CanteenFragment : Fragment() {
|
||||
filteredForPreferences.filter {
|
||||
it.category == offer.category && it.date == offer.date && it.canteen == offer.canteen
|
||||
}.map {
|
||||
// Map the individual items to their respective groups
|
||||
CanteenOfferGroupElement(
|
||||
it.title,
|
||||
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.*
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
// Nullable view binding reference
|
||||
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!!
|
||||
|
||||
// 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
|
||||
|
||||
// Titles for the view pager's tabs
|
||||
private val dayStrings = listOf(
|
||||
R.string.monday,
|
||||
R.string.tuesday,
|
||||
@@ -35,9 +47,7 @@ class EventFragment : Fragment() {
|
||||
R.string.friday,
|
||||
)
|
||||
|
||||
private lateinit var viewPagerAdapter: StudIPEventPageAdapter
|
||||
private val viewModel: EventViewModel by viewModels()
|
||||
|
||||
// Instantiate the view binding
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentEventBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@@ -46,6 +56,10 @@ class EventFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
/*
|
||||
* Determine the current day.
|
||||
* TODO expand to weekend
|
||||
*/
|
||||
dayOfWeek = when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.TUESDAY -> 1
|
||||
Calendar.WEDNESDAY -> 2
|
||||
@@ -54,42 +68,53 @@ class EventFragment : Fragment() {
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// Set up the view pager's adapter
|
||||
viewPagerAdapter = StudIPEventPageAdapter(listOf(), object : StudIPEventItemAdapter.OnClickListener {
|
||||
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
|
||||
|
||||
// Create and attach the object mediating the tabs for the view pager
|
||||
TabLayoutMediator(binding.dayTabLayout, binding.viewPager) { tab, position ->
|
||||
tab.text = getString(dayStrings[position])
|
||||
}.attach()
|
||||
|
||||
// Set up LiveData observer to refresh the view on update
|
||||
viewModel.allEvents.observe(viewLifecycleOwner) { events ->
|
||||
// Update the item list in the view pager's adapter
|
||||
viewPagerAdapter.setNewItems(events)
|
||||
|
||||
// Scroll to the current day
|
||||
switchToCurrentDayView()
|
||||
}
|
||||
|
||||
// binding.buttonFirst.setOnClickListener {
|
||||
// findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Scroll to the current day
|
||||
switchToCurrentDayView()
|
||||
}
|
||||
|
||||
// Invalidate the view binding
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the current day. Current day is determined after the fragment's view has been
|
||||
* created.
|
||||
*/
|
||||
private fun switchToCurrentDayView() {
|
||||
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.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.denizk0461.studip.BuildConfig
|
||||
import com.denizk0461.studip.activity.FetcherActivity
|
||||
import com.denizk0461.studip.data.Misc
|
||||
import com.denizk0461.studip.databinding.FragmentSettingsBinding
|
||||
|
||||
/**
|
||||
* User-facing fragment view that is used to change app settings.
|
||||
*/
|
||||
class SettingsFragment : Fragment() {
|
||||
|
||||
// Nullable view binding reference
|
||||
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!!
|
||||
|
||||
// Click counter on the app version button
|
||||
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 {
|
||||
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@@ -32,47 +41,39 @@ class SettingsFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// binding.layoutQuarter.setOnClickListener {
|
||||
// isQuarterChecked = !isQuarterChecked
|
||||
// binding.switchQuarter.isChecked = isQuarterChecked
|
||||
// }
|
||||
|
||||
// Launch the Stud.IP schedule fetcher activity
|
||||
binding.buttonRefreshSchedule.setOnClickListener {
|
||||
launchWebView()
|
||||
}
|
||||
|
||||
binding.switchQuarter.setOnClickListener {
|
||||
isQuarterChecked = !isQuarterChecked
|
||||
}
|
||||
|
||||
// Set click listener for the app version button
|
||||
binding.buttonAppVersion.setOnClickListener {
|
||||
when (appVersionClick) {
|
||||
19 -> {
|
||||
appVersionClick += 1
|
||||
}
|
||||
20 -> {
|
||||
appVersionClick += 1
|
||||
}
|
||||
21 -> {
|
||||
appVersionClick += 1
|
||||
}
|
||||
22 -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Misc.mysteryLink)))
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(mysteryLink)))
|
||||
appVersionClick = 0
|
||||
}
|
||||
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")
|
||||
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() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the Stud.IP schedule fetcher activity.
|
||||
*/
|
||||
private fun launchWebView() {
|
||||
startActivity(Intent(context, FetcherActivity::class.java))
|
||||
}
|
||||
|
||||
@@ -2,9 +2,25 @@ package com.denizk0461.studip.model
|
||||
|
||||
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(
|
||||
// @PrimaryKey val id: Int,
|
||||
val itemId: Int,
|
||||
val date: String,
|
||||
val dateId: Int,
|
||||
@@ -13,6 +29,6 @@ data class CanteenOffer(
|
||||
val canteen: String,
|
||||
val canteenId: Int,
|
||||
val title: String,
|
||||
val price: String, // price for students
|
||||
val price: String,
|
||||
@ColumnInfo(name = "dietary_preferences") val dietaryPreferences: String,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
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(
|
||||
val date: String,
|
||||
val dateId: Int,
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
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(
|
||||
val title: String,
|
||||
val price: String,
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
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(
|
||||
val isFair: Boolean,
|
||||
val isFish: Boolean,
|
||||
@@ -13,11 +28,18 @@ data class DietaryPrefObject(
|
||||
val isGame: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
private const val C_TRUE = 't'
|
||||
private const val C_FALSE = '.'
|
||||
/* assume that the values must be parsed as follows:
|
||||
* "xxxxxxxxxx"
|
||||
* t = true, . = false
|
||||
// Char used to construct a regex to denote that a preference is met
|
||||
const val C_TRUE = 't'
|
||||
// Char used to construct a regex to denote that a preference is not met
|
||||
const val C_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(
|
||||
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(
|
||||
charArrayOf(
|
||||
if (this.isFair) C_TRUE else C_FALSE,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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) {
|
||||
FAIR("isFair"),
|
||||
FISH("isFish"),
|
||||
|
||||
@@ -3,6 +3,12 @@ package com.denizk0461.studip.model
|
||||
import androidx.room.Entity
|
||||
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(
|
||||
tableName = "offer_canteen",
|
||||
)
|
||||
|
||||
@@ -4,6 +4,15 @@ import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
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(
|
||||
tableName = "offer_category",
|
||||
foreignKeys = [
|
||||
|
||||
@@ -3,6 +3,12 @@ package com.denizk0461.studip.model
|
||||
import androidx.room.Entity
|
||||
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(
|
||||
tableName = "offer_date",
|
||||
)
|
||||
|
||||
@@ -5,6 +5,16 @@ import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
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(
|
||||
tableName = "offer_item",
|
||||
foreignKeys = [
|
||||
@@ -22,14 +32,4 @@ data class OfferItem(
|
||||
val title: String,
|
||||
val price: 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.OfferDate
|
||||
|
||||
/**
|
||||
* View model for [com.denizk0461.studip.fragment.CanteenFragment]
|
||||
*
|
||||
* @param app reference to the app
|
||||
*/
|
||||
class CanteenViewModel(app: Application) : TemplateViewModel(app) {
|
||||
|
||||
// Instantiate a parser, should the user want to refresh the canteen offers
|
||||
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
|
||||
|
||||
/**
|
||||
* Retrieve the user's dietary preferences.
|
||||
*
|
||||
* @return dietary preferences
|
||||
*/
|
||||
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 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)
|
||||
}
|
||||
@@ -4,10 +4,17 @@ import android.app.Application
|
||||
import androidx.lifecycle.LiveData
|
||||
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) {
|
||||
|
||||
/**
|
||||
* Retrieves all Stud.IP events.
|
||||
*
|
||||
* @return all Stud.IP events exposed through a LiveData object
|
||||
*/
|
||||
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 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) {
|
||||
|
||||
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) } }
|
||||
|
||||
/**
|
||||
* Delete all Stud.IP events from the database asynchronously.
|
||||
*/
|
||||
fun nukeEvents() { doAsync { repo.nukeEvents() } }
|
||||
}
|
||||
@@ -4,21 +4,43 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.launch
|
||||
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) {
|
||||
|
||||
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) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
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) {
|
||||
function()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user