diff --git a/app/build.gradle b/app/build.gradle index 3e7e2be30..3f38a1ba8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,7 +164,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:bb08354" + implementation "io.github.wulkanowy:sdk:496dc01d15" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' @@ -182,7 +182,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.0.4" + implementation "androidx.constraintlayout:constraintlayout:2.1.0-beta02" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "com.google.android.material:material:1.4.0" implementation "com.github.wulkanowy:material-chips-input:2.2.0" @@ -209,6 +209,7 @@ dependencies { implementation "com.squareup.moshi:moshi:$moshi" implementation "com.squareup.moshi:moshi-adapters:$moshi" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" + implementation "com.jakewharton.timber:timber:4.7.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.4' @@ -216,6 +217,7 @@ dependencies { implementation "io.coil-kt:coil:1.3.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation 'me.xdrop:fuzzywuzzy:1.3.1' + implementation 'com.fredporciuncula:flow-preferences:1.5.0' playImplementation platform('com.google.firebase:firebase-bom:28.3.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' @@ -244,9 +246,9 @@ dependencies { testImplementation "com.google.dagger:hilt-android-testing:$hilt_version" kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version" - androidTestImplementation "androidx.test:core:1.3.0" - androidTestImplementation "androidx.test:runner:1.3.0" - androidTestImplementation "androidx.test.ext:junit:1.1.2" + androidTestImplementation "androidx.test:core:1.4.0" + androidTestImplementation "androidx.test:runner:1.4.0" + androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" } diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index ed82a4903..68a3d1038 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager +import com.fredporciuncula.flow.preferences.FlowSharedPreferences import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,6 +19,7 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.ExperimentalCoroutinesApi import timber.log.Timber import javax.inject.Singleton @@ -77,6 +79,12 @@ internal class RepositoryModule { fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + @OptIn(ExperimentalCoroutinesApi::class) + @Singleton + @Provides + fun provideFlowSharedPref(sharedPreferences: SharedPreferences) = + FlowSharedPreferences(sharedPreferences) + @Singleton @Provides fun provideStudentDao(database: AppDatabase) = database.studentDao diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt index 4ed9aecf5..e84bad592 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/ConferenceDao.kt @@ -4,12 +4,13 @@ import androidx.room.Dao import androidx.room.Query import io.github.wulkanowy.data.db.entities.Conference import kotlinx.coroutines.flow.Flow +import java.time.LocalDateTime import javax.inject.Singleton @Dao @Singleton interface ConferenceDao : BaseDao { - @Query("SELECT * FROM Conferences WHERE diary_id = :diaryId AND student_id = :studentId") - fun loadAll(diaryId: Int, studentId: Int): Flow> + @Query("SELECT * FROM Conferences WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :startDate") + fun loadAll(diaryId: Int, studentId: Int, startDate: LocalDateTime): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index cd4403c7d..58659914f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -25,9 +25,17 @@ class AttendanceSummaryRepository @Inject constructor( private val cacheKey = "attendance_summary" - fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource( + fun getAttendanceSummary( + student: Student, + semester: Semester, + subjectId: Int, + forceRefresh: Boolean + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, + shouldFetch = { + it.isEmpty() || forceRefresh + || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index 0d20d5a56..16d7c3c6c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -13,6 +13,9 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset import javax.inject.Inject import javax.inject.Singleton @@ -31,7 +34,8 @@ class ConferenceRepository @Inject constructor( student: Student, semester: Semester, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, + startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { @@ -39,15 +43,13 @@ class ConferenceRepository @Inject constructor( || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, query = { - conferenceDb.loadAll( - semester.diaryId, - student.studentId - ) + conferenceDb.loadAll(semester.diaryId, student.studentId, startDate) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getConferences() .mapToEntities(semester) + .filter { it.date >= startDate } }, saveFetchResult = { old, new -> val conferencesToSave = (new uniqueSubtract old).onEach { @@ -60,9 +62,12 @@ class ConferenceRepository @Inject constructor( } ) - fun getConferenceFromDatabase(semester: Semester): Flow> { - return conferenceDb.loadAll(semester.diaryId, semester.studentId) - } + fun getConferenceFromDatabase(semester: Semester): Flow> = + conferenceDb.loadAll( + diaryId = semester.diaryId, + studentId = semester.studentId, + startDate = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) + ) suspend fun updateConference(conference: List) = conferenceDb.updateAll(conference) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 426e08ffe..827f5e09c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -2,16 +2,24 @@ package io.github.wulkanowy.data.repositories import android.content.Context import android.content.SharedPreferences +import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.fredporciuncula.flow.preferences.Preference import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton +@OptIn(ExperimentalCoroutinesApi::class) @Singleton class PreferencesRepository @Inject constructor( private val sharedPref: SharedPreferences, + private val flowSharedPref: FlowSharedPreferences, @ApplicationContext val context: Context ) { val startMenuIndex: Int @@ -151,6 +159,36 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_optional_arithmetic_average ) + val selectedDashboardTilesFlow: Flow> + get() = selectedDashboardTilesPreference.asFlow() + .map { set -> + set.map { DashboardItem.Tile.valueOf(it) } + .plus(DashboardItem.Tile.ACCOUNT) + .toSet() + } + + var selectedDashboardTiles: Set + get() = selectedDashboardTilesPreference.get() + .map { DashboardItem.Tile.valueOf(it) } + .plus(DashboardItem.Tile.ACCOUNT) + .toSet() + set(value) { + val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } + .map { it.name } + .toSet() + + selectedDashboardTilesPreference.set(filteredValue) + } + + private val selectedDashboardTilesPreference: Preference> + get() { + val defaultSet = + context.resources.getStringArray(R.array.pref_default_dashboard_tiles).toSet() + val prefKey = context.getString(R.string.pref_key_dashboard_tiles) + + return flowSharedPref.getStringSet(prefKey, defaultSet) + } + private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: String, default: Int) = diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt new file mode 100644 index 000000000..e864eff70 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -0,0 +1,713 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.annotation.SuppressLint +import android.graphics.Typeface +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.db.entities.TimetableHeader +import io.github.wulkanowy.databinding.ItemDashboardAccountBinding +import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding +import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding +import io.github.wulkanowy.databinding.ItemDashboardExamsBinding +import io.github.wulkanowy.databinding.ItemDashboardGradesBinding +import io.github.wulkanowy.databinding.ItemDashboardHomeworkBinding +import io.github.wulkanowy.databinding.ItemDashboardHorizontalGroupBinding +import io.github.wulkanowy.databinding.ItemDashboardLessonsBinding +import io.github.wulkanowy.utils.createNameInitialsDrawable +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.left +import io.github.wulkanowy.utils.nickOrName +import io.github.wulkanowy.utils.toFormattedString +import java.time.Duration +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.Timer +import javax.inject.Inject +import kotlin.concurrent.timer + +class DashboardAdapter @Inject constructor() : + ListAdapter(DashboardAdapterDiffCallback()) { + + var lessonsTimer: Timer? = null + + var onAccountTileClickListener: () -> Unit = {} + + var onLuckyNumberTileClickListener: () -> Unit = {} + + var onMessageTileClickListener: () -> Unit = {} + + var onGradeTileClickListener: () -> Unit = {} + + var onAttendanceTileClickListener: () -> Unit = {} + + var onLessonsTileClickListener: () -> Unit = {} + + var onHomeworkTileClickListener: () -> Unit = {} + + var onAnnouncementsTileClickListener: () -> Unit = {} + + var onExamsTileClickListener: () -> Unit = {} + + var onConferencesTileClickListener: () -> Unit = {} + + override fun getItemViewType(position: Int) = getItem(position).type.ordinal + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + + return when (viewType) { + DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder( + ItemDashboardAccountBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder( + ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.GRADES.ordinal -> GradesViewHolder( + ItemDashboardGradesBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder( + ItemDashboardLessonsBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder( + ItemDashboardHomeworkBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder( + ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder( + ItemDashboardExamsBinding.inflate(inflater, parent, false) + ) + DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder( + ItemDashboardConferencesBinding.inflate(inflater, parent, false) + ) + else -> throw IllegalArgumentException() + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is AccountViewHolder -> bindAccountViewHolder(holder, position) + is HorizontalGroupViewHolder -> bindHorizontalGroupViewHolder(holder, position) + is GradesViewHolder -> bindGradesViewHolder(holder, position) + is LessonsViewHolder -> bindLessonsViewHolder(holder, position) + is HomeworkViewHolder -> bindHomeworkViewHolder(holder, position) + is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position) + is ExamsViewHolder -> bindExamsViewHolder(holder, position) + is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) + } + } + + fun clearTimers() { + lessonsTimer?.let { + it.cancel() + it.purge() + } + lessonsTimer = null + } + + private fun bindAccountViewHolder(accountViewHolder: AccountViewHolder, position: Int) { + val item = getItem(position) as DashboardItem.Account + val student = item.student + val isLoading = item.isLoading + + val avatar = student?.let { + accountViewHolder.binding.root.context.createNameInitialsDrawable( + text = it.nickOrName, + backgroundColor = it.avatarColor + ) + } + + with(accountViewHolder.binding) { + dashboardAccountItemContent.isVisible = !isLoading + dashboardAccountItemProgress.isVisible = isLoading + + dashboardAccountItemAvatar.setImageDrawable(avatar) + dashboardAccountItemName.text = student?.nickOrName.orEmpty() + dashboardAccountItemSchoolName.text = student?.schoolName.orEmpty() + + root.setOnClickListener { onAccountTileClickListener() } + } + } + + @SuppressLint("SetTextI18n") + private fun bindHorizontalGroupViewHolder( + horizontalGroupViewHolder: HorizontalGroupViewHolder, + position: Int + ) { + val item = getItem(position) as DashboardItem.HorizontalGroup + val unreadMessagesCount = item.unreadMessagesCount + val attendancePercentage = item.attendancePercentage + val luckyNumber = item.luckyNumber + val error = item.error + val isLoading = item.isLoading + val binding = horizontalGroupViewHolder.binding + val context = binding.root.context + val attendanceColor = when { + attendancePercentage ?: 0.0 <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { + context.getThemeAttrColor(R.attr.colorPrimary) + } + attendancePercentage ?: 0.0 <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { + context.getThemeAttrColor(R.attr.colorTimetableChange) + } + else -> context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(binding.dashboardHorizontalGroupItemAttendanceValue) { + text = "%.2f%%".format(attendancePercentage) + setTextColor(attendanceColor) + } + + with(binding) { + dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString() + dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == -1) { + context.getString(R.string.dashboard_horizontal_group_no_lukcy_number) + } else luckyNumber?.toString() + + if (dashboardHorizontalGroupItemInfoContainer.isVisible != (error != null || isLoading)) { + dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading + } + + if (dashboardHorizontalGroupItemInfoProgress.isVisible != isLoading) { + dashboardHorizontalGroupItemInfoProgress.isVisible = isLoading + } + + dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null + + with(dashboardHorizontalGroupItemLuckyContainer) { + isVisible = error == null && !isLoading && luckyNumber != null + setOnClickListener { onLuckyNumberTileClickListener() } + } + + with(dashboardHorizontalGroupItemAttendanceContainer) { + isVisible = error == null && !isLoading && attendancePercentage != null + updateLayoutParams { + matchConstraintPercentWidth = when { + luckyNumber == null && unreadMessagesCount == null -> 1.0f + luckyNumber == null || unreadMessagesCount == null -> 0.5f + else -> 0.4f + } + } + setOnClickListener { onAttendanceTileClickListener() } + } + + with(dashboardHorizontalGroupItemMessageContainer) { + isVisible = error == null && !isLoading && unreadMessagesCount != null + setOnClickListener { onMessageTileClickListener() } + } + } + } + + private fun bindGradesViewHolder(gradesViewHolder: GradesViewHolder, position: Int) { + val item = getItem(position) as DashboardItem.Grades + val subjectWithGrades = item.subjectWithGrades.orEmpty() + val gradeTheme = item.gradeTheme + val error = item.error + val isLoading = item.isLoading + val dashboardGradesAdapter = gradesViewHolder.adapter.apply { + this.items = subjectWithGrades.toList() + this.gradeTheme = gradeTheme.orEmpty() + } + + with(gradesViewHolder.binding) { + dashboardGradesItemEmpty.isVisible = + subjectWithGrades.isEmpty() && error == null && !isLoading + dashboardGradesItemError.isVisible = error != null && !isLoading + dashboardGradesItemProgress.isVisible = + isLoading && error == null && subjectWithGrades.isEmpty() + + with(dashboardGradesItemRecycler) { + adapter = dashboardGradesAdapter + layoutManager = LinearLayoutManager(context) + isVisible = subjectWithGrades.isNotEmpty() && error == null + suppressLayout(true) + } + + root.setOnClickListener { onGradeTileClickListener() } + } + } + + private fun bindLessonsViewHolder(lessonsViewHolder: LessonsViewHolder, position: Int) { + val item = getItem(position) as DashboardItem.Lessons + val timetableFull = item.lessons + val binding = lessonsViewHolder.binding + + fun updateLessonState() { + val currentDateTime = LocalDateTime.now() + val currentDate = LocalDate.now() + + val currentTimetable = timetableFull?.lessons + .orEmpty() + .filter { it.date == currentDate } + .filter { it.end.isAfter(currentDateTime) } + .filterNot { it.canceled } + val currentDayHeader = + timetableFull?.headers.orEmpty().singleOrNull { it.date == currentDate } + + val tomorrowTimetable = timetableFull?.lessons.orEmpty() + .filter { it.date == currentDate.plusDays(1) } + .filterNot { it.canceled } + val tomorrowDayHeader = + timetableFull?.headers.orEmpty().singleOrNull { it.date == currentDate.plusDays(1) } + + when { + currentTimetable.isNotEmpty() -> { + updateLessonView(item, currentTimetable, binding) + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + } + currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { + updateLessonView(item, emptyList(), binding, currentDayHeader) + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + } + tomorrowTimetable.isNotEmpty() -> { + updateLessonView(item, tomorrowTimetable, binding) + binding.dashboardLessonsItemTitleTomorrow.isVisible = true + } + tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { + updateLessonView(item, emptyList(), binding, tomorrowDayHeader) + binding.dashboardLessonsItemTitleTomorrow.isVisible = true + } + else -> { + updateLessonView(item, emptyList(), binding) + binding.dashboardLessonsItemTitleTomorrow.isVisible = + !(item.isLoading && item.error == null) + } + } + } + + updateLessonState() + + lessonsTimer?.cancel() + lessonsTimer = timer(period = 1000) { + Handler(Looper.getMainLooper()).post { updateLessonState() } + } + + binding.root.setOnClickListener { onLessonsTileClickListener() } + } + + private fun updateLessonView( + item: DashboardItem.Lessons, + timetableToShow: List, + binding: ItemDashboardLessonsBinding, + header: TimetableHeader? = null, + ) { + val currentDateTime = LocalDateTime.now() + val nextLessons = timetableToShow.filter { it.end.isAfter(currentDateTime) } + .sortedBy { it.start } + + with(binding) { + dashboardLessonsItemEmpty.isVisible = + (timetableToShow.isEmpty() || nextLessons.isEmpty()) && item.error == null && header == null && !item.isLoading + dashboardLessonsItemError.isVisible = item.error != null && !item.isLoading + dashboardLessonsItemProgress.isVisible = + item.isLoading && (timetableToShow.isEmpty() || nextLessons.isEmpty()) && item.error == null && header == null + + val secondLesson = nextLessons.getOrNull(1) + val firstLesson = nextLessons.getOrNull(0) + + updateFirstLessonView(binding, firstLesson, currentDateTime) + updateSecondLesson(binding, firstLesson, secondLesson) + updateLessonSummary(binding, nextLessons) + updateLessonHeader(binding, header) + } + } + + private fun updateFirstLessonView( + binding: ItemDashboardLessonsBinding, + firstLesson: Timetable?, + currentDateTime: LocalDateTime + ) { + val context = binding.root.context + val sansSerifFont = Typeface.create("sans-serif", Typeface.NORMAL) + val sansSerifMediumFont = Typeface.create("sans-serif-medium", Typeface.NORMAL) + + with(binding) { + dashboardLessonsItemFirstTitle.isVisible = firstLesson != null + dashboardLessonsItemFirstTime.isVisible = firstLesson != null + dashboardLessonsItemFirstTimeRange.isVisible = firstLesson != null + dashboardLessonsItemFirstValue.isVisible = firstLesson != null + } + + firstLesson ?: return + + val minutesToStartLesson = + Duration.between(currentDateTime, firstLesson.start).toMinutes() + val isFirstTimeVisible: Boolean + val isFirstTimeRangeVisible: Boolean + val firstTimeText: String + val firstTimeRangeText: String + val firstTitleText: String + val firstTitleAndValueTextColor: Int + val firstTitleAndValueTextFont: Typeface + + if (currentDateTime.isBefore(firstLesson.start)) { + if (minutesToStartLesson > 60) { + val formattedStartTime = firstLesson.start.toFormattedString("HH:mm") + val formattedEndTime = firstLesson.end.toFormattedString("HH:mm") + + firstTimeRangeText = "${formattedStartTime}-${formattedEndTime}" + firstTimeText = "" + + isFirstTimeRangeVisible = true + isFirstTimeVisible = false + } else { + firstTimeText = context.resources.getQuantityString( + R.plurals.dashboard_timetable_first_lesson_time_in_minutes, + minutesToStartLesson.toInt(), + minutesToStartLesson + ) + firstTimeRangeText = "" + + isFirstTimeRangeVisible = false + isFirstTimeVisible = true + } + + when { + minutesToStartLesson < 60 -> { + firstTitleAndValueTextColor = context.getThemeAttrColor(R.attr.colorPrimary) + firstTitleAndValueTextFont = sansSerifMediumFont + firstTitleText = + context.getString(R.string.dashboard_timetable_first_lesson_title_moment) + } + minutesToStartLesson < 240 -> { + firstTitleAndValueTextColor = + context.getThemeAttrColor(R.attr.colorOnSurface) + firstTitleAndValueTextFont = sansSerifFont + firstTitleText = + context.getString(R.string.dashboard_timetable_first_lesson_title_soon) + } + else -> { + firstTitleAndValueTextColor = + context.getThemeAttrColor(R.attr.colorOnSurface) + firstTitleAndValueTextFont = sansSerifFont + firstTitleText = + context.getString(R.string.dashboard_timetable_first_lesson_title_first) + } + } + } else { + val minutesToEndLesson = firstLesson.left!!.toMinutes() + + firstTimeText = context.resources.getQuantityString( + R.plurals.dashboard_timetable_first_lesson_time_more_minutes, + minutesToEndLesson.toInt(), + minutesToEndLesson + ) + firstTimeRangeText = "" + + isFirstTimeRangeVisible = false + isFirstTimeVisible = true + + firstTitleAndValueTextColor = context.getThemeAttrColor(R.attr.colorPrimary) + firstTitleAndValueTextFont = sansSerifMediumFont + firstTitleText = context.getString(R.string.dashboard_timetable_first_lesson_title_now) + } + + with(binding.dashboardLessonsItemFirstTime) { + isVisible = isFirstTimeVisible + text = firstTimeText + } + with(binding.dashboardLessonsItemFirstTimeRange) { + isVisible = isFirstTimeRangeVisible + text = firstTimeRangeText + } + with(binding.dashboardLessonsItemFirstTitle) { + setTextColor(firstTitleAndValueTextColor) + typeface = firstTitleAndValueTextFont + text = firstTitleText + } + with(binding.dashboardLessonsItemFirstValue) { + setTextColor(firstTitleAndValueTextColor) + typeface = firstTitleAndValueTextFont + text = context.getString( + R.string.dashboard_timetable_lesson_value, + firstLesson.subject, + firstLesson.room + ) + } + } + + private fun updateSecondLesson( + binding: ItemDashboardLessonsBinding, + firstLesson: Timetable?, + secondLesson: Timetable? + ) { + val context = binding.root.context + + val formattedStartTime = secondLesson?.start?.toFormattedString("HH:mm") + val formattedEndTime = secondLesson?.end?.toFormattedString("HH:mm") + + val secondTimeText = "${formattedStartTime}-${formattedEndTime}" + val secondValueText = if (secondLesson != null) { + context.getString( + R.string.dashboard_timetable_lesson_value, + secondLesson.subject, + secondLesson.room + ) + } else { + context.getString(R.string.dashboard_timetable_second_lesson_value_end) + } + + with(binding.dashboardLessonsItemSecondTime) { + isVisible = secondLesson != null + text = secondTimeText + } + with(binding.dashboardLessonsItemSecondValue) { + isVisible = !(secondLesson == null && firstLesson == null) + text = secondValueText + } + binding.dashboardLessonsItemSecondTitle.isVisible = + !(secondLesson == null && firstLesson == null) + } + + private fun updateLessonSummary( + binding: ItemDashboardLessonsBinding, + nextLessons: List + ) { + val context = binding.root.context + val formattedEndTime = nextLessons.lastOrNull()?.end?.toFormattedString("HH:mm") + + with(binding) { + dashboardLessonsItemThirdTime.isVisible = + nextLessons.size > LESSON_SUMMARY_VISIBILITY_THRESHOLD + dashboardLessonsItemThirdTitle.isVisible = + nextLessons.size > LESSON_SUMMARY_VISIBILITY_THRESHOLD + dashboardLessonsItemThirdValue.isVisible = + nextLessons.size > LESSON_SUMMARY_VISIBILITY_THRESHOLD + dashboardLessonsItemDivider.isVisible = + nextLessons.size > LESSON_SUMMARY_VISIBILITY_THRESHOLD + + dashboardLessonsItemThirdValue.text = context.resources.getQuantityString( + R.plurals.dashboard_timetable_third_value, + nextLessons.size - LESSON_SUMMARY_VISIBILITY_THRESHOLD, + nextLessons.size - LESSON_SUMMARY_VISIBILITY_THRESHOLD + ) + dashboardLessonsItemThirdTime.text = + context.getString(R.string.dashboard_timetable_third_time, formattedEndTime) + } + } + + private fun updateLessonHeader( + binding: ItemDashboardLessonsBinding, + header: TimetableHeader? + ) { + with(binding.dashboardLessonsItemDayHeader) { + isVisible = header != null + text = header?.content + } + } + + private fun bindHomeworkViewHolder(homeworkViewHolder: HomeworkViewHolder, position: Int) { + val item = getItem(position) as DashboardItem.Homework + val homeworkList = item.homework.orEmpty() + val error = item.error + val isLoading = item.isLoading + val context = homeworkViewHolder.binding.root.context + val homeworkAdapter = homeworkViewHolder.adapter.apply { + this.items = homeworkList.take(MAX_VISIBLE_LIST_ITEMS) + } + + with(homeworkViewHolder.binding) { + dashboardHomeworkItemEmpty.isVisible = + homeworkList.isEmpty() && error == null && !isLoading + dashboardHomeworkItemError.isVisible = error != null && !isLoading + dashboardHomeworkItemProgress.isVisible = + isLoading && error == null && homeworkList.isEmpty() + dashboardHomeworkItemDivider.isVisible = homeworkList.size > MAX_VISIBLE_LIST_ITEMS + dashboardHomeworkItemMore.isVisible = homeworkList.size > MAX_VISIBLE_LIST_ITEMS + dashboardHomeworkItemMore.text = context.resources.getQuantityString( + R.plurals.dashboard_homework_more, + homeworkList.size - MAX_VISIBLE_LIST_ITEMS, + homeworkList.size - MAX_VISIBLE_LIST_ITEMS + ) + + with(dashboardHomeworkItemRecycler) { + adapter = homeworkAdapter + layoutManager = LinearLayoutManager(context) + isVisible = homeworkList.isNotEmpty() && error == null + suppressLayout(true) + } + + root.setOnClickListener { onHomeworkTileClickListener() } + } + } + + private fun bindAnnouncementsViewHolder( + announcementsViewHolder: AnnouncementsViewHolder, + position: Int + ) { + val item = getItem(position) as DashboardItem.Announcements + val schoolAnnouncementList = item.announcement.orEmpty() + val error = item.error + val isLoading = item.isLoading + val context = announcementsViewHolder.binding.root.context + val schoolAnnouncementsAdapter = announcementsViewHolder.adapter.apply { + this.items = schoolAnnouncementList.take(MAX_VISIBLE_LIST_ITEMS) + } + + with(announcementsViewHolder.binding) { + dashboardAnnouncementsItemEmpty.isVisible = + schoolAnnouncementList.isEmpty() && error == null && !isLoading + dashboardAnnouncementsItemError.isVisible = error != null && !isLoading + dashboardAnnouncementsItemProgress.isVisible = + isLoading && error == null && schoolAnnouncementList.isEmpty() + dashboardAnnouncementsItemDivider.isVisible = + schoolAnnouncementList.size > MAX_VISIBLE_LIST_ITEMS + dashboardAnnouncementsItemMore.isVisible = + schoolAnnouncementList.size > MAX_VISIBLE_LIST_ITEMS + dashboardAnnouncementsItemMore.text = context.resources.getQuantityString( + R.plurals.dashboard_announcements_more, + schoolAnnouncementList.size - MAX_VISIBLE_LIST_ITEMS, + schoolAnnouncementList.size - MAX_VISIBLE_LIST_ITEMS + ) + + with(dashboardAnnouncementsItemRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = schoolAnnouncementsAdapter + isVisible = schoolAnnouncementList.isNotEmpty() && error == null + suppressLayout(true) + } + + root.setOnClickListener { onAnnouncementsTileClickListener() } + } + } + + private fun bindExamsViewHolder(examsViewHolder: ExamsViewHolder, position: Int) { + val item = getItem(position) as DashboardItem.Exams + val exams = item.exams.orEmpty() + val error = item.error + val isLoading = item.isLoading + val context = examsViewHolder.binding.root.context + val examAdapter = examsViewHolder.adapter.apply { + this.items = exams.take(MAX_VISIBLE_LIST_ITEMS) + } + + with(examsViewHolder.binding) { + dashboardExamsItemEmpty.isVisible = exams.isEmpty() && error == null && !isLoading + dashboardExamsItemError.isVisible = error != null && !isLoading + dashboardExamsItemProgress.isVisible = isLoading && error == null && exams.isEmpty() + dashboardExamsItemDivider.isVisible = exams.size > MAX_VISIBLE_LIST_ITEMS + dashboardExamsItemMore.isVisible = exams.size > MAX_VISIBLE_LIST_ITEMS + dashboardExamsItemMore.text = context.resources.getQuantityString( + R.plurals.dashboard_exams_more, + exams.size - MAX_VISIBLE_LIST_ITEMS, + exams.size - MAX_VISIBLE_LIST_ITEMS + ) + + with(dashboardExamsItemRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = examAdapter + isVisible = exams.isNotEmpty() && error == null + suppressLayout(true) + } + + root.setOnClickListener { onExamsTileClickListener() } + } + } + + private fun bindConferencesViewHolder( + conferencesViewHolder: ConferencesViewHolder, + position: Int + ) { + val item = getItem(position) as DashboardItem.Conferences + val conferences = item.conferences.orEmpty() + val error = item.error + val isLoading = item.isLoading + val context = conferencesViewHolder.binding.root.context + val conferenceAdapter = conferencesViewHolder.adapter.apply { + this.items = conferences.take(MAX_VISIBLE_LIST_ITEMS) + } + + with(conferencesViewHolder.binding) { + dashboardConferencesItemEmpty.isVisible = + conferences.isEmpty() && error == null && !isLoading + dashboardConferencesItemError.isVisible = error != null && !isLoading + dashboardConferencesItemProgress.isVisible = + isLoading && error == null && conferences.isEmpty() + dashboardConferencesItemDivider.isVisible = conferences.size > MAX_VISIBLE_LIST_ITEMS + dashboardConferencesItemMore.isVisible = conferences.size > MAX_VISIBLE_LIST_ITEMS + dashboardConferencesItemMore.text = context.resources.getQuantityString( + R.plurals.dashboard_conference_more, + conferences.size - MAX_VISIBLE_LIST_ITEMS, + conferences.size - MAX_VISIBLE_LIST_ITEMS + ) + + with(dashboardConferencesItemRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = conferenceAdapter + isVisible = conferences.isNotEmpty() && error == null + suppressLayout(true) + } + + root.setOnClickListener { onConferencesTileClickListener() } + } + } + + class AccountViewHolder(val binding: ItemDashboardAccountBinding) : + RecyclerView.ViewHolder(binding.root) + + class HorizontalGroupViewHolder(val binding: ItemDashboardHorizontalGroupBinding) : + RecyclerView.ViewHolder(binding.root) + + class GradesViewHolder(val binding: ItemDashboardGradesBinding) : + RecyclerView.ViewHolder(binding.root) { + + val adapter by lazy { DashboardGradesAdapter() } + } + + class LessonsViewHolder(val binding: ItemDashboardLessonsBinding) : + RecyclerView.ViewHolder(binding.root) + + class HomeworkViewHolder(val binding: ItemDashboardHomeworkBinding) : + RecyclerView.ViewHolder(binding.root) { + + val adapter by lazy { DashboardHomeworkAdapter() } + } + + class AnnouncementsViewHolder(val binding: ItemDashboardAnnouncementsBinding) : + RecyclerView.ViewHolder(binding.root) { + + val adapter by lazy { DashboardAnnouncementsAdapter() } + } + + class ExamsViewHolder(val binding: ItemDashboardExamsBinding) : + RecyclerView.ViewHolder(binding.root) { + + val adapter by lazy { DashboardExamsAdapter() } + } + + class ConferencesViewHolder(val binding: ItemDashboardConferencesBinding) : + RecyclerView.ViewHolder(binding.root) { + + val adapter by lazy { DashboardConferencesAdapter() } + } + + class DashboardAdapterDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: DashboardItem, newItem: DashboardItem) = + oldItem.type == newItem.type + + override fun areContentsTheSame(oldItem: DashboardItem, newItem: DashboardItem) = + oldItem == newItem + } + + private companion object { + + private const val LESSON_SUMMARY_VISIBILITY_THRESHOLD = 2 + + private const val MAX_VISIBLE_LIST_ITEMS = 5 + + private const val ATTENDANCE_FIRST_WARNING_THRESHOLD = 75.0 + + private const val ATTENDANCE_SECOND_WARNING_THRESHOLD = 50.0 + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAnnouncementsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAnnouncementsAdapter.kt new file mode 100644 index 000000000..7a4c2b257 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAnnouncementsAdapter.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement +import io.github.wulkanowy.databinding.SubitemDashboardAnnouncementsBinding +import io.github.wulkanowy.utils.toFormattedString + +class DashboardAnnouncementsAdapter : + RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + SubitemDashboardAnnouncementsBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + dashboardHomeworkSubitemTime.text = item.date.toFormattedString() + dashboardHomeworkSubitemTitle.text = item.subject + } + } + + class ViewHolder(val binding: SubitemDashboardAnnouncementsBinding) : + RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardConferencesAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardConferencesAdapter.kt new file mode 100644 index 000000000..64cf599c8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardConferencesAdapter.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.databinding.SubitemDashboardConferencesBinding +import io.github.wulkanowy.utils.toFormattedString + +class DashboardConferencesAdapter : + RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + SubitemDashboardConferencesBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + + with(holder.binding) { + dashboardHomeworkSubitemTime.text = item.date.toFormattedString("HH:mm dd.MM.yyyy") + dashboardHomeworkSubitemTitle.text = item.title + } + } + + class ViewHolder(val binding: SubitemDashboardConferencesBinding) : + RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardExamsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardExamsAdapter.kt new file mode 100644 index 000000000..060f224b3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardExamsAdapter.kt @@ -0,0 +1,59 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.databinding.SubitemDashboardExamsBinding +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate + +class DashboardExamsAdapter : + RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + SubitemDashboardExamsBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val context = holder.binding.root.context + val primaryWarningTextColor = context.getThemeAttrColor( + if (item.date == LocalDate.now()) { + R.attr.colorPrimary + } else { + android.R.attr.textColorPrimary + } + ) + val secondaryWarningTextColor = context.getThemeAttrColor( + if (item.date == LocalDate.now()) { + R.attr.colorPrimary + } else { + android.R.attr.textColorSecondary + } + ) + + with(holder.binding) { + dashboardHomeworkSubitemTime.text = item.date.toFormattedString("dd.MM") + dashboardHomeworkSubitemTime.setTextColor(secondaryWarningTextColor) + + dashboardHomeworkSubitemTitle.text = "${item.type} - ${item.subject}" + dashboardHomeworkSubitemTitle.setTextColor(primaryWarningTextColor) + } + } + + class ViewHolder(val binding: SubitemDashboardExamsBinding) : + RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt new file mode 100644 index 000000000..54d3f40fc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -0,0 +1,180 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentDashboardBinding +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.account.AccountFragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint +class DashboardFragment : BaseFragment(R.layout.fragment_dashboard), + DashboardView, MainView.TitledView, MainView.MainChildView { + + @Inject + lateinit var presenter: DashboardPresenter + + @Inject + lateinit var dashboardAdapter: DashboardAdapter + + override val titleStringId get() = R.string.dashboard_title + + override var subtitleString = + LocalDate.now().toFormattedString("EEEE, d MMMM yyyy").capitalise() + + companion object { + + fun newInstance() = DashboardFragment() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentDashboardBinding.bind(view) + presenter.onAttachView(this) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.action_menu_dashboard, menu) + } + + override fun initView() { + val mainActivity = requireActivity() as MainActivity + + dashboardAdapter.apply { + onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) } + onLuckyNumberTileClickListener = { + mainActivity.pushView(LuckyNumberFragment.newInstance()) + } + onMessageTileClickListener = { mainActivity.pushView(MessageFragment.newInstance()) } + onAttendanceTileClickListener = { + mainActivity.pushView(AttendanceFragment.newInstance()) + } + onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) } + onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) } + onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) } + onAnnouncementsTileClickListener = { + mainActivity.pushView(SchoolAnnouncementFragment.newInstance()) + } + onExamsTileClickListener = { mainActivity.pushView(ExamFragment.newInstance()) } + onConferencesTileClickListener = { + mainActivity.pushView(ConferenceFragment.newInstance()) + } + } + + with(binding) { + dashboardErrorRetry.setOnClickListener { presenter.onRetry() } + dashboardErrorDetails.setOnClickListener { presenter.onDetailsClick() } + dashboardSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + dashboardSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) + dashboardSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) + ) + + with(dashboardRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = dashboardAdapter + (itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected() + else -> false + } + } + + override fun showDashboardTileSettings(selectedItems: List) { + val entries = requireContext().resources.getStringArray(R.array.dashboard_tile_entries) + val values = requireContext().resources.getStringArray(R.array.dashboard_tile_values) + val selectedItemsState = values.map { value -> selectedItems.any { it.name == value } } + + AlertDialog.Builder(requireContext()) + .setTitle(R.string.pref_dashboard_appearance_tiles_title) + .setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> } + .setPositiveButton(android.R.string.ok) { dialog, _ -> + val selectedState = (dialog as AlertDialog).listView.checkedItemPositions + val selectedValues = values.filterIndexed { index, _ -> selectedState[index] } + + presenter.onDashboardTileSettingSelected(selectedValues) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + + override fun updateData(data: List) { + dashboardAdapter.submitList(data.toMutableList()) + } + + override fun showMessage(text: String) { + //Empty function to avoid message flood + } + + override fun showRefresh(show: Boolean) { + binding.dashboardSwipe.isRefreshing = show + } + + override fun showProgress(show: Boolean) { + binding.dashboardProgress.isVisible = show + } + + override fun showContent(show: Boolean) { + binding.dashboardRecycler.isVisible = show + } + + override fun showErrorView(show: Boolean) { + binding.dashboardErrorContainer.isVisible = show + } + + override fun setErrorDetails(message: String) { + binding.dashboardErrorMessage.text = message + } + + override fun resetView() { + binding.dashboardRecycler.smoothScrollToPosition(0) + } + + override fun popViewToRoot() { + (requireActivity() as MainActivity).popView(20) + } + + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun onDestroyView() { + dashboardAdapter.clearTimers() + presenter.onDetachView() + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardGradesAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardGradesAdapter.kt new file mode 100644 index 000000000..aeecf5bfe --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardGradesAdapter.kt @@ -0,0 +1,49 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.databinding.SubitemDashboardGradesBinding +import io.github.wulkanowy.databinding.SubitemDashboardSmallGradeBinding +import io.github.wulkanowy.utils.getBackgroundColor + +class DashboardGradesAdapter : RecyclerView.Adapter() { + + var items = listOf>>() + + var gradeTheme = "" + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + SubitemDashboardGradesBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val (subject, grades) = items[position] + val context = holder.binding.root.context + + with(holder.binding) { + dashboardGradesSubitemTitle.text = subject + + grades.forEach { + val subitemBinding = SubitemDashboardSmallGradeBinding.inflate( + LayoutInflater.from(context), + dashboardGradesSubitemGradeContainer, + false + ) + + with(subitemBinding.dashboardSmallGradeSubitemValue) { + text = it.entry + setBackgroundResource(it.getBackgroundColor(gradeTheme)) + } + + dashboardGradesSubitemGradeContainer.addView(subitemBinding.root) + } + } + } + + class ViewHolder(val binding: SubitemDashboardGradesBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardHomeworkAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardHomeworkAdapter.kt new file mode 100644 index 000000000..55ec90294 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardHomeworkAdapter.kt @@ -0,0 +1,56 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.databinding.SubitemDashboardHomeworkBinding +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate + +class DashboardHomeworkAdapter : RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + SubitemDashboardHomeworkBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val context = holder.binding.root.context + val formattedDate = item.date.toFormattedString("dd.MM") + val primaryWarningTextColor = context.getThemeAttrColor( + if (item.date == LocalDate.now()) { + R.attr.colorPrimary + } else { + android.R.attr.textColorPrimary + } + ) + val secondaryWarningTextColor = context.getThemeAttrColor( + if (item.date == LocalDate.now()) { + R.attr.colorPrimary + } else { + android.R.attr.textColorSecondary + } + ) + + with(holder.binding) { + dashboardHomeworkSubitemTitle.text = "${item.subject} - ${item.content}" + dashboardHomeworkSubitemTitle.setTextColor(primaryWarningTextColor) + + dashboardHomeworkSubitemTime.text = + context.getString(R.string.dashboard_homework_time, formattedDate) + dashboardHomeworkSubitemTime.setTextColor(secondaryWarningTextColor) + } + } + + class ViewHolder(val binding: SubitemDashboardHomeworkBinding) : + RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt new file mode 100644 index 000000000..2948b42fa --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -0,0 +1,134 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.TimetableFull +import io.github.wulkanowy.data.db.entities.Homework as EntitiesHomework + +sealed class DashboardItem(val type: Type) { + + abstract val error: Throwable? + + abstract val isLoading: Boolean + + abstract val isDataLoaded: Boolean + + data class Account( + val student: Student? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ACCOUNT) { + + override val isDataLoaded get() = student != null + } + + data class HorizontalGroup( + val unreadMessagesCount: Int? = null, + val attendancePercentage: Double? = null, + val luckyNumber: Int? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.HORIZONTAL_GROUP) { + + override val isDataLoaded + get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null + } + + data class Grades( + val subjectWithGrades: Map>? = null, + val gradeTheme: String? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.GRADES) { + + override val isDataLoaded get() = subjectWithGrades != null + } + + data class Lessons( + val lessons: TimetableFull? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.LESSONS) { + + override val isDataLoaded get() = lessons != null + } + + data class Homework( + val homework: List? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.HOMEWORK) { + + override val isDataLoaded get() = homework != null + } + + data class Announcements( + val announcement: List? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ANNOUNCEMENTS) { + + override val isDataLoaded get() = announcement != null + } + + data class Exams( + val exams: List? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.EXAMS) { + + override val isDataLoaded get() = exams != null + } + + data class Conferences( + val conferences: List? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.CONFERENCES) { + + override val isDataLoaded get() = conferences != null + } + + enum class Type { + ACCOUNT, + HORIZONTAL_GROUP, + LESSONS, + GRADES, + HOMEWORK, + ANNOUNCEMENTS, + EXAMS, + CONFERENCES, + ADS + } + + enum class Tile { + ACCOUNT, + LUCKY_NUMBER, + MESSAGES, + ATTENDANCE, + LESSONS, + GRADES, + HOMEWORK, + ANNOUNCEMENTS, + EXAMS, + CONFERENCES, + ADS + } +} + +fun DashboardItem.Tile.toDashboardItemType() = when (this) { + DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT + DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP + DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP + DashboardItem.Tile.ATTENDANCE -> DashboardItem.Type.HORIZONTAL_GROUP + DashboardItem.Tile.LESSONS -> DashboardItem.Type.LESSONS + DashboardItem.Tile.GRADES -> DashboardItem.Type.GRADES + DashboardItem.Tile.HOMEWORK -> DashboardItem.Type.HOMEWORK + DashboardItem.Tile.ANNOUNCEMENTS -> DashboardItem.Type.ANNOUNCEMENTS + DashboardItem.Tile.EXAMS -> DashboardItem.Type.EXAMS + DashboardItem.Tile.CONFERENCES -> DashboardItem.Type.CONFERENCES + DashboardItem.Tile.ADS -> DashboardItem.Type.ADS +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt new file mode 100644 index 000000000..12374859d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -0,0 +1,684 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository +import io.github.wulkanowy.data.repositories.ConferenceRepository +import io.github.wulkanowy.data.repositories.ExamRepository +import io.github.wulkanowy.data.repositories.GradeRepository +import io.github.wulkanowy.data.repositories.HomeworkRepository +import io.github.wulkanowy.data.repositories.LuckyNumberRepository +import io.github.wulkanowy.data.repositories.MessageRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.calculatePercentage +import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.flowWithResourceIn +import io.github.wulkanowy.utils.nextOrSameSchoolDay +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.time.LocalDate +import java.time.LocalDateTime +import javax.inject.Inject + +class DashboardPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val luckyNumberRepository: LuckyNumberRepository, + private val gradeRepository: GradeRepository, + private val semesterRepository: SemesterRepository, + private val messageRepository: MessageRepository, + private val attendanceSummaryRepository: AttendanceSummaryRepository, + private val timetableRepository: TimetableRepository, + private val homeworkRepository: HomeworkRepository, + private val examRepository: ExamRepository, + private val conferenceRepository: ConferenceRepository, + private val preferencesRepository: PreferencesRepository, + private val schoolAnnouncementRepository: SchoolAnnouncementRepository +) : BasePresenter(errorHandler, studentRepository) { + + private val dashboardItemLoadedList = mutableListOf() + + private val dashboardItemRefreshLoadedList = mutableListOf() + + private lateinit var dashboardItemsToLoad: Set + + private var dashboardTilesToLoad: Set = emptySet() + + private lateinit var lastError: Throwable + + override fun onAttachView(view: DashboardView) { + super.onAttachView(view) + + with(view) { + initView() + showProgress(true) + showContent(false) + } + + preferencesRepository.selectedDashboardTilesFlow + .onEach { loadData(tilesToLoad = it) } + .launch("dashboard_pref") + } + + fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set) { + val oldDashboardDataToLoad = dashboardTilesToLoad + + dashboardTilesToLoad = tilesToLoad + dashboardItemsToLoad = dashboardTilesToLoad.map { it.toDashboardItemType() }.toSet() + + removeUnselectedTiles() + + val newTileList = generateTileListToLoad(oldDashboardDataToLoad, forceRefresh) + loadTiles(forceRefresh, newTileList) + } + + private fun removeUnselectedTiles() { + val isLuckyNumberToLoad = + dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER } + val isMessagesToLoad = + dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES } + val isAttendanceToLoad = + dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE } + + dashboardItemLoadedList.removeAll { loadedTile -> dashboardItemsToLoad.none { it == loadedTile.type } } + + val horizontalGroup = + dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup? + + if (horizontalGroup != null) { + val horizontalIndex = dashboardItemLoadedList.indexOf(horizontalGroup) + dashboardItemLoadedList.remove(horizontalGroup) + + var updatedHorizontalGroup = horizontalGroup + + if (horizontalGroup.luckyNumber != null && !isLuckyNumberToLoad) { + updatedHorizontalGroup = updatedHorizontalGroup.copy(luckyNumber = null) + } + + if (horizontalGroup.attendancePercentage != null && !isAttendanceToLoad) { + updatedHorizontalGroup = updatedHorizontalGroup.copy(attendancePercentage = null) + } + + if (horizontalGroup.unreadMessagesCount != null && !isMessagesToLoad) { + updatedHorizontalGroup = updatedHorizontalGroup.copy(unreadMessagesCount = null) + } + + if (horizontalGroup.error != null) { + updatedHorizontalGroup = updatedHorizontalGroup.copy(error = null, isLoading = true) + } + + dashboardItemLoadedList.add(horizontalIndex, updatedHorizontalGroup) + } + + view?.updateData(dashboardItemLoadedList) + } + + private fun loadTiles(forceRefresh: Boolean, tileList: List) { + tileList.forEach { + when (it) { + DashboardItem.Tile.ACCOUNT -> loadCurrentAccount(forceRefresh) + DashboardItem.Tile.LUCKY_NUMBER -> loadLuckyNumber(forceRefresh) + DashboardItem.Tile.MESSAGES -> loadMessages(forceRefresh) + DashboardItem.Tile.ATTENDANCE -> loadAttendance(forceRefresh) + DashboardItem.Tile.LESSONS -> loadLessons(forceRefresh) + DashboardItem.Tile.GRADES -> loadGrades(forceRefresh) + DashboardItem.Tile.HOMEWORK -> loadHomework(forceRefresh) + DashboardItem.Tile.ANNOUNCEMENTS -> loadSchoolAnnouncements(forceRefresh) + DashboardItem.Tile.EXAMS -> loadExams(forceRefresh) + DashboardItem.Tile.CONFERENCES -> loadConferences(forceRefresh) + DashboardItem.Tile.ADS -> TODO() + } + } + } + + private fun generateTileListToLoad( + oldDashboardTileToLoad: Set, + forceRefresh: Boolean + ) = dashboardTilesToLoad.filter { newTileToLoad -> + oldDashboardTileToLoad.none { it == newTileToLoad } || forceRefresh + } + + fun onSwipeRefresh() { + Timber.i("Force refreshing the dashboard") + loadData(true, preferencesRepository.selectedDashboardTiles) + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData(true, preferencesRepository.selectedDashboardTiles) + } + + fun onViewReselected() { + Timber.i("Dashboard view is reselected") + view?.run { + resetView() + popViewToRoot() + } + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + fun onDashboardTileSettingsSelected(): Boolean { + view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) + return true + } + + fun onDashboardTileSettingSelected(selectedItems: List) { + preferencesRepository.selectedDashboardTiles = selectedItems.map { + DashboardItem.Tile.valueOf(it) + }.toSet() + } + + private fun loadCurrentAccount(forceRefresh: Boolean) { + flowWithResource { studentRepository.getCurrentStudent(false) } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard account data started") + if (forceRefresh) return@onEach + updateData(DashboardItem.Account(it.data, isLoading = true), forceRefresh) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard account result: Success") + updateData(DashboardItem.Account(it.data), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard account result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Account(error = it.error), forceRefresh) + } + } + } + .launch("dashboard_account") + } + + private fun loadLuckyNumber(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + + luckyNumberRepository.getLuckyNumber(student, forceRefresh) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard lucky number data started") + if (forceRefresh) return@onEach + processHorizontalGroupData( + luckyNumber = it.data?.luckyNumber, + isLoading = true, + forceRefresh = forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard lucky number result: Success") + processHorizontalGroupData( + luckyNumber = it.data?.luckyNumber ?: -1, + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard lucky number result: An exception occurred") + errorHandler.dispatch(it.error!!) + processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh) + } + } + }.launch("dashboard_lucky_number") + } + + private fun loadMessages(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + + messageRepository.getMessages(student, semester, MessageFolder.RECEIVED, forceRefresh) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard messages data started") + if (forceRefresh) return@onEach + val unreadMessagesCount = it.data?.count { message -> message.unread } + + processHorizontalGroupData( + unreadMessagesCount = unreadMessagesCount, + isLoading = true, + forceRefresh = forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard messages result: Success") + val unreadMessagesCount = it.data?.count { message -> message.unread } + + processHorizontalGroupData( + unreadMessagesCount = unreadMessagesCount, + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard messages result: An exception occurred") + errorHandler.dispatch(it.error!!) + processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh) + } + } + }.launch("dashboard_messages") + } + + private fun loadAttendance(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + + attendanceSummaryRepository.getAttendanceSummary(student, semester, -1, forceRefresh) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard attendance data started") + if (forceRefresh) return@onEach + val attendancePercentage = it.data?.calculatePercentage() + + processHorizontalGroupData( + attendancePercentage = attendancePercentage, + isLoading = true, + forceRefresh = forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard attendance result: Success") + val attendancePercentage = it.data?.calculatePercentage() + + processHorizontalGroupData( + attendancePercentage = attendancePercentage, + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard attendance result: An exception occurred") + errorHandler.dispatch(it.error!!) + + processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh) + } + } + }.launch("dashboard_attendance") + } + + private fun loadGrades(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + + gradeRepository.getGrades(student, semester, forceRefresh) + }.map { originalResource -> + val filteredSubjectWithGrades = originalResource.data?.first.orEmpty() + .filter { grade -> + grade.date.isAfter(LocalDate.now().minusDays(7)) + } + .groupBy { grade -> grade.subject } + .mapValues { entry -> + entry.value + .take(5) + .sortedBy { grade -> grade.date } + } + .toList() + .sortedBy { subjectWithGrades -> subjectWithGrades.second[0].date } + .toMap() + + Resource( + status = originalResource.status, + data = filteredSubjectWithGrades.takeIf { originalResource.data != null }, + error = originalResource.error + ) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard grades data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Grades( + subjectWithGrades = it.data, + gradeTheme = preferencesRepository.gradeColorTheme, + isLoading = true + ), forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard grades result: Success") + updateData( + DashboardItem.Grades( + subjectWithGrades = it.data, + gradeTheme = preferencesRepository.gradeColorTheme + ), forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard grades result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Grades(error = it.error), forceRefresh) + } + } + }.launch("dashboard_grades") + } + + private fun loadLessons(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + val date = LocalDate.now().nextOrSameSchoolDay + + timetableRepository.getTimetable( + student = student, + semester = semester, + start = date, + end = date.plusDays(1), + forceRefresh = forceRefresh + ) + + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard lessons data started") + if (forceRefresh) return@onEach + updateData(DashboardItem.Lessons(it.data, isLoading = true), forceRefresh) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard lessons result: Success") + updateData(DashboardItem.Lessons(it.data), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard lessons result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Lessons(error = it.error), forceRefresh) + } + } + }.launch("dashboard_lessons") + } + + private fun loadHomework(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + val date = LocalDate.now().nextOrSameSchoolDay + + homeworkRepository.getHomework( + student = student, + semester = semester, + start = date, + end = date, + forceRefresh = forceRefresh + ) + }.map { homeworkResource -> + val currentDate = LocalDate.now() + + val filteredHomework = homeworkResource.data?.filter { + (it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone + } + + homeworkResource.copy(data = filteredHomework) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard homework data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Homework(it.data ?: emptyList(), isLoading = true), + forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard homework result: Success") + updateData(DashboardItem.Homework(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard homework result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Homework(error = it.error), forceRefresh) + } + } + }.launch("dashboard_homework") + } + + private fun loadSchoolAnnouncements(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + + schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard announcements data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Announcements( + it.data ?: emptyList(), + isLoading = true + ), forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard announcements result: Success") + updateData(DashboardItem.Announcements(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard announcements result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Announcements(error = it.error), forceRefresh) + } + } + }.launch("dashboard_announcements") + } + + private fun loadExams(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + + examRepository.getExams( + student = student, + semester = semester, + start = LocalDate.now(), + end = LocalDate.now().plusDays(7), + forceRefresh = forceRefresh + ) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard exams data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Exams(it.data.orEmpty(), isLoading = true), + forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard exams result: Success") + updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard exams result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Exams(error = it.error), forceRefresh) + } + } + }.launch("dashboard_exams") + } + + private fun loadConferences(forceRefresh: Boolean) { + flowWithResourceIn { + val student = studentRepository.getCurrentStudent(true) + val semester = semesterRepository.getCurrentSemester(student) + + conferenceRepository.getConferences( + student = student, + semester = semester, + forceRefresh = forceRefresh, + startDate = LocalDateTime.now() + ) + }.onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard conferences data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true), + forceRefresh + ) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard conferences result: Success") + updateData(DashboardItem.Conferences(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard conferences result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Conferences(error = it.error), forceRefresh) + } + } + }.launch("dashboard_conferences") + } + + private fun processHorizontalGroupData( + luckyNumber: Int? = null, + unreadMessagesCount: Int? = null, + attendancePercentage: Double? = null, + error: Throwable? = null, + isLoading: Boolean = false, + forceRefresh: Boolean + ) { + val isLuckyNumberToLoad = + dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER } + val isMessagesToLoad = + dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES } + val isAttendanceToLoad = + dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE } + val isPushedToList = + dashboardItemLoadedList.any { it.type == DashboardItem.Type.HORIZONTAL_GROUP } + + if (error != null) { + updateData(DashboardItem.HorizontalGroup(error = error), forceRefresh) + return + } + + if (isLoading) { + val horizontalGroup = + dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup? + val updatedHorizontalGroup = + horizontalGroup?.copy(isLoading = true) ?: DashboardItem.HorizontalGroup(isLoading = true) + + updateData(updatedHorizontalGroup, forceRefresh) + } + + if (forceRefresh && !isPushedToList) { + updateData(DashboardItem.HorizontalGroup(), forceRefresh) + } + + val horizontalGroup = + dashboardItemLoadedList.single { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup + + when { + luckyNumber != null -> { + updateData(horizontalGroup.copy(luckyNumber = luckyNumber), forceRefresh) + } + unreadMessagesCount != null -> { + updateData( + horizontalGroup.copy(unreadMessagesCount = unreadMessagesCount), + forceRefresh + ) + } + attendancePercentage != null -> { + updateData( + horizontalGroup.copy(attendancePercentage = attendancePercentage), + forceRefresh + ) + } + } + + val isHorizontalGroupLoaded = dashboardItemLoadedList.any { + if (it !is DashboardItem.HorizontalGroup) return@any false + + val isLuckyNumberStateCorrect = (it.luckyNumber != null) == isLuckyNumberToLoad + val isMessagesStateCorrect = (it.unreadMessagesCount != null) == isMessagesToLoad + val isAttendanceStateCorrect = (it.attendancePercentage != null) == isAttendanceToLoad + + isLuckyNumberStateCorrect && isAttendanceStateCorrect && isMessagesStateCorrect + } + + if (isHorizontalGroupLoaded) { + val updatedHorizontalGroup = + dashboardItemLoadedList.single { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup + + updateData(updatedHorizontalGroup.copy(isLoading = false, error = null), forceRefresh) + } + } + + private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { + val isForceRefreshError = forceRefresh && dashboardItem.error != null + + with(dashboardItemLoadedList) { + removeAll { it.type == dashboardItem.type && !isForceRefreshError } + if (!isForceRefreshError) add(dashboardItem) + sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal } + } + + if (forceRefresh) { + with(dashboardItemRefreshLoadedList) { + removeAll { it.type == dashboardItem.type } + add(dashboardItem) + } + } + + dashboardItemLoadedList.sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal } + + val isItemsLoaded = + dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } } + val isRefreshItemLoaded = + dashboardItemsToLoad.all { type -> dashboardItemRefreshLoadedList.any { it.type == type } } + val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all { + it.isDataLoaded || it.error != null + } + val isRefreshItemsDataLoaded = isRefreshItemLoaded && dashboardItemRefreshLoadedList.all { + it.isDataLoaded || it.error != null + } + + if (isRefreshItemsDataLoaded) { + view?.showRefresh(false) + dashboardItemRefreshLoadedList.clear() + } + + view?.run { + if (!forceRefresh) { + showProgress(!isItemsDataLoaded) + showContent(isItemsDataLoaded) + } + updateData(dashboardItemLoadedList.toList()) + } + + if (isItemsLoaded) { + val filteredItems = + dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val isAccountItemError = + dashboardItemLoadedList.single { it.type == DashboardItem.Type.ACCOUNT }.error != null + val isGeneralError = + filteredItems.all { it.error != null } && filteredItems.isNotEmpty() || isAccountItemError + + val errorMessage = filteredItems.map { it.error?.stackTraceToString() }.toString() + + lastError = Exception(errorMessage) + + view?.run { + showProgress(false) + showContent(!isGeneralError) + showErrorView(isGeneralError) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt new file mode 100644 index 000000000..d5c5e5a70 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -0,0 +1,26 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import io.github.wulkanowy.ui.base.BaseView + +interface DashboardView : BaseView { + + fun initView() + + fun updateData(data: List) + + fun showDashboardTileSettings(selectedItems: List) + + fun showProgress(show: Boolean) + + fun showContent(show: Boolean) + + fun showRefresh(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) + + fun resetView() + + fun popViewToRoot() +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 65b6ba542..188cf5fc9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -33,7 +33,7 @@ import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment @@ -218,12 +218,12 @@ class MainActivity : BaseActivity(), MainVie with(binding.mainBottomNav) { with(menu) { - add(Menu.NONE, 0, Menu.NONE, R.string.grade_title) + add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) + .setIcon(R.drawable.ic_main_dashboard) + add(Menu.NONE, 1, Menu.NONE, R.string.grade_title) .setIcon(R.drawable.ic_main_grade) - add(Menu.NONE, 1, Menu.NONE, R.string.attendance_title) + add(Menu.NONE, 2, Menu.NONE, R.string.attendance_title) .setIcon(R.drawable.ic_main_attendance) - add(Menu.NONE, 2, Menu.NONE, R.string.exam_title) - .setIcon(R.drawable.ic_main_exam) add(Menu.NONE, 3, Menu.NONE, R.string.timetable_title) .setIcon(R.drawable.ic_main_timetable) add(Menu.NONE, 4, Menu.NONE, R.string.more_title) @@ -256,9 +256,9 @@ class MainActivity : BaseActivity(), MainVie } fragmentHideStrategy = HIDE rootFragments = listOf( + DashboardFragment.newInstance(), GradeFragment.newInstance(), AttendanceFragment.newInstance(), - ExamFragment.newInstance(), TimetableFragment.newInstance(), MoreFragment.newInstance() ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt index f5ba23ab2..2f0957c46 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt @@ -9,15 +9,15 @@ import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentMoreBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.utils.getCompatDrawable import javax.inject.Inject @@ -48,9 +48,6 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), override val noteRes: Pair? get() = context?.run { getString(R.string.note_title) to getCompatDrawable(R.drawable.ic_more_note) } - override val luckyNumberRes: Pair? - get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) } - override val conferencesRes: Pair? get() = context?.run { getString(R.string.conferences_title) to getCompatDrawable(R.drawable.ic_more_conferences) } @@ -66,6 +63,9 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), override val settingsRes: Pair? get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) } + override val examRes: Pair? + get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMoreBinding.bind(view) @@ -104,10 +104,6 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), (activity as? MainActivity)?.pushView(NoteFragment.newInstance()) } - override fun openLuckyNumberView() { - (activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance()) - } - override fun openSchoolAnnouncementView() { (activity as? MainActivity)?.pushView(SchoolAnnouncementFragment.newInstance()) } @@ -128,6 +124,10 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), (activity as? MainActivity)?.pushView(SettingsFragment.newInstance()) } + override fun openExamView() { + (activity as? MainActivity)?.pushView(ExamFragment.newInstance()) + } + override fun popView(depth: Int) { (activity as? MainActivity)?.popView(depth) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index 4a31aaf73..a2b7f204e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -23,9 +23,9 @@ class MorePresenter @Inject constructor( view?.run { when (title) { messagesRes?.first -> openMessagesView() + examRes?.first -> openExamView() homeworkRes?.first -> openHomeworkView() noteRes?.first -> openNoteView() - luckyNumberRes?.first -> openLuckyNumberView() conferencesRes?.first -> openConferencesView() schoolAnnouncementRes?.first -> openSchoolAnnouncementView() schoolAndTeachersRes?.first -> openSchoolAndTeachersView() @@ -45,9 +45,9 @@ class MorePresenter @Inject constructor( view?.run { updateData(listOfNotNull( messagesRes, + examRes, homeworkRes, noteRes, - luckyNumberRes, conferencesRes, schoolAnnouncementRes, schoolAndTeachersRes, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index 6cf88043b..c4a07bdcc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -11,8 +11,6 @@ interface MoreView : BaseView { val noteRes: Pair? - val luckyNumberRes: Pair? - val conferencesRes: Pair? val schoolAnnouncementRes: Pair? @@ -23,6 +21,8 @@ interface MoreView : BaseView { val settingsRes: Pair? + val examRes: Pair? + fun initView() fun updateData(data: List>) @@ -37,8 +37,6 @@ interface MoreView : BaseView { fun openNoteView() - fun openLuckyNumberView() - fun openSchoolAnnouncementView() fun openConferencesView() @@ -46,4 +44,6 @@ interface MoreView : BaseView { fun openSchoolAndTeachersView() fun openMobileDevicesView() + + fun openExamView() } diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt index b0ec3a712..94b6a2191 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -1,48 +1,46 @@ package io.github.wulkanowy.utils -import android.annotation.SuppressLint import java.text.SimpleDateFormat import java.time.DayOfWeek.FRIDAY import java.time.DayOfWeek.MONDAY import java.time.DayOfWeek.SATURDAY import java.time.DayOfWeek.SUNDAY -import java.time.Instant.ofEpochMilli +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime -import java.time.LocalDateTime.now -import java.time.LocalDateTime.ofInstant import java.time.LocalTime import java.time.Month import java.time.ZoneId import java.time.ZoneOffset -import java.time.format.DateTimeFormatter.ofPattern +import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAdjusters.firstInMonth import java.time.temporal.TemporalAdjusters.next import java.time.temporal.TemporalAdjusters.previous import java.util.Locale -private const val DATE_PATTERN = "dd.MM.yyyy" +private const val DEFAULT_DATE_PATTERN = "dd.MM.yyyy" -fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate = - LocalDate.parse(this, ofPattern(format)) +fun String.toLocalDate(format: String = DEFAULT_DATE_PATTERN): LocalDate = + LocalDate.parse(this, DateTimeFormatter.ofPattern(format)) fun LocalDateTime.toTimestamp() = atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toInstant().toEpochMilli() -fun Long.toLocalDateTime(): LocalDateTime = ofInstant(ofEpochMilli(this), ZoneId.systemDefault()) +fun Long.toLocalDateTime(): LocalDateTime = + LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault()) fun LocalDate.toTimestamp() = atTime(LocalTime.now()).toTimestamp() -fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = format(ofPattern(format)) +fun LocalDate.toFormattedString(pattern: String = DEFAULT_DATE_PATTERN): String = + format(DateTimeFormatter.ofPattern(pattern)) -fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = - format(ofPattern(format)) +fun LocalDateTime.toFormattedString(pattern: String = DEFAULT_DATE_PATTERN): String = + format(DateTimeFormatter.ofPattern(pattern)) -@SuppressLint("DefaultLocale") fun Month.getFormattedName(): String { val formatter = SimpleDateFormat("LLLL", Locale.getDefault()) - val date = now().withMonth(value) + val date = LocalDateTime.now().withMonth(value) return formatter.format(date.toInstant(ZoneOffset.UTC).toEpochMilli()).capitalise() } @@ -85,7 +83,7 @@ inline val LocalDate.previousOrSameSchoolDay: LocalDate } inline val LocalDate.weekDayName: String - get() = format(ofPattern("EEEE", Locale.getDefault())) + get() = format(DateTimeFormatter.ofPattern("EEEE", Locale.getDefault())) inline val LocalDate.monday: LocalDate get() = with(MONDAY) diff --git a/app/src/main/res/drawable/ic_main_dashboard.xml b/app/src/main/res/drawable/ic_main_dashboard.xml new file mode 100644 index 000000000..ed2269256 --- /dev/null +++ b/app/src/main/res/drawable/ic_main_dashboard.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml new file mode 100644 index 000000000..cf799ecc2 --- /dev/null +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_account.xml b/app/src/main/res/layout/item_dashboard_account.xml new file mode 100644 index 000000000..139157c8b --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_account.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_announcements.xml b/app/src/main/res/layout/item_dashboard_announcements.xml new file mode 100644 index 000000000..19f720884 --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_announcements.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_conferences.xml b/app/src/main/res/layout/item_dashboard_conferences.xml new file mode 100644 index 000000000..02d3edfc8 --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_conferences.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_exams.xml b/app/src/main/res/layout/item_dashboard_exams.xml new file mode 100644 index 000000000..9cc98d790 --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_exams.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_grades.xml b/app/src/main/res/layout/item_dashboard_grades.xml new file mode 100644 index 000000000..5cc9ce308 --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_grades.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_homework.xml b/app/src/main/res/layout/item_dashboard_homework.xml new file mode 100644 index 000000000..975d66efd --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_homework.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml new file mode 100644 index 000000000..aaf12834f --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml new file mode 100644 index 000000000..a2a92c54c --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subitem_dashboard_announcements.xml b/app/src/main/res/layout/subitem_dashboard_announcements.xml new file mode 100644 index 000000000..13c8fa0a4 --- /dev/null +++ b/app/src/main/res/layout/subitem_dashboard_announcements.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subitem_dashboard_conferences.xml b/app/src/main/res/layout/subitem_dashboard_conferences.xml new file mode 100644 index 000000000..e80809365 --- /dev/null +++ b/app/src/main/res/layout/subitem_dashboard_conferences.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subitem_dashboard_exams.xml b/app/src/main/res/layout/subitem_dashboard_exams.xml new file mode 100644 index 000000000..4eb943ce9 --- /dev/null +++ b/app/src/main/res/layout/subitem_dashboard_exams.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subitem_dashboard_grades.xml b/app/src/main/res/layout/subitem_dashboard_grades.xml new file mode 100644 index 000000000..b29fae6ab --- /dev/null +++ b/app/src/main/res/layout/subitem_dashboard_grades.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subitem_dashboard_homework.xml b/app/src/main/res/layout/subitem_dashboard_homework.xml new file mode 100644 index 000000000..6e07790b3 --- /dev/null +++ b/app/src/main/res/layout/subitem_dashboard_homework.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subitem_dashboard_small_grade.xml b/app/src/main/res/layout/subitem_dashboard_small_grade.xml new file mode 100644 index 000000000..986d9602a --- /dev/null +++ b/app/src/main/res/layout/subitem_dashboard_small_grade.xml @@ -0,0 +1,15 @@ + + diff --git a/app/src/main/res/menu/action_menu_dashboard.xml b/app/src/main/res/menu/action_menu_dashboard.xml new file mode 100644 index 000000000..dbdd6e812 --- /dev/null +++ b/app/src/main/res/menu/action_menu_dashboard.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index dac94c3ff..158490471 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -40,7 +40,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login - http://fakelog.tk/?email + http://fakelog.cf/?email Default diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index a3aa62f81..5721763f1 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -25,4 +25,12 @@ false false false + + LUCKY_NUMBER + MESSAGES + ATTENDANCE + LESSONS + GRADES + ANNOUNCEMENTS + diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 5a758709f..5b10d5ed0 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -3,6 +3,7 @@ start_menu attendance_present app_theme + dashboard_tiles grade_color_scheme expand_grade grade_average_mode diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 2d2eedb4c..f5a2b864e 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -109,4 +109,27 @@ both_semesters all_year + + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + + + LUCKY_NUMBER + MESSAGES + ATTENDANCE + LESSONS + GRADES + HOMEWORK + ANNOUNCEMENTS + EXAMS + CONFERENCES + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 248de3f4d..3b42a7e77 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Select account Account details Student info + Dashboard @@ -469,6 +470,74 @@ Refresh + + Lessons + (Tomorrow) + %1$s (%2$s) + In a moment: + Soon: + First: + Now: + + in %1$d minute + in %1$d minutes + + + %1$d more minute + %1$d more minutes + + End of lessons + Next: + Later: + + %1$d more lesson + %1$d more lessons + + until %1$s + No lessons + An error occurred while loading the lesson + + Homework + No homework + An error occurred while loading the homework + + %1$d more homework + %1$d more homework + + due %1$s + + Last grades + No new grades + An error occurred while loading the grades + + School announcements + No announcements + An error occurred while loading the announcements + + %1$d more announcement + %1$d more announcements + + + Exams + No exams + An error occurred while loading the exams + + %1$d more exam + %1$d more exams + + + Conferences + No conferences + An error occurred while loading the conferences + + %1$d more conference + %1$d more conferences + + + An error occurred while loading data + No + + Check for updates Before reporting a bug, check first if an update with the bug fix is available @@ -552,6 +621,8 @@ Synchronization Grades + Dashboard + Tiles visibility Attendance Timetable Grades diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 095c5070b..b34fd417d 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -29,6 +29,17 @@ app:title="@string/pref_view_list" app:useSimpleSummaryProvider="true" /> + + + @@ -38,8 +49,7 @@ app:entryValues="@array/grade_color_scheme_values" app:iconSpaceReserved="false" app:key="@string/pref_key_grade_color_scheme" - app:title="@string/pref_view_grade_color_scheme" - app:useSimpleSummaryProvider="true" /> + app:title="@string/pref_view_grade_color_scheme" /> - +