diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarController.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarController.kt index 9d419fc23..f08e69367 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarController.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarController.kt @@ -23,6 +23,7 @@ import de.tum.`in`.tumcampusapp.utils.Const import de.tum.`in`.tumcampusapp.utils.Utils import de.tum.`in`.tumcampusapp.utils.sync.SyncManager import org.joda.time.DateTime +import org.joda.time.LocalDate import java.util.* /** @@ -61,6 +62,22 @@ class CalendarController(private val context: Context) : ProvidesCard, ProvidesN fun getFromDbNotCancelledBetweenDates(begin: DateTime, end: DateTime) = applyEventColors(calendarDao.getAllNotCancelledBetweenDates(begin, end)) + fun getEventsForMonth(date: LocalDate): Map> { + val startOfMonth = date.withDayOfMonth(1) + val endOfMonth = date.withDayOfMonth(date.dayOfMonth().maximumValue) + val events = getFromDbBetweenDates(startOfMonth.toDateTimeAtCurrentTime(), endOfMonth.toDateTimeAtCurrentTime()) + val eventMap = mutableMapOf>() + for (event in events) { + val day = event.dtstart.toLocalDate().dayOfMonth.toString() + if (!eventMap.containsKey(day)) { + eventMap[day] = mutableListOf(event) + } else { + eventMap[day]?.add(event) + } + } + return eventMap + } + private fun applyEventColors(calendarItems: List): List { calendarItems.forEach { it.color = eventColorProvider.getColor(it) diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarFragment.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarFragment.kt index d7f555be0..0aa9d7d5e 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarFragment.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/CalendarFragment.kt @@ -9,13 +9,13 @@ import android.os.Bundle import de.tum.`in`.tumcampusapp.utils.ThemedAlertDialogBuilder import android.provider.CalendarContract import android.text.format.DateUtils -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View +import android.view.* +import android.widget.TextView import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat.checkSelfPermission +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.alamkanak.weekview.DateTimeInterpreter import com.alamkanak.weekview.WeekViewDisplayable import com.zhuinden.fragmentviewbindingdelegatekt.viewBinding @@ -38,6 +38,7 @@ import io.reactivex.schedulers.Schedulers import org.joda.time.DateTime import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat +import java.time.YearMonth import java.util.* class CalendarFragment : @@ -61,7 +62,27 @@ class CalendarFragment : value ?: "" } - private var isWeekMode = false + private enum class ViewMode { + DAY { + override fun numberOfVisibleDays(): Int { + return 1 + } + }, + WEEK { + override fun numberOfVisibleDays(): Int { + return 5 + } + }, + MONTH { + override fun numberOfVisibleDays(): Int { + return 30 + } + }; + + abstract fun numberOfVisibleDays(): Int + } + + private var viewMode = ViewMode.MONTH private var isFetched: Boolean = false private var menuItemSwitchView: MenuItem? = null @@ -73,6 +94,11 @@ class CalendarFragment : private val binding by viewBinding(FragmentCalendarBinding::bind) + private lateinit var monthYearText: TextView + private lateinit var monthRecyclerView: RecyclerView + private var selectedDate: LocalDate = LocalDate.now() + private lateinit var monthViewAdapter: MonthViewAdapter + override val swipeRefreshLayout get() = binding.swipeRefreshLayout override val layoutAllErrorsBinding get() = binding.layoutAllErrors @@ -108,10 +134,22 @@ class CalendarFragment : showDate?.let { openEvent(eventId) } - isWeekMode = Utils.getSettingBool(requireContext(), Const.CALENDAR_WEEK_MODE, false) + viewMode = ViewMode.valueOf(Utils.getSetting(requireContext(), Const.CALENDAR_VIEW_MODE, ViewMode.MONTH.toString())) disableRefresh() + monthYearText = binding.layoutMonth.monthYearText + monthRecyclerView = binding.layoutMonth.monthGrid + refreshMonthView() + binding.layoutMonth.monthBackButton.setOnClickListener { + selectedDate = selectedDate.minusMonths(1) + refreshMonthView() + } + binding.layoutMonth.monthForwardButton.setOnClickListener { + selectedDate = selectedDate.plusMonths(1) + refreshMonthView() + } + // Tracks whether the user has used the calendar module before. This is used in determining when to prompt for a // Google Play store review Utils.setSetting(requireContext(), Const.HAS_VISITED_CALENDAR, true) @@ -120,7 +158,7 @@ class CalendarFragment : override fun onStart() { super.onStart() refreshWeekView() - + refreshMonthView() // In case the timezone changes when reopening the calendar, while the app is still open, this ensures // that the lectures are still adjusted to the new timezone loadEvents(CacheControl.BYPASS_CACHE) @@ -197,8 +235,12 @@ class CalendarFragment : override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_switch_view_mode -> { - isWeekMode = !isWeekMode - Utils.setSetting(requireContext(), Const.CALENDAR_WEEK_MODE, isWeekMode) + viewMode = when (viewMode) { + ViewMode.DAY -> ViewMode.WEEK + ViewMode.WEEK -> ViewMode.MONTH + ViewMode.MONTH -> ViewMode.DAY + } + Utils.setSetting(requireContext(), Const.CALENDAR_VIEW_MODE, viewMode.toString()) refreshWeekView() return true } @@ -439,15 +481,25 @@ class CalendarFragment : } private fun refreshWeekView() { - setupDateTimeInterpreter(isWeekMode) - val icon: Int + setupDateTimeInterpreter(viewMode == ViewMode.WEEK) - if (isWeekMode) { - icon = R.drawable.ic_outline_calendar_view_day_24px - binding.weekView.numberOfVisibleDays = 5 - } else { - icon = R.drawable.ic_outline_view_column_24px - binding.weekView.numberOfVisibleDays = 1 + val icon = when (viewMode) { + ViewMode.DAY -> R.drawable.ic_outline_calendar_view_month_24px + ViewMode.WEEK -> R.drawable.ic_outline_calendar_view_day_24px + ViewMode.MONTH -> R.drawable.ic_outline_view_column_24px + } + + when (viewMode) { + ViewMode.DAY, ViewMode.WEEK -> { + binding.layoutWeek.visibility = View.VISIBLE + binding.layoutMonth.root.visibility = View.GONE + binding.weekView.numberOfVisibleDays = viewMode.numberOfVisibleDays() + } + + ViewMode.MONTH -> { + binding.layoutWeek.visibility = View.GONE + binding.layoutMonth.root.visibility = View.VISIBLE + } } // Go to current date or the one given in the intent @@ -514,6 +566,44 @@ class CalendarFragment : .show() } + private fun refreshMonthView() { + monthYearText.text = formatLocalDate(selectedDate) + val daysInMonth = daysInMonth(selectedDate) + + val eventMap = calendarController.getEventsForMonth(selectedDate) + + if (!::monthViewAdapter.isInitialized) { + monthViewAdapter = MonthViewAdapter(daysInMonth, eventMap) + monthRecyclerView.adapter = monthViewAdapter + } else { + monthViewAdapter.updateData(daysInMonth, eventMap) + } + + val layoutManager: RecyclerView.LayoutManager = GridLayoutManager(requireContext(), 7) + monthRecyclerView.layoutManager = layoutManager + } + + private fun daysInMonth(date: LocalDate): ArrayList { + val daysInMonthArray: ArrayList = ArrayList() + val yearMonth = YearMonth.of(date.year, date.monthOfYear) + val daysInMonth = yearMonth.lengthOfMonth() + val firstOfMonth = date.withDayOfMonth(1) + var dayOfWeek = firstOfMonth.dayOfWeek().get() - 1 // Monday is the first day of the week in Europe + for (i in 1..42) { + if (i <= dayOfWeek || i > daysInMonth + dayOfWeek) { + daysInMonthArray.add("") + } else { + daysInMonthArray.add((i - dayOfWeek).toString()) + } + } + return daysInMonthArray + } + + private fun formatLocalDate(date: LocalDate): String { + val formatter = DateTimeFormat.forPattern("MMMM yyyy") + return formatter.print(date.withDayOfMonth(1)) + } + override fun onDestroyView() { super.onDestroyView() menuItemSwitchView = null diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewAdapter.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewAdapter.kt new file mode 100644 index 000000000..753f80c9e --- /dev/null +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewAdapter.kt @@ -0,0 +1,38 @@ +package de.tum.`in`.tumcampusapp.component.tumui.calendar + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.tum.`in`.tumcampusapp.R +import de.tum.`in`.tumcampusapp.component.tumui.calendar.model.CalendarItem + +class MonthViewAdapter(private var daysOfMonth: ArrayList, private var eventMap: Map>) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MonthViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.calendar_month_view_cell, parent, false) + val layoutParams = view.layoutParams + layoutParams.height = (parent.height * 1 / 6) + return MonthViewHolder(view) + } + + override fun onBindViewHolder(holder: MonthViewHolder, position: Int) { + val events = eventMap[daysOfMonth[position]] + + holder.dayOfMonth.text = daysOfMonth[position] + holder.eventsContainer.adapter = events?.let { MonthViewEventAdapter(it) } + holder.eventsContainer.layoutManager = LinearLayoutManager(holder.itemView.context) + } + + override fun getItemCount(): Int { + return daysOfMonth.size + } + + fun updateData(daysOfMonth: ArrayList, eventMap: Map>) { + this.daysOfMonth = daysOfMonth + this.eventMap = eventMap + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewEventAdapter.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewEventAdapter.kt new file mode 100644 index 000000000..638872802 --- /dev/null +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewEventAdapter.kt @@ -0,0 +1,27 @@ +package de.tum.`in`.tumcampusapp.component.tumui.calendar + +import android.graphics.drawable.GradientDrawable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import de.tum.`in`.tumcampusapp.R +import de.tum.`in`.tumcampusapp.component.tumui.calendar.model.CalendarItem + +class MonthViewEventAdapter(private val events: List) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MonthViewEventViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.calendar_month_event_view, parent, false) + return MonthViewEventViewHolder(view) + } + + override fun onBindViewHolder(holder: MonthViewEventViewHolder, position: Int) { + val event = events[position] + holder.title.text = event.title + val background = holder.title.background as GradientDrawable + background.setColor(event.color!!) + } + + override fun getItemCount(): Int { + return events.size + } +} diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewEventViewHolder.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewEventViewHolder.kt new file mode 100644 index 000000000..99e298921 --- /dev/null +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewEventViewHolder.kt @@ -0,0 +1,10 @@ +package de.tum.`in`.tumcampusapp.component.tumui.calendar + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import de.tum.`in`.tumcampusapp.R + +class MonthViewEventViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val title: TextView = view.findViewById(R.id.eventTitle) +} \ No newline at end of file diff --git a/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewHolder.kt b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewHolder.kt new file mode 100644 index 000000000..2d7a5f6b7 --- /dev/null +++ b/app/src/main/java/de/tum/in/tumcampusapp/component/tumui/calendar/MonthViewHolder.kt @@ -0,0 +1,13 @@ +package de.tum.`in`.tumcampusapp.component.tumui.calendar + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import de.tum.`in`.tumcampusapp.R + +class MonthViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + val dayOfMonth: TextView = itemView.findViewById(R.id.cellDayNumber) + + val eventsContainer: RecyclerView = itemView.findViewById(R.id.eventsContainer) +} diff --git a/app/src/main/java/de/tum/in/tumcampusapp/utils/Const.kt b/app/src/main/java/de/tum/in/tumcampusapp/utils/Const.kt index 91d0d97aa..0dd6b75ae 100644 --- a/app/src/main/java/de/tum/in/tumcampusapp/utils/Const.kt +++ b/app/src/main/java/de/tum/in/tumcampusapp/utils/Const.kt @@ -11,7 +11,7 @@ object Const { const val CAFETERIA_DATE = "cafeteriaDate" const val CAFETERIAS = "cafeterias" const val CAFETERIA_BY_LOCATION_SETTINGS_ID = "-1" - const val CALENDAR_WEEK_MODE = "calender_week_mode" + const val CALENDAR_VIEW_MODE = "calender_view_mode" const val EVENT_BOOKED_MODE = "event_booked_mode" const val DATABASE_NAME = "tca.db" const val DATE = "date" diff --git a/app/src/main/res/drawable/grid_monthly_calendar.xml b/app/src/main/res/drawable/grid_monthly_calendar.xml new file mode 100644 index 000000000..5824046b8 --- /dev/null +++ b/app/src/main/res/drawable/grid_monthly_calendar.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml new file mode 100644 index 000000000..3d0af55bf --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_calendar_view_month_24px.xml b/app/src/main/res/drawable/ic_outline_calendar_view_month_24px.xml new file mode 100644 index 000000000..2f312aae2 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_calendar_view_month_24px.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/rounded_corners_monthly_calendar.xml b/app/src/main/res/drawable/rounded_corners_monthly_calendar.xml new file mode 100644 index 000000000..1e3f28b94 --- /dev/null +++ b/app/src/main/res/drawable/rounded_corners_monthly_calendar.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/layout/calendar_month_event_view.xml b/app/src/main/res/layout/calendar_month_event_view.xml new file mode 100644 index 000000000..f6dbad782 --- /dev/null +++ b/app/src/main/res/layout/calendar_month_event_view.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/layout/calendar_month_view.xml b/app/src/main/res/layout/calendar_month_view.xml new file mode 100644 index 000000000..3834dbc91 --- /dev/null +++ b/app/src/main/res/layout/calendar_month_view.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/calendar_month_view_cell.xml b/app/src/main/res/layout/calendar_month_view_cell.xml new file mode 100644 index 000000000..2883de267 --- /dev/null +++ b/app/src/main/res/layout/calendar_month_view_cell.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_calendar.xml b/app/src/main/res/layout/fragment_calendar.xml index 4a83e9864..31d2dcfaf 100644 --- a/app/src/main/res/layout/fragment_calendar.xml +++ b/app/src/main/res/layout/fragment_calendar.xml @@ -24,7 +24,8 @@ + android:layout_height="match_parent" + android:id="@+id/layoutWeek"> + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index eadfbb116..0f3c9568f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -49,7 +49,15 @@ Bernstein Orange Auf alle Vorkommen anwenden - + MO + DI + MI + DO + FR + SA + SO + Gehe zum vorherigen Monat + Gehe zum nächsten Monat Keine Karte verfügbar diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2633bf33..20ba77132 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,15 @@ Amber Orange Apply to all occurrences + MO + TU + WE + TH + FR + SA + SU + Go to previous month + Go to next month No map available