From 0d0bc1d1a24ac54ec30fae6aea27348fcd448812 Mon Sep 17 00:00:00 2001 From: denizk0461 Date: Tue, 4 Jul 2023 12:51:12 +0200 Subject: [PATCH] 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 --- app/build.gradle.kts | 4 +-- .../weserplaner/adapter/DropdownAdapter.kt | 2 +- .../weserplaner/data/StudIPParser.kt | 24 ++++++++----- .../com/denizk0461/weserplaner/db/AppDAO.kt | 9 +++++ .../weserplaner/db/AppRepository.kt | 17 +++++++++ .../weserplaner/fragment/EventPageFragment.kt | 2 +- .../weserplaner/fragment/SettingsFragment.kt | 28 ++++++++++++++- .../weserplaner/model/FormattedDate.kt | 7 +--- .../weserplaner/values/SettingsPreferences.kt | 5 +++ .../viewmodel/EventPageViewModel.kt | 4 +++ .../viewmodel/SettingsViewModel.kt | 7 ++++ app/src/main/res/layout/fragment_settings.xml | 36 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 14 files changed, 132 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e942169..ac3d042 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/com/denizk0461/weserplaner/adapter/DropdownAdapter.kt b/app/src/main/java/com/denizk0461/weserplaner/adapter/DropdownAdapter.kt index 9fc544b..7b503fc 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/adapter/DropdownAdapter.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/adapter/DropdownAdapter.kt @@ -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) +class DropdownAdapter(context: Context, items: MutableList) : ArrayAdapter(context, R.layout.item_dropdown, items) { private val noOpFilter = object : Filter() { diff --git a/app/src/main/java/com/denizk0461/weserplaner/data/StudIPParser.kt b/app/src/main/java/com/denizk0461/weserplaner/data/StudIPParser.kt index c23a3af..826d33d 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/data/StudIPParser.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/data/StudIPParser.kt @@ -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>() // 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) { diff --git a/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt b/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt index 7d068ba..2fe2371 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/db/AppDAO.kt @@ -37,6 +37,9 @@ interface AppDAO { @Query("SELECT * FROM studip_events WHERE day = :day ORDER BY timeslotId, eventId") fun getEventsForDay(day: Int): LiveData> + @Query("SELECT * FROM studip_events WHERE day = :day AND timetableId = :timetable ORDER BY timeslotId, eventId") + fun getEventsForDayAndTimetable(day: Int, timetable: Int): LiveData> + /** * 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> + /* --- event tasks --- */ /** diff --git a/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt b/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt index e5e3053..ece4d3d 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/db/AppRepository.kt @@ -80,6 +80,9 @@ class AppRepository(app: Application) { */ fun getEventsForDay(day: Int): LiveData> = dao.getEventsForDay(day) + fun getEventsForDayAndTimetable(day: Int, timetable: Int): LiveData> = + 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> = 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. */ diff --git a/app/src/main/java/com/denizk0461/weserplaner/fragment/EventPageFragment.kt b/app/src/main/java/com/denizk0461/weserplaner/fragment/EventPageFragment.kt index b273f99..7ae3ddc 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/fragment/EventPageFragment.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/fragment/EventPageFragment.kt @@ -86,7 +86,7 @@ class EventPageFragment : AppFragment(), 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( diff --git a/app/src/main/java/com/denizk0461/weserplaner/fragment/SettingsFragment.kt b/app/src/main/java/com/denizk0461/weserplaner/fragment/SettingsFragment.kt index 1f23ab5..f97a89c 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/fragment/SettingsFragment.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/fragment/SettingsFragment.kt @@ -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() { // 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() { 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() { // 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 diff --git a/app/src/main/java/com/denizk0461/weserplaner/model/FormattedDate.kt b/app/src/main/java/com/denizk0461/weserplaner/model/FormattedDate.kt index fb63e82..81d99bc 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/model/FormattedDate.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/model/FormattedDate.kt @@ -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: diff --git a/app/src/main/java/com/denizk0461/weserplaner/values/SettingsPreferences.kt b/app/src/main/java/com/denizk0461/weserplaner/values/SettingsPreferences.kt index 2b89d59..c616ff1 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/values/SettingsPreferences.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/values/SettingsPreferences.kt @@ -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"), ; } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/weserplaner/viewmodel/EventPageViewModel.kt b/app/src/main/java/com/denizk0461/weserplaner/viewmodel/EventPageViewModel.kt index 8089953..47405a5 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/viewmodel/EventPageViewModel.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/viewmodel/EventPageViewModel.kt @@ -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> = repo.getEventsForDay(day) + fun getEventsForDayAndTimetable(day: Int, timetable: Int): LiveData> = + repo.getEventsForDayAndTimetable(day, timetable) + + fun getSelectedTimetable(): Int = repo.getPreferenceSelectedTimetable() /** * This value determines whether the user wants to have the next course in their schedule diff --git a/app/src/main/java/com/denizk0461/weserplaner/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/denizk0461/weserplaner/viewmodel/SettingsViewModel.kt index d162161..fd101e9 100644 --- a/app/src/main/java/com/denizk0461/weserplaner/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/denizk0461/weserplaner/viewmodel/SettingsViewModel.kt @@ -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> = 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. */ diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index f01bc21..3816b9e 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -129,6 +129,42 @@ + + + + + + + + + + + + + Aktualisiere deinen Stud.IP-Stundenplan Dies wird alle Anpassungen löschen! + Wähle, welchen Stundenplan einzusehen + Stundenplan + Starte Stundenplan am heutigen Tag Ansonsten wird Montag beim Start angezeigt diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 29e88ea..a756b9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -251,6 +251,9 @@ Refresh your Stud.IP schedule This will clear any customisations! + Select which timetable to display + Timetable + Launch timetable on current day If unselected, Monday will be shown on launch instead