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