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"
minSdk = 24
targetSdk = 33
versionCode = 8
versionName = "1.1.0"
versionCode = 9
versionName = "1.2.0"
resourceConfigurations += arrayOf("en", "de")
@@ -11,7 +11,7 @@ import com.denizk0461.weserplaner.R
* @param context context reference
* @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) {
private val noOpFilter = object : Filter() {
@@ -3,11 +3,14 @@ package com.denizk0461.weserplaner.data
import android.app.Application
import com.denizk0461.weserplaner.db.AppRepository
import com.denizk0461.weserplaner.exception.NotLoggedInException
import com.denizk0461.weserplaner.model.FormattedDate
import com.denizk0461.weserplaner.model.StudIPEvent
import com.denizk0461.weserplaner.model.Timetable
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.io.IOException
import java.util.Date
import kotlin.jvm.Throws
/**
@@ -27,6 +30,7 @@ class StudIPParser(application: Application) {
*/
@Throws(NotLoggedInException::class, IOException::class)
fun parse(html: String): Int {
// Counts how many elements could not be successfully fetched
var elementsNotFetched = 0
@@ -38,6 +42,14 @@ class StudIPParser(application: Application) {
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
* persistent storage.
@@ -96,7 +108,7 @@ class StudIPParser(application: Application) {
// Construct the newly scraped Stud.IP event
val event = StudIPEvent(
timetableId = 0, // TODO
timetableId = timetableId,
title = parsedTitle,
lecturer = parsedLecturers,
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
repo.insertEvents(newEvents)
@@ -133,10 +142,9 @@ class StudIPParser(application: Application) {
val list = mutableListOf<Pair<Int, Element>>()
// Find headers that include day information for every schedule column
val headers = getElementById("schedule_data")
?.getElementsByTag("thead")
?.get(0)
?.getElementsByTag("td") ?: return list
val headers = getElementById("schedule_data")?.let {
it.getElementsByTag("thead")[0].getElementsByTag("td")
} ?: return list
// Iterate through all headers
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")
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.
*
@@ -78,6 +81,12 @@ interface AppDAO {
@Insert
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 --- */
/**
@@ -80,6 +80,9 @@ class AppRepository(app: Application) {
*/
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.
*
@@ -163,6 +166,10 @@ class AppRepository(app: Application) {
dao.insertTimetable(timetable)
}
fun getLargestTimetableId(): Int = dao.getLargestTimetableId() ?: 0
fun getTimetables(): LiveData<List<Timetable>> = dao.getTimetables()
/**
* Retrieves all canteen offer date objects.
*
@@ -398,6 +405,16 @@ class AppRepository(app: Application) {
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.
*/
@@ -86,7 +86,7 @@ class EventPageFragment : AppFragment<RecyclerViewBinding>(), StudIPEventItemAda
}
// 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(
// Use an event with eventId = -1 to denote that there are no events for this day
StudIPEvent(
@@ -18,6 +18,7 @@ import com.denizk0461.weserplaner.BuildConfig
import com.denizk0461.weserplaner.R
import com.denizk0461.weserplaner.activity.FetcherActivity
import com.denizk0461.weserplaner.activity.ImageActivity
import com.denizk0461.weserplaner.adapter.DropdownAdapter
import com.denizk0461.weserplaner.data.getTextSheet
import com.denizk0461.weserplaner.data.showSnackBar
import com.denizk0461.weserplaner.data.showToast
@@ -49,6 +50,8 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
// 222
private val mysteryLink = "https://www.youtube.com/watch?v=nhIQMCXJzLI"
private lateinit var timetableAdapter: DropdownAdapter
// Instantiate the view binding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
@@ -78,6 +81,29 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
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
binding.switchCurrentDay.apply {
isChecked = viewModel.preferenceCurrentDay
@@ -355,7 +381,7 @@ class SettingsFragment : AppFragment<FragmentSettingsBinding>() {
// Set build version code and time text
@SuppressLint("SetTextI18n")
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
@@ -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:
* 24.05.2023, 8:45
*
* @param context used to retrieve the string resource
* @return formatted string
*/
fun commaSeparatedString(context: Context): String = context.getString(
R.string.formatted_date_string_comma,
dateString,
timeString,
)
fun commaSeparatedString(): String = "$dateString, $timeString"
/**
* 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].
*/
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
*/
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
@@ -1,6 +1,8 @@
package com.denizk0461.weserplaner.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizk0461.weserplaner.model.Timetable
import com.denizk0461.weserplaner.values.AppLayout
/**
@@ -10,6 +12,11 @@ import com.denizk0461.weserplaner.values.AppLayout
*/
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.
*/
@@ -129,6 +129,42 @@
</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 -->
<androidx.constraintlayout.widget.ConstraintLayout
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_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_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_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_subtitle">If unselected, Monday will be shown on launch instead</string>