Stud.IP events now get added to their proper timetable rather than being nuked on every refresh; users can now switch between timetables through the settings app

This commit is contained in:
denizk0461
2023-07-04 12:51:12 +02:00
parent 694d962896
commit 0d0bc1d1a2
14 changed files with 132 additions and 19 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.denizk0461.weserplaner" applicationId = "com.denizk0461.weserplaner"
minSdk = 24 minSdk = 24
targetSdk = 33 targetSdk = 33
versionCode = 8 versionCode = 9
versionName = "1.1.0" versionName = "1.2.0"
resourceConfigurations += arrayOf("en", "de") resourceConfigurations += arrayOf("en", "de")
@@ -11,7 +11,7 @@ import com.denizk0461.weserplaner.R
* @param context context reference * @param context context reference
* @param items list of texts to display * @param items list of texts to display
*/ */
class DropdownAdapter(context: Context, items: List<String>) class DropdownAdapter(context: Context, items: MutableList<String>)
: ArrayAdapter<String>(context, R.layout.item_dropdown, items) { : ArrayAdapter<String>(context, R.layout.item_dropdown, items) {
private val noOpFilter = object : Filter() { private val noOpFilter = object : Filter() {
@@ -3,11 +3,14 @@ package com.denizk0461.weserplaner.data
import android.app.Application import android.app.Application
import com.denizk0461.weserplaner.db.AppRepository import com.denizk0461.weserplaner.db.AppRepository
import com.denizk0461.weserplaner.exception.NotLoggedInException import com.denizk0461.weserplaner.exception.NotLoggedInException
import com.denizk0461.weserplaner.model.FormattedDate
import com.denizk0461.weserplaner.model.StudIPEvent import com.denizk0461.weserplaner.model.StudIPEvent
import com.denizk0461.weserplaner.model.Timetable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.io.IOException import java.io.IOException
import java.util.Date
import kotlin.jvm.Throws import kotlin.jvm.Throws
/** /**
@@ -27,6 +30,7 @@ class StudIPParser(application: Application) {
*/ */
@Throws(NotLoggedInException::class, IOException::class) @Throws(NotLoggedInException::class, IOException::class)
fun parse(html: String): Int { fun parse(html: String): Int {
// Counts how many elements could not be successfully fetched // Counts how many elements could not be successfully fetched
var elementsNotFetched = 0 var elementsNotFetched = 0
@@ -38,6 +42,14 @@ class StudIPParser(application: Application) {
throw NotLoggedInException() throw NotLoggedInException()
} }
val timetableId = repo.getLargestTimetableId() + 1
repo.insertTimetable(
Timetable(timetableId, FormattedDate(Date(System.currentTimeMillis())).commaSeparatedString())
)
repo.setPreferenceSelectedTimetable(timetableId)
/* /*
* Create the list that all events will be added to temporarily before saving them to * Create the list that all events will be added to temporarily before saving them to
* persistent storage. * persistent storage.
@@ -96,7 +108,7 @@ class StudIPParser(application: Application) {
// Construct the newly scraped Stud.IP event // Construct the newly scraped Stud.IP event
val event = StudIPEvent( val event = StudIPEvent(
timetableId = 0, // TODO timetableId = timetableId,
title = parsedTitle, title = parsedTitle,
lecturer = parsedLecturers, lecturer = parsedLecturers,
room = entryInfo.getOrQuestionMark(1), room = entryInfo.getOrQuestionMark(1),
@@ -111,9 +123,6 @@ class StudIPParser(application: Application) {
} }
} }
// Delete all previously saved events
repo.nukeEvents()
// After fetching has finished, save the list of new items into persistent storage // After fetching has finished, save the list of new items into persistent storage
repo.insertEvents(newEvents) repo.insertEvents(newEvents)
@@ -133,10 +142,9 @@ class StudIPParser(application: Application) {
val list = mutableListOf<Pair<Int, Element>>() val list = mutableListOf<Pair<Int, Element>>()
// Find headers that include day information for every schedule column // Find headers that include day information for every schedule column
val headers = getElementById("schedule_data") val headers = getElementById("schedule_data")?.let {
?.getElementsByTag("thead") it.getElementsByTag("thead")[0].getElementsByTag("td")
?.get(0) } ?: return list
?.getElementsByTag("td") ?: return list
// Iterate through all headers // Iterate through all headers
for (index in 1 until headers.size) { for (index in 1 until headers.size) {
@@ -37,6 +37,9 @@ interface AppDAO {
@Query("SELECT * FROM studip_events WHERE day = :day ORDER BY timeslotId, eventId") @Query("SELECT * FROM studip_events WHERE day = :day ORDER BY timeslotId, eventId")
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>>
@Query("SELECT * FROM studip_events WHERE day = :day AND timetableId = :timetable ORDER BY timeslotId, eventId")
fun getEventsForDayAndTimetable(day: Int, timetable: Int): LiveData<List<StudIPEvent>>
/** /**
* Updates a given Stud.IP element. * Updates a given Stud.IP element.
* *
@@ -78,6 +81,12 @@ interface AppDAO {
@Insert @Insert
fun insertTimetable(timetable: Timetable) fun insertTimetable(timetable: Timetable)
@Query("SELECT MAX(id) FROM timetables")
fun getLargestTimetableId(): Int?
@Query("SELECT * FROM timetables ORDER BY id")
fun getTimetables(): LiveData<List<Timetable>>
/* --- event tasks --- */ /* --- event tasks --- */
/** /**
@@ -80,6 +80,9 @@ class AppRepository(app: Application) {
*/ */
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> = dao.getEventsForDay(day) fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> = dao.getEventsForDay(day)
fun getEventsForDayAndTimetable(day: Int, timetable: Int): LiveData<List<StudIPEvent>> =
dao.getEventsForDayAndTimetable(day, timetable + 1) // I'm not sure why I need +1 here but it works
/** /**
* Inserts a list of Stud.IP events into the database. * Inserts a list of Stud.IP events into the database.
* *
@@ -163,6 +166,10 @@ class AppRepository(app: Application) {
dao.insertTimetable(timetable) dao.insertTimetable(timetable)
} }
fun getLargestTimetableId(): Int = dao.getLargestTimetableId() ?: 0
fun getTimetables(): LiveData<List<Timetable>> = dao.getTimetables()
/** /**
* Retrieves all canteen offer date objects. * Retrieves all canteen offer date objects.
* *
@@ -398,6 +405,16 @@ class AppRepository(app: Application) {
setPreference(SettingsPreferences.PRICING, newValue) setPreference(SettingsPreferences.PRICING, newValue)
} }
/**
* User-selected timetable to display.
*/
fun getPreferenceSelectedTimetable(): Int = getIntPreference(
SettingsPreferences.SELECTED_TIMETABLE,
)
fun setPreferenceSelectedTimetable(newValue: Int) {
setPreference(SettingsPreferences.SELECTED_TIMETABLE, newValue)
}
/** /**
* This value determines which navigation graph will be used for app navigation. * This value determines which navigation graph will be used for app navigation.
*/ */
@@ -86,7 +86,7 @@ class EventPageFragment : AppFragment<RecyclerViewBinding>(), StudIPEventItemAda
} }
// Set up LiveData observer to refresh the view on update // Set up LiveData observer to refresh the view on update
viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events -> viewModel.getEventsForDayAndTimetable(currentDay, viewModel.getSelectedTimetable()).observe(viewLifecycleOwner) { events ->
eventAdapter.setNewData(events.ifEmpty { listOf( eventAdapter.setNewData(events.ifEmpty { listOf(
// Use an event with eventId = -1 to denote that there are no events for this day // Use an event with eventId = -1 to denote that there are no events for this day
StudIPEvent( StudIPEvent(
@@ -18,6 +18,7 @@ import com.denizk0461.weserplaner.BuildConfig
import com.denizk0461.weserplaner.R import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.activity.FetcherActivity import com.denizk0461.weserplaner.activity.FetcherActivity
import com.denizk0461.weserplaner.activity.ImageActivity import com.denizk0461.weserplaner.activity.ImageActivity
import com.denizk0461.weserplaner.adapter.DropdownAdapter
import com.denizk0461.weserplaner.data.getTextSheet import com.denizk0461.weserplaner.data.getTextSheet
import com.denizk0461.weserplaner.data.showSnackBar import com.denizk0461.weserplaner.data.showSnackBar
import com.denizk0461.weserplaner.data.showToast import com.denizk0461.weserplaner.data.showToast
@@ -49,6 +50,8 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
// 222 // 222
private val mysteryLink = "https://www.youtube.com/watch?v=nhIQMCXJzLI" private val mysteryLink = "https://www.youtube.com/watch?v=nhIQMCXJzLI"
private lateinit var timetableAdapter: DropdownAdapter
// Instantiate the view binding // Instantiate the view binding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentSettingsBinding.inflate(inflater, container, false) _binding = FragmentSettingsBinding.inflate(inflater, container, false)
@@ -78,6 +81,29 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
launchWebView() launchWebView()
} }
// --- selected timetable --- //
timetableAdapter = DropdownAdapter(
context,
mutableListOf(),
)
viewModel.getTimetables().observe(viewLifecycleOwner) { timetables ->
timetableAdapter.clear()
timetableAdapter.addAll(timetables.map { it.name })
if (timetables.isNotEmpty()) {
binding.autoCompleteTimetable.setText(
timetableAdapter.getItem(viewModel.getSelectedTimetable()).toString(),
false,
)
}
}
binding.autoCompleteTimetable.setAdapter(timetableAdapter)
binding.autoCompleteTimetable.setOnItemClickListener { _, _, position, _ ->
viewModel.setSelectedTimetable(position)
binding.autoCompleteTimetable.setText(timetableAdapter.getItem(position), false)
}
// Set up switch for highlighting the next course in the schedule // Set up switch for highlighting the next course in the schedule
binding.switchCurrentDay.apply { binding.switchCurrentDay.apply {
isChecked = viewModel.preferenceCurrentDay isChecked = viewModel.preferenceCurrentDay
@@ -355,7 +381,7 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
// Set build version code and time text // Set build version code and time text
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
binding.buildTimeText.text = "v${BuildConfig.VERSION_CODE} | ${ binding.buildTimeText.text = "v${BuildConfig.VERSION_CODE} | ${
FormattedDate(BuildConfig.BUILD_TIME_MILLIS).commaSeparatedString(context) FormattedDate(BuildConfig.BUILD_TIME_MILLIS).commaSeparatedString()
}" }"
// Set up switch for showing beta screens // Set up switch for showing beta screens
@@ -24,14 +24,9 @@ class FormattedDate(val date: Date, format: DateFormat = DateFormat.ISO8601) {
* Produces a string containing date and time of the object, separated by a comma. Example: * Produces a string containing date and time of the object, separated by a comma. Example:
* 24.05.2023, 8:45 * 24.05.2023, 8:45
* *
* @param context used to retrieve the string resource
* @return formatted string * @return formatted string
*/ */
fun commaSeparatedString(context: Context): String = context.getString( fun commaSeparatedString(): String = "$dateString, $timeString"
R.string.formatted_date_string_comma,
dateString,
timeString,
)
/** /**
* Produces a string containing date and time of the object, localised to the user's locale. Example: * Produces a string containing date and time of the object, localised to the user's locale. Example:
@@ -78,5 +78,10 @@ enum class SettingsPreferences(val key: String) {
* The user's app layout preference. Also see [com.denizk0461.weserplaner.values.AppLayout]. * The user's app layout preference. Also see [com.denizk0461.weserplaner.values.AppLayout].
*/ */
APP_LAYOUT("app_layout"), APP_LAYOUT("app_layout"),
/**
* Which timetable the user has selected.
*/
SELECTED_TIMETABLE("selected_timetable"),
; ;
} }
@@ -17,6 +17,10 @@ class EventPageViewModel(app: Application) : AppViewModel(app) {
* @return Stud.IP events for a certain day exposed through a LiveData object * @return Stud.IP events for a certain day exposed through a LiveData object
*/ */
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> = repo.getEventsForDay(day) fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> = repo.getEventsForDay(day)
fun getEventsForDayAndTimetable(day: Int, timetable: Int): LiveData<List<StudIPEvent>> =
repo.getEventsForDayAndTimetable(day, timetable)
fun getSelectedTimetable(): Int = repo.getPreferenceSelectedTimetable()
/** /**
* This value determines whether the user wants to have the next course in their schedule * This value determines whether the user wants to have the next course in their schedule
@@ -1,6 +1,8 @@
package com.denizk0461.weserplaner.viewmodel package com.denizk0461.weserplaner.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.LiveData
import com.denizk0461.weserplaner.model.Timetable
import com.denizk0461.weserplaner.values.AppLayout import com.denizk0461.weserplaner.values.AppLayout
/** /**
@@ -10,6 +12,11 @@ import com.denizk0461.weserplaner.values.AppLayout
*/ */
class SettingsViewModel(app: Application) : AppViewModel(app) { class SettingsViewModel(app: Application) : AppViewModel(app) {
fun getTimetables(): LiveData<List<Timetable>> = repo.getTimetables()
fun getSelectedTimetable(): Int = repo.getPreferenceSelectedTimetable()
fun setSelectedTimetable(newValue: Int) = repo.setPreferenceSelectedTimetable(newValue)
/** /**
* This value determines whether the user wants their timetable to launch with the current day. * This value determines whether the user wants their timetable to launch with the current day.
*/ */
@@ -129,6 +129,42 @@
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
<!-- Selected timetable -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/lato_bolditalic"
android:text="@string/settings_selected_timetable_header"
android:textSize="16sp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_timetable"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_selected_timetable_hint"
app:boxStrokeColor="@color/selector_text_input"
app:hintTextColor="@color/selector_text_input">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/auto_complete_timetable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorOnSurfaceVariant"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- Highlight course --> <!-- Highlight course -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_current_day" android:id="@+id/layout_current_day"
+3
View File
@@ -246,6 +246,9 @@
<string name="refresh_schedule_title">Aktualisiere deinen Stud.IP-Stundenplan</string> <string name="refresh_schedule_title">Aktualisiere deinen Stud.IP-Stundenplan</string>
<string name="refresh_schedule_subtitle">Dies wird alle Anpassungen löschen!</string> <string name="refresh_schedule_subtitle">Dies wird alle Anpassungen löschen!</string>
<string name="settings_selected_timetable_header">Wähle, welchen Stundenplan einzusehen</string>
<string name="settings_selected_timetable_hint">Stundenplan</string>
<string name="settings_current_day_title">Starte Stundenplan am heutigen Tag</string> <string name="settings_current_day_title">Starte Stundenplan am heutigen Tag</string>
<string name="settings_current_day_subtitle">Ansonsten wird Montag beim Start angezeigt</string> <string name="settings_current_day_subtitle">Ansonsten wird Montag beim Start angezeigt</string>
+3
View File
@@ -251,6 +251,9 @@
<string name="refresh_schedule_title">Refresh your Stud.IP schedule</string> <string name="refresh_schedule_title">Refresh your Stud.IP schedule</string>
<string name="refresh_schedule_subtitle">This will clear any customisations!</string> <string name="refresh_schedule_subtitle">This will clear any customisations!</string>
<string name="settings_selected_timetable_header">Select which timetable to display</string>
<string name="settings_selected_timetable_hint">Timetable</string>
<string name="settings_current_day_title">Launch timetable on current day</string> <string name="settings_current_day_title">Launch timetable on current day</string>
<string name="settings_current_day_subtitle">If unselected, Monday will be shown on launch instead</string> <string name="settings_current_day_subtitle">If unselected, Monday will be shown on launch instead</string>