diff --git a/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt b/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt index 0d9029f..d8d7b24 100644 --- a/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt +++ b/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt @@ -20,7 +20,7 @@ import com.denizk0461.studip.model.CanteenOfferGroup * @param displayAllergens whether the user wants allergens to be marked */ class CanteenOfferPageAdapter( - val fragmentActivity: FragmentActivity, + fragmentActivity: FragmentActivity, private var offers: List, private var daysCovered: Int, private val onClickListener: CanteenOfferItemAdapter.OnClickListener, diff --git a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt index f71a422..d54122f 100644 --- a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt +++ b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt @@ -3,7 +3,9 @@ package com.denizk0461.studip.adapter import android.content.res.ColorStateList import android.view.LayoutInflater import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.denizk0461.studip.data.AppDiffUtilCallback import com.google.android.material.R import com.denizk0461.studip.data.getThemedColor import com.denizk0461.studip.data.parseToMinutes @@ -12,29 +14,30 @@ import com.denizk0461.studip.databinding.ItemEventBinding import java.util.* /** - * Custom RecyclerView adapter that lays out the Stud.IP events of a given day. + * Custom RecyclerView adapter that lays out the Stud.IP events for a given day. * - * @param events list of all events (still not filtered by day at this point) - * @param currentDay current day that is used to show only events of a given day (0 = Monday, + + * @param currentDay current day that is used to show only a given day's events (0 = Monday, * 4 = Friday) * @param onClickListener used for listening to clicks and long presses */ class StudIPEventItemAdapter( - events: List, private val currentDay: Int, private val onClickListener: OnClickListener, ) : RecyclerView.Adapter() { - // Filter events to only show those of a given day - private val filteredEvents: List = events.filter { it.day == currentDay } + /** + * List of all events (still not filtered by day at this point) + */ + private var events: MutableList = mutableListOf() - /* + /** * Retrieve an instance of Calendar to check whether the adapter's day matches the current day. - * TODO this can be optimised by checking in StudIPEventPageAdapter.kt and delivering a boolean + * TODO this can be optimised by checking in EventPageFragment.kt and delivering a boolean */ private val currentCalendar = Calendar.getInstance() - /* + /** * Used to disallow highlighting more than one course at a time, since only a single course * could be coming up at a given time. */ @@ -58,12 +61,11 @@ class StudIPEventItemAdapter( } // Set the amount of items to the size of the filtered list, so only events of the current day - override fun getItemCount(): Int = filteredEvents.size + override fun getItemCount(): Int = events.size - // TODO inflate a view saying "no events" if none are available for a given day override fun onBindViewHolder(holder: EventViewHolder, position: Int) { // Retrieve item for current position - val currentItem = filteredEvents[position] + val currentItem = events[position] // Set up text fields with corresponding values holder.binding.textTitle.text = currentItem.title @@ -117,6 +119,26 @@ class StudIPEventItemAdapter( } } + /** + * Updates the data and calculates the difference between the old dataset and the newly provided + * dataset. + * + * @param newData new dataset to be displayed + */ + fun setNewData(newData: List) { + // Calculate the difference between the old list and the new list + val diffResult = DiffUtil.calculateDiff(AppDiffUtilCallback(events, newData)) + + // Remove all items from the list + events.clear() + + // Add all items from the new list + events.addAll(newData) + + // Tell the DiffUtil which items have changed between the two lists + diffResult.dispatchUpdatesTo(this) + } + /** * Checks if a given course is the next course coming up. This is done by checking whether the * day of the course matches the current real world day, as well as by evaluating if the end @@ -131,7 +153,6 @@ class StudIPEventItemAdapter( (calendar.get(Calendar.DAY_OF_WEEK) == this.day.toCalendarDay()) && ((calendar.get(Calendar.MINUTE) + calendar.get(Calendar.HOUR_OF_DAY) * 60) < this.timeslotEnd.parseToMinutes()) - // Converts day as defined in StudIPEvent.kt to day from Calendar /** * Converts a numeric value as defined in StudIPEvent.kt to a calendar day. Used to convert from * American convention (where Sunday is the first day, 1 = Sunday, 2 = Monday, 7 = Saturday) to diff --git a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt index ab13c8d..aa95431 100644 --- a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt +++ b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt @@ -3,20 +3,15 @@ package com.denizk0461.studip.adapter import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter -import com.denizk0461.studip.model.StudIPEvent import com.denizk0461.studip.fragment.EventPageFragment /** * Custom ViewPager adapter for managing multiple pages of Stud.IP events. * * @param fragmentActivity parent fragment activity - * @param events all events (not filtered by day at this point) - * @param onClickListener for managing click and long press events */ class StudIPEventPageAdapter( fragmentActivity: FragmentActivity, - private var events: List, - private val onClickListener: StudIPEventItemAdapter.OnClickListener, ) : FragmentStateAdapter(fragmentActivity) { /* @@ -25,18 +20,5 @@ class StudIPEventPageAdapter( override fun getItemCount(): Int = 7 override fun createFragment(position: Int): Fragment = - EventPageFragment(events, position, onClickListener) - - /** - * Update the entire list of items - * - * @param items the new set of items - */ - fun setNewItems(items: List) { - // Update items - events = items - - // Force an update of the view. TODO replace with individual updates, as this is inefficient - notifyDataSetChanged() - } + EventPageFragment(position) } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt b/app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt new file mode 100644 index 0000000..e0c519b --- /dev/null +++ b/app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt @@ -0,0 +1,50 @@ +package com.denizk0461.studip.data + +import androidx.recyclerview.widget.DiffUtil + +/** + * Utility class used to calculate the difference between two lists to provide the adapter with + * means to perform operations only on items that have been newly added/modified/deleted. + * + * @param oldList old list + * @param newList new list + */ +class AppDiffUtilCallback( + private val oldList: List, + private val newList: List, +) : DiffUtil.Callback() { + + /** + * Get the old list's size count. + * + * @return size of the old list + */ + override fun getOldListSize(): Int = oldList.size + + /** + * Get the new list's size count. + * + * @return size of the new list + */ + override fun getNewListSize(): Int = newList.size + + /** + * Check whether two items in the lists contain items of the same class. + * + * @param oldItemPosition position of the item in the old list to be checked + * @param newItemPosition position of the item in the new list to be checked + * @return whether the two items are of the same class + */ + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + oldList[oldItemPosition].javaClass == newList[newItemPosition].javaClass + + /** + * Check whether two items in the lists have the same contents. + * + * @param oldItemPosition position of the item in the old list to be checked + * @param newItemPosition position of the item in the new list to be checked + * @return whether the two items are the same (content is equal) + */ + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + oldList[oldItemPosition].hashCode() == newList[newItemPosition].hashCode() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt b/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt index fcdd77b..3b54b69 100644 --- a/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt +++ b/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt @@ -21,6 +21,14 @@ interface AppDAO { @get:Query("SELECT * FROM studipevents ORDER BY timeslotId, id") val allEvents: LiveData> + /** + * Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs. + * + * @return Stud.IP events for a certain day exposed through a LiveData object + */ + @Query("SELECT * FROM studipevents WHERE day = :day ORDER BY timeslotId, id") + fun getEventsForDay(day: Int): LiveData> + /** * Updates a given Stud.IP element. * diff --git a/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt b/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt index 983496e..a7d88be 100644 --- a/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt +++ b/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt @@ -26,11 +26,11 @@ class AppRepository(app: Application) { private val blankDietaryPrefs: String = DietaryPreferences.C_FALSE.toString().repeat(10) /** - * Retrieves all Stud.IP events. + * Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs. * - * @return all Stud.IP events exposed through a LiveData object + * @return Stud.IP events for a certain day exposed through a LiveData object */ - val allEvents: LiveData> = dao.allEvents + fun getEventsForDay(day: Int): LiveData> = dao.getEventsForDay(day) /** * Inserts a list of Stud.IP events into the database. @@ -166,13 +166,6 @@ class AppRepository(app: Application) { */ fun nukeOfferDates() { dao.nukeOfferDates() } - /** - * Inserts a date into the database. - * - * @param date object to be saved to the database - */ - fun insert(date: OfferDate) { dao.insert(date) } - /** * Inserts a list of dates into the database. * @@ -180,13 +173,6 @@ class AppRepository(app: Application) { */ fun insertDates(dates: List) { dao.insertDates(dates) } - /** - * Inserts a canteen into the database. - * - * @param canteen object to be saved to the database - */ - fun insert(canteen: OfferCanteen) { dao.insert(canteen) } - /** * Inserts a list of canteens into the database. * @@ -194,13 +180,6 @@ class AppRepository(app: Application) { */ fun insertCanteens(canteens: List) { dao.insertCanteens(canteens) } - /** - * Inserts a canteen offer category into the database. - * - * @param category object to be saved to the database - */ - fun insert(category: OfferCategory) { dao.insert(category) } - /** * Inserts a list of categories into the database. * @@ -208,13 +187,6 @@ class AppRepository(app: Application) { */ fun insertCategories(categories: List) { dao.insertCategories(categories) } - /** - * Inserts a canteen offer item into the database. - * - * @param item object to be saved to the database - */ - fun insert(item: OfferItem) { dao.insert(item) } - /** * Inserts a list of items into the database. * diff --git a/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt index 5f06ff2..24287e1 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt @@ -1,7 +1,6 @@ package com.denizk0461.studip.fragment import android.os.Bundle -import android.os.Parcelable import android.util.Log import android.view.LayoutInflater import android.view.View @@ -9,11 +8,8 @@ import android.view.ViewGroup import androidx.fragment.app.FragmentActivity import androidx.fragment.app.viewModels import com.denizk0461.studip.R -import com.denizk0461.studip.adapter.StudIPEventItemAdapter import com.denizk0461.studip.adapter.StudIPEventPageAdapter import com.denizk0461.studip.databinding.FragmentEventBinding -import com.denizk0461.studip.model.StudIPEvent -import com.denizk0461.studip.sheet.ScheduleUpdateSheet import com.denizk0461.studip.viewmodel.EventViewModel import com.google.android.material.tabs.TabLayoutMediator import java.util.* @@ -73,24 +69,7 @@ class EventFragment : AppFragment() { } // Set up the view pager's adapter - viewPagerAdapter = - StudIPEventPageAdapter(activity as FragmentActivity, listOf(), object : StudIPEventItemAdapter.OnClickListener { - override fun onClick(event: StudIPEvent) { - // TODO implement functionality or delete - } - override fun onLongClick(event: StudIPEvent): Boolean { - // Save the current page of the ViewPager - viewPagerPosition = binding.viewPager.currentItem - - // Open a bottom sheet to edit the event - openBottomSheet(ScheduleUpdateSheet(event, onUpdate = { eventToUpdate -> - viewModel.update(eventToUpdate) - }, onDelete = { eventToDelete -> - viewModel.delete(eventToDelete) - })) - return true - } - }) + viewPagerAdapter = StudIPEventPageAdapter(activity as FragmentActivity) // Assign the adapter to the view pager binding.viewPager.adapter = viewPagerAdapter @@ -101,25 +80,25 @@ class EventFragment : AppFragment() { }.attach() // Set up LiveData observer to refresh the view on update - viewModel.allEvents.observe(viewLifecycleOwner) { events -> - - Log.d("AAAbA?", events.toString()) - - // Update the item list in the view pager's adapter - viewPagerAdapter.setNewItems(events) - - if (!hasFragmentStarted) { - // Scroll to the current day, if no page has been stored to be scrolled to - switchToCurrentDayView() - hasFragmentStarted = true -// } else { -// /* -// * If a page was previously saved, scroll to that one instead. This is meant to -// * prevent jumping from -// */ -// binding.viewPager.currentItem = viewPagerPosition - } - } +// viewModel.allEvents.observe(viewLifecycleOwner) { events -> +// +// Log.d("AAAbA?", events.toString()) +// +// // Update the item list in the view pager's adapter +// viewPagerAdapter.setNewItems(events) +// +// if (!hasFragmentStarted) { +// // Scroll to the current day, if no page has been stored to be scrolled to +// switchToCurrentDayView() +// hasFragmentStarted = true +//// } else { +//// /* +//// * If a page was previously saved, scroll to that one instead. This is meant to +//// * prevent jumping from +//// */ +//// binding.viewPager.currentItem = viewPagerPosition +// } +// } } override fun onPause() { diff --git a/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt index a0faecf..a3bd2c4 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt @@ -4,26 +4,25 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import com.denizk0461.studip.adapter.StudIPEventPageAdapter import com.denizk0461.studip.adapter.StudIPEventItemAdapter import com.denizk0461.studip.databinding.RecyclerViewBinding import com.denizk0461.studip.model.StudIPEvent +import com.denizk0461.studip.sheet.ScheduleUpdateSheet +import com.denizk0461.studip.viewmodel.EventPageViewModel /** * Fragment that is instantiated by [StudIPEventPageAdapter] to display individual days' pages and * their events. * - * @param events list of all events (still not filtered by day at this point) - * @param currentDay current day that is used to show only events of a given day (0 = Monday, - * 4 = Friday) - * @param onClickListener used for listening to clicks and long presses + * @param currentDay current day that is used to show only events of a given day (0 = Monday, + * 4 = Friday) */ class EventPageFragment( - private val events: List, private val currentDay: Int, - private val onClickListener: StudIPEventItemAdapter.OnClickListener, -) : AppFragment() { +) : AppFragment(), StudIPEventItemAdapter.OnClickListener { // Nullable view binding reference private var _binding: RecyclerViewBinding? = null @@ -34,6 +33,14 @@ class EventPageFragment( */ private val binding get() = _binding!! + // View model reference for providing access to the database + private val viewModel: EventPageViewModel by viewModels() + + /** + * Adapter that manages the page's items + */ + private val eventAdapter = StudIPEventItemAdapter(currentDay, this) + // Instantiate the view binding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = RecyclerViewBinding.inflate(inflater, container, false) @@ -49,12 +56,32 @@ class EventPageFragment( /* * Create new adapter for every page. Attribute position denotes day that will be set up - * by the newly created adapter (0 = Monday, 4 = Friday) + * by the newly created adapter (0 = Monday, 4 = Friday, 6 = Sunday) */ - adapter = StudIPEventItemAdapter(events, currentDay, onClickListener) // TODO check if empty + adapter = eventAdapter // Animate creation of new page scheduleLayoutAnimation() } + + // Set up LiveData observer to refresh the view on update + viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events -> +// eventAdapter.setNewItems(events) + eventAdapter.setNewData(events) + } + } + + override fun onClick(event: StudIPEvent) { + // TODO implement functionality or delete + } + + override fun onLongClick(event: StudIPEvent): Boolean { + // Open a bottom sheet to edit the event + openBottomSheet(ScheduleUpdateSheet(event, onUpdate = { eventToUpdate -> + viewModel.update(eventToUpdate) + }, onDelete = { eventToDelete -> + viewModel.delete(eventToDelete) + })) + return true } } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt b/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt index 31fa6a4..37c8fea 100644 --- a/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt +++ b/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt @@ -37,7 +37,7 @@ data class StudIPEvent( */ fun timeslot(): String = "$timeslotStart – $timeslotEnd" - // Auto-generated methods + // Auto-generated methods. Necessary for [AppDiffUtilCallback] override fun equals(other: Any?): Boolean { if (this === other) return true @@ -45,11 +45,19 @@ data class StudIPEvent( other as StudIPEvent + if (id != other.id) return false if (title != other.title) return false - if (!lecturer.contentEquals(other.lecturer)) return false + if (lecturer != other.lecturer) return false + if (room != other.room) return false + if (day != other.day) return false + if (timeslotStart != other.timeslotStart) return false + if (timeslotEnd != other.timeslotEnd) return false + if (timeslotId != other.timeslotId) return false + if (colour != other.colour) return false return true } + override fun hashCode(): Int { var result = id result = 31 * result + title.hashCode() @@ -58,7 +66,8 @@ data class StudIPEvent( result = 31 * result + day result = 31 * result + timeslotStart.hashCode() result = 31 * result + timeslotEnd.hashCode() - result = 31 * result + colour.hashCode() + result = 31 * result + timeslotId + result = 31 * result + colour return result } } diff --git a/app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt b/app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt new file mode 100644 index 0000000..7ad7d07 --- /dev/null +++ b/app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt @@ -0,0 +1,34 @@ +package com.denizk0461.studip.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import com.denizk0461.studip.model.StudIPEvent + +/** + * View model for [com.denizk0461.studip.fragment.EventPageFragment] + * + * @param app reference to the app + */ +class EventPageViewModel(app: Application) : AppViewModel(app) { + + /** + * Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs. + * + * @return Stud.IP events for a certain day exposed through a LiveData object + */ + fun getEventsForDay(day: Int): LiveData> = repo.getEventsForDay(day) + + /** + * Updates a schedule element. + * + * @param event the event to update + */ + fun update(event: StudIPEvent) { doAsync { repo.update(event) } } + + /** + * Deletes a schedule element. + * + * @param event the event to delete + */ + fun delete(event: StudIPEvent) { doAsync { repo.delete(event) } } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt b/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt index 5637f58..405204a 100644 --- a/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt +++ b/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt @@ -1,34 +1,10 @@ package com.denizk0461.studip.viewmodel import android.app.Application -import androidx.lifecycle.LiveData -import com.denizk0461.studip.model.StudIPEvent /** * View model for [com.denizk0461.studip.fragment.EventFragment] * * @param app reference to the app */ -class EventViewModel(app: Application) : AppViewModel(app) { - - /** - * Retrieves all Stud.IP events. - * - * @return all Stud.IP events exposed through a LiveData object - */ - val allEvents: LiveData> = repo.allEvents - - /** - * Updates a schedule element. - * - * @param event the event to update - */ - fun update(event: StudIPEvent) { doAsync { repo.update(event) } } - - /** - * Deletes a schedule element. - * - * @param event the event to delete - */ - fun delete(event: StudIPEvent) { doAsync { repo.delete(event) } } -} \ No newline at end of file +class EventViewModel(app: Application) : AppViewModel(app) \ No newline at end of file