Archived
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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user