Merge branch 'release/1.2.1'

This commit is contained in:
Mikołaj Pich 2021-09-05 23:29:23 +02:00
commit 3d0dcead50
39 changed files with 825 additions and 580 deletions

View File

@ -21,8 +21,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode 93 versionCode 94
versionName "1.2.0" versionName "1.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -133,7 +133,7 @@ play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12') serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false defaultToAppBundles = false
track = 'beta' track = 'production'
updatePriority = 3 updatePriority = 3
} }
@ -157,11 +157,11 @@ ext {
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.2.0" implementation "io.github.wulkanowy:sdk:1.2.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "androidx.core:core-ktx:1.6.0" implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.activity:activity-ktx:1.3.1" implementation "androidx.activity:activity-ktx:1.3.1"
@ -215,10 +215,10 @@ dependencies {
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.0' playImplementation 'com.google.android.play:core:1.10.1'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
hmsImplementation 'com.huawei.hms:hianalytics:6.1.1.300' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -228,7 +228,7 @@ dependencies {
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk" testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.6.1' testImplementation 'org.robolectric:robolectric:4.6.1'

View File

@ -33,10 +33,16 @@ class GradeRepository @Inject constructor(
private val cacheKey = "grade" private val cacheKey = "grade"
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( fun getGrades(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) -> shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) val isShouldBeRefreshed =
refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
}, },
query = { query = {
@ -59,8 +65,14 @@ class GradeRepository @Inject constructor(
} }
) )
private suspend fun refreshGradeDetails(student: Student, oldGrades: List<Grade>, newDetails: List<Grade>, notify: Boolean) { private suspend fun refreshGradeDetails(
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() student: Student,
oldGrades: List<Grade>,
newDetails: List<Grade>,
notify: Boolean
) {
val notifyBreakDate =
oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply { if (it.date >= notifyBreakDate) it.apply {
@ -70,10 +82,15 @@ class GradeRepository @Inject constructor(
}) })
} }
private suspend fun refreshGradeSummaries(oldSummaries: List<GradeSummary>, newSummary: List<GradeSummary>, notify: Boolean) { private suspend fun refreshGradeSummaries(
oldSummaries: List<GradeSummary>,
newSummary: List<GradeSummary>,
notify: Boolean
) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
val oldSummary = oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject } val oldSummary =
oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject }
summary.isPredictedGradeNotified = when { summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false notify && oldSummary?.predictedGrade != summary.predictedGrade -> false

View File

@ -22,7 +22,11 @@ class SemesterRepository @Inject constructor(
private val dispatchers: DispatchersProvider private val dispatchers: DispatchersProvider
) { ) {
suspend fun getSemesters(student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false) = withContext(dispatchers.backgroundThread) { suspend fun getSemesters(
student: Student,
forceRefresh: Boolean = false,
refreshOnNoCurrent: Boolean = false
) = withContext(dispatchers.backgroundThread) {
val semesters = semesterDb.loadAll(student.studentId, student.classId) val semesters = semesterDb.loadAll(student.studentId, student.classId)
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
@ -31,14 +35,21 @@ class SemesterRepository @Inject constructor(
} else semesters } else semesters
} }
private fun isShouldFetch(student: Student, semesters: List<Semester>, forceRefresh: Boolean, refreshOnNoCurrent: Boolean): Boolean { private fun isShouldFetch(
student: Student,
semesters: List<Semester>,
forceRefresh: Boolean,
refreshOnNoCurrent: Boolean
): Boolean {
val isNoSemesters = semesters.isEmpty() val isNoSemesters = semesters.isEmpty()
val isRefreshOnModeChangeRequired = if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { val isRefreshOnModeChangeRequired =
if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
semesters.firstOrNull { it.isCurrent }?.diaryId == 0 semesters.firstOrNull { it.isCurrent }?.diaryId == 0
} else false } else false
val isRefreshOnNoCurrentAppropriate = refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } val isRefreshOnNoCurrentAppropriate =
refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent }
return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate
} }
@ -52,7 +63,8 @@ class SemesterRepository @Inject constructor(
semesterDb.insertSemesters(new.uniqueSubtract(old)) semesterDb.insertSemesters(new.uniqueSubtract(old))
} }
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = withContext(dispatchers.backgroundThread) { suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
withContext(dispatchers.backgroundThread) {
getSemesters(student, forceRefresh).getCurrentOrLast() getSemesters(student, forceRefresh).getCurrentOrLast()
} }
} }

View File

@ -18,7 +18,7 @@ open class BasePresenter<T : BaseView>(
protected val studentRepository: StudentRepository protected val studentRepository: StudentRepository
) : CoroutineScope { ) : CoroutineScope {
private var job: Job = Job() private var job = Job()
private val jobs = mutableMapOf<String, Job>() private val jobs = mutableMapOf<String, Job>()

View File

@ -8,7 +8,7 @@ import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.FragmentAccountBinding import io.github.wulkanowy.databinding.FragmentAccountBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
@ -75,9 +75,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
} }
} }
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) { override fun openAccountDetailsView(student: Student) {
(activity as? MainActivity)?.pushView( (activity as? MainActivity)?.pushView(AccountDetailsFragment.newInstance(student))
AccountDetailsFragment.newInstance(studentWithSemesters)
)
} }
} }

View File

@ -28,7 +28,7 @@ class AccountPresenter @Inject constructor(
} }
fun onItemSelected(studentWithSemesters: StudentWithSemesters) { fun onItemSelected(studentWithSemesters: StudentWithSemesters) {
view?.openAccountDetailsView(studentWithSemesters) view?.openAccountDetailsView(studentWithSemesters.student)
} }
private fun loadData() { private fun loadData() {

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.account package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView { interface AccountView : BaseView {
@ -11,5 +11,5 @@ interface AccountView : BaseView {
fun openLoginView() fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) fun openAccountDetailsView(student: Student)
} }

View File

@ -37,9 +37,9 @@ class AccountDetailsFragment :
private const val ARGUMENT_KEY = "Data" private const val ARGUMENT_KEY = "Data"
fun newInstance(studentWithSemesters: StudentWithSemesters) = fun newInstance(student: Student) =
AccountDetailsFragment().apply { AccountDetailsFragment().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, studentWithSemesters) } arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) }
} }
} }
@ -51,7 +51,7 @@ class AccountDetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view) binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as StudentWithSemesters) presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
} }
override fun initView() { override fun initView() {

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.account.accountdetails
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
@ -27,9 +28,9 @@ class AccountDetailsPresenter @Inject constructor(
private var studentId: Long? = null private var studentId: Long? = null
fun onAttachView(view: AccountDetailsView, studentWithSemesters: StudentWithSemesters) { fun onAttachView(view: AccountDetailsView, student: Student) {
super.onAttachView(view) super.onAttachView(view)
studentId = studentWithSemesters.student.id studentId = student.id
view.initView() view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError errorHandler.showErrorMessage = ::showErrorViewOnError

View File

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding import io.github.wulkanowy.databinding.ItemDashboardAccountBinding
@ -41,7 +42,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private var lessonsTimer: Timer? = null private var lessonsTimer: Timer? = null
var onAccountTileClickListener: () -> Unit = {} var onAccountTileClickListener: (Student) -> Unit = {}
var onLuckyNumberTileClickListener: () -> Unit = {} var onLuckyNumberTileClickListener: () -> Unit = {}
@ -152,7 +153,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
dashboardAccountItemName.text = student?.nickOrName.orEmpty() dashboardAccountItemName.text = student?.nickOrName.orEmpty()
dashboardAccountItemSchoolName.text = student?.schoolName.orEmpty() dashboardAccountItemSchoolName.text = student?.schoolName.orEmpty()
root.setOnClickListener { onAccountTileClickListener() } root.setOnClickListener { student?.let(onAccountTileClickListener) }
} }
} }
@ -170,38 +171,41 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val binding = horizontalGroupViewHolder.binding val binding = horizontalGroupViewHolder.binding
val context = binding.root.context val context = binding.root.context
val attendanceColor = when { val attendanceColor = when {
attendancePercentage ?: 0.0 <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { attendancePercentage == null || attendancePercentage == .0 -> {
context.getThemeAttrColor(R.attr.colorOnSurface)
}
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorPrimary) context.getThemeAttrColor(R.attr.colorPrimary)
} }
attendancePercentage ?: 0.0 <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorTimetableChange) context.getThemeAttrColor(R.attr.colorTimetableChange)
} }
else -> context.getThemeAttrColor(R.attr.colorOnSurface) else -> context.getThemeAttrColor(R.attr.colorOnSurface)
} }
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
} else {
"%.2f%%".format(attendancePercentage)
}
with(binding.dashboardHorizontalGroupItemAttendanceValue) { with(binding.dashboardHorizontalGroupItemAttendanceValue) {
text = "%.2f%%".format(attendancePercentage) text = attendanceString
setTextColor(attendanceColor) setTextColor(attendanceColor)
} }
with(binding) { with(binding) {
dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString() dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString()
dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == -1) { dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) {
context.getString(R.string.dashboard_horizontal_group_no_lukcy_number) context.getString(R.string.dashboard_horizontal_group_no_data)
} else luckyNumber?.toString() } else luckyNumber?.toString()
if (dashboardHorizontalGroupItemInfoContainer.isVisible != (error != null || isLoading)) {
dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading
} dashboardHorizontalGroupItemInfoProgress.isVisible =
(isLoading && !item.isDataLoaded) || (isLoading && !item.isFullDataLoaded)
if (dashboardHorizontalGroupItemInfoProgress.isVisible != isLoading) {
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoading
}
dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null
with(dashboardHorizontalGroupItemLuckyContainer) { with(dashboardHorizontalGroupItemLuckyContainer) {
isVisible = error == null && !isLoading && luckyNumber != null isVisible = luckyNumber != null && luckyNumber != -1
setOnClickListener { onLuckyNumberTileClickListener() } setOnClickListener { onLuckyNumberTileClickListener() }
updateLayoutParams<ViewGroup.MarginLayoutParams> { updateLayoutParams<ViewGroup.MarginLayoutParams> {
@ -216,7 +220,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
with(dashboardHorizontalGroupItemAttendanceContainer) { with(dashboardHorizontalGroupItemAttendanceContainer) {
isVisible = error == null && !isLoading && attendancePercentage != null isVisible = attendancePercentage != null && attendancePercentage != -1.0
updateLayoutParams<ConstraintLayout.LayoutParams> { updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = when { matchConstraintPercentWidth = when {
luckyNumber == null && unreadMessagesCount == null -> 1.0f luckyNumber == null && unreadMessagesCount == null -> 1.0f
@ -228,7 +232,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
with(dashboardHorizontalGroupItemMessageContainer) { with(dashboardHorizontalGroupItemMessageContainer) {
isVisible = error == null && !isLoading && unreadMessagesCount != null isVisible = unreadMessagesCount != null && unreadMessagesCount != -1
setOnClickListener { onMessageTileClickListener() } setOnClickListener { onMessageTileClickListener() }
} }
} }
@ -291,14 +295,14 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
updateLessonView(item, currentTimetable, binding) updateLessonView(item, currentTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
} }
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowTimetable.isNotEmpty() -> { tomorrowTimetable.isNotEmpty() -> {
updateLessonView(item, tomorrowTimetable, binding) updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
} }
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, tomorrowDayHeader) updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
@ -348,6 +352,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
} }
@SuppressLint("SetTextI18n")
private fun updateFirstLessonView( private fun updateFirstLessonView(
binding: ItemDashboardLessonsBinding, binding: ItemDashboardLessonsBinding,
firstLesson: Timetable?, firstLesson: Timetable?,
@ -367,7 +372,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstLesson ?: return firstLesson ?: return
val minutesToStartLesson = val minutesToStartLesson =
Duration.between(currentDateTime, firstLesson.start).toMinutes() Duration.between(currentDateTime, firstLesson.start).toMinutes() + 1
val isFirstTimeVisible: Boolean val isFirstTimeVisible: Boolean
val isFirstTimeRangeVisible: Boolean val isFirstTimeRangeVisible: Boolean
val firstTimeText: String val firstTimeText: String
@ -376,12 +381,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val firstTitleAndValueTextColor: Int val firstTitleAndValueTextColor: Int
val firstTitleAndValueTextFont: Typeface val firstTitleAndValueTextFont: Typeface
if (currentDateTime.isBefore(firstLesson.start)) { if (currentDateTime < firstLesson.start) {
if (minutesToStartLesson > 60) { if (minutesToStartLesson > 60) {
val formattedStartTime = firstLesson.start.toFormattedString("HH:mm") val formattedStartTime = firstLesson.start.toFormattedString("HH:mm")
val formattedEndTime = firstLesson.end.toFormattedString("HH:mm") val formattedEndTime = firstLesson.end.toFormattedString("HH:mm")
firstTimeRangeText = "${formattedStartTime}-${formattedEndTime}" firstTimeRangeText = "$formattedStartTime - $formattedEndTime"
firstTimeText = "" firstTimeText = ""
isFirstTimeRangeVisible = true isFirstTimeRangeVisible = true
@ -421,7 +426,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
} }
} else { } else {
val minutesToEndLesson = firstLesson.left!!.toMinutes() val minutesToEndLesson = firstLesson.left!!.toMinutes() + 1
firstTimeText = context.resources.getQuantityString( firstTimeText = context.resources.getQuantityString(
R.plurals.dashboard_timetable_first_lesson_time_more_minutes, R.plurals.dashboard_timetable_first_lesson_time_more_minutes,
@ -454,11 +459,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
with(binding.dashboardLessonsItemFirstValue) { with(binding.dashboardLessonsItemFirstValue) {
setTextColor(firstTitleAndValueTextColor) setTextColor(firstTitleAndValueTextColor)
typeface = firstTitleAndValueTextFont typeface = firstTitleAndValueTextFont
text = context.getString( text =
R.string.dashboard_timetable_lesson_value, "${firstLesson.subject} ${if (firstLesson.room.isNotBlank()) "(${firstLesson.room})" else ""}"
firstLesson.subject,
firstLesson.room
)
} }
} }
@ -472,13 +474,11 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val formattedStartTime = secondLesson?.start?.toFormattedString("HH:mm") val formattedStartTime = secondLesson?.start?.toFormattedString("HH:mm")
val formattedEndTime = secondLesson?.end?.toFormattedString("HH:mm") val formattedEndTime = secondLesson?.end?.toFormattedString("HH:mm")
val secondTimeText = "${formattedStartTime}-${formattedEndTime}" val secondTimeText = "$formattedStartTime - $formattedEndTime"
val secondValueText = if (secondLesson != null) { val secondValueText = if (secondLesson != null) {
context.getString( val roomString = if (secondLesson.room.isNotBlank()) "(${secondLesson.room})" else ""
R.string.dashboard_timetable_lesson_value,
secondLesson.subject, "${secondLesson.subject} $roomString"
secondLesson.room
)
} else { } else {
context.getString(R.string.dashboard_timetable_second_lesson_value_end) context.getString(R.string.dashboard_timetable_second_lesson_value_end)
} }

View File

@ -14,7 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentDashboardBinding import io.github.wulkanowy.databinding.FragmentDashboardBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.AccountFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment
@ -77,7 +77,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
) )
dashboardAdapter.apply { dashboardAdapter.apply {
onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) } onAccountTileClickListener = {
mainActivity.pushView(AccountDetailsFragment.newInstance(it))
}
onLuckyNumberTileClickListener = { onLuckyNumberTileClickListener = {
mainActivity.pushView(LuckyNumberFragment.newInstance()) mainActivity.pushView(LuckyNumberFragment.newInstance())
} }

View File

@ -35,6 +35,9 @@ sealed class DashboardItem(val type: Type) {
override val isDataLoaded override val isDataLoaded
get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null
val isFullDataLoaded
get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1
} }
data class Grades( data class Grades(

View File

@ -2,6 +2,8 @@ package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ConferenceRepository
@ -18,11 +20,18 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.flowWithResourceIn
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@ -48,9 +57,11 @@ class DashboardPresenter @Inject constructor(
private val dashboardItemRefreshLoadedList = mutableListOf<DashboardItem>() private val dashboardItemRefreshLoadedList = mutableListOf<DashboardItem>()
private lateinit var dashboardItemsToLoad: Set<DashboardItem.Type> private var dashboardItemsToLoad = emptySet<DashboardItem.Type>()
private var dashboardTilesToLoad: Set<DashboardItem.Tile> = emptySet() private var dashboardTileLoadedList = emptySet<DashboardItem.Tile>()
private val firstLoadedItemList = mutableListOf<DashboardItem.Type>()
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -69,8 +80,10 @@ class DashboardPresenter @Inject constructor(
} }
fun onDragAndDropEnd(list: List<DashboardItem>) { fun onDragAndDropEnd(list: List<DashboardItem>) {
dashboardItemLoadedList.clear() with(dashboardItemLoadedList) {
dashboardItemLoadedList.addAll(list) clear()
addAll(list)
}
val positionList = val positionList =
list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap() list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap()
@ -78,87 +91,102 @@ class DashboardPresenter @Inject constructor(
preferencesRepository.dashboardItemsPosition = positionList preferencesRepository.dashboardItemsPosition = positionList
} }
fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set<DashboardItem.Tile>) { fun loadData(
val oldDashboardDataToLoad = dashboardTilesToLoad tilesToLoad: Set<DashboardItem.Tile>,
forceRefresh: Boolean = false,
) {
val oldDashboardTileLoadedList = dashboardTileLoadedList
dashboardItemsToLoad = tilesToLoad.map { it.toDashboardItemType() }.toSet()
dashboardTileLoadedList = tilesToLoad
dashboardTilesToLoad = tilesToLoad val itemsToLoad = generateDashboardTileListToLoad(
dashboardItemsToLoad = dashboardTilesToLoad.map { it.toDashboardItemType() }.toSet() dashboardTilesToLoad = tilesToLoad,
dashboardLoadedTiles = oldDashboardTileLoadedList,
forceRefresh = forceRefresh
).map { it.toDashboardItemType() }
removeUnselectedTiles() removeUnselectedTiles(tilesToLoad.toList())
loadTiles(tileList = itemsToLoad, forceRefresh = forceRefresh)
val newTileList = generateTileListToLoad(oldDashboardDataToLoad, forceRefresh)
loadTiles(forceRefresh, newTileList)
} }
private fun removeUnselectedTiles() { private fun generateDashboardTileListToLoad(
val isLuckyNumberToLoad = dashboardTilesToLoad: Set<DashboardItem.Tile>,
dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER } dashboardLoadedTiles: Set<DashboardItem.Tile>,
val isMessagesToLoad = forceRefresh: Boolean
dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES } ) = dashboardTilesToLoad.filter { newItemToLoad ->
val isAttendanceToLoad = dashboardLoadedTiles.none { it == newItemToLoad } || forceRefresh
dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE } }
private fun removeUnselectedTiles(tilesToLoad: List<DashboardItem.Tile>) {
dashboardItemLoadedList.removeAll { loadedTile -> dashboardItemsToLoad.none { it == loadedTile.type } } dashboardItemLoadedList.removeAll { loadedTile -> dashboardItemsToLoad.none { it == loadedTile.type } }
val horizontalGroup = val horizontalGroup =
dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup? dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup?
if (horizontalGroup != null) { if (horizontalGroup != null) {
val horizontalIndex = dashboardItemLoadedList.indexOf(horizontalGroup) val isLuckyNumberToLoad = DashboardItem.Tile.LUCKY_NUMBER in tilesToLoad
dashboardItemLoadedList.remove(horizontalGroup) val isMessagesToLoad = DashboardItem.Tile.MESSAGES in tilesToLoad
val isAttendanceToLoad = DashboardItem.Tile.ATTENDANCE in tilesToLoad
var updatedHorizontalGroup = horizontalGroup val horizontalGroupIndex = dashboardItemLoadedList.indexOf(horizontalGroup)
if (horizontalGroup.luckyNumber != null && !isLuckyNumberToLoad) { val newHorizontalGroup = horizontalGroup.copy(
updatedHorizontalGroup = updatedHorizontalGroup.copy(luckyNumber = null) attendancePercentage = horizontalGroup.attendancePercentage.takeIf { isAttendanceToLoad },
unreadMessagesCount = horizontalGroup.unreadMessagesCount.takeIf { isMessagesToLoad },
luckyNumber = horizontalGroup.luckyNumber.takeIf { isLuckyNumberToLoad }
)
with(dashboardItemLoadedList) {
removeAt(horizontalGroupIndex)
add(horizontalGroupIndex, newHorizontalGroup)
} }
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) view?.updateData(dashboardItemLoadedList)
} }
private fun loadTiles(forceRefresh: Boolean, tileList: List<DashboardItem.Tile>) { private fun loadTiles(
tileList: List<DashboardItem.Type>,
forceRefresh: Boolean
) {
launch {
Timber.i("Loading dashboard account data started")
val student = runCatching { studentRepository.getCurrentStudent(true) }
.onFailure {
Timber.i("Loading dashboard account result: An exception occurred")
errorHandler.dispatch(it)
updateData(DashboardItem.Account(error = it), forceRefresh)
}
.onSuccess { Timber.i("Loading dashboard account result: Success") }
.getOrNull() ?: return@launch
tileList.forEach { tileList.forEach {
when (it) { when (it) {
DashboardItem.Tile.ACCOUNT -> loadCurrentAccount(forceRefresh) DashboardItem.Type.ACCOUNT -> {
DashboardItem.Tile.LUCKY_NUMBER -> loadLuckyNumber(forceRefresh) updateData(DashboardItem.Account(student), forceRefresh)
DashboardItem.Tile.MESSAGES -> loadMessages(forceRefresh) }
DashboardItem.Tile.ATTENDANCE -> loadAttendance(forceRefresh) DashboardItem.Type.HORIZONTAL_GROUP -> {
DashboardItem.Tile.LESSONS -> loadLessons(forceRefresh) loadHorizontalGroup(student, forceRefresh)
DashboardItem.Tile.GRADES -> loadGrades(forceRefresh) }
DashboardItem.Tile.HOMEWORK -> loadHomework(forceRefresh) DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Tile.ANNOUNCEMENTS -> loadSchoolAnnouncements(forceRefresh) DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Tile.EXAMS -> loadExams(forceRefresh) DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Tile.CONFERENCES -> loadConferences(forceRefresh) DashboardItem.Type.ANNOUNCEMENTS -> {
DashboardItem.Tile.ADS -> TODO() loadSchoolAnnouncements(student, forceRefresh)
}
DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh)
DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh)
}
DashboardItem.Type.ADS -> TODO()
} }
} }
} }
private fun generateTileListToLoad(
oldDashboardTileToLoad: Set<DashboardItem.Tile>,
forceRefresh: Boolean
) = dashboardTilesToLoad.filter { newTileToLoad ->
oldDashboardTileToLoad.none { it == newTileToLoad } || forceRefresh
} }
fun onSwipeRefresh() { fun onSwipeRefresh() {
Timber.i("Force refreshing the dashboard") Timber.i("Force refreshing the dashboard")
loadData(true, preferencesRepository.selectedDashboardTiles) loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
} }
fun onRetry() { fun onRetry() {
@ -166,7 +194,7 @@ class DashboardPresenter @Inject constructor(
showErrorView(false) showErrorView(false)
showProgress(true) showProgress(true)
} }
loadData(true, preferencesRepository.selectedDashboardTiles) loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
} }
fun onViewReselected() { fun onViewReselected() {
@ -192,139 +220,86 @@ class DashboardPresenter @Inject constructor(
}.toSet() }.toSet()
} }
private fun loadCurrentAccount(forceRefresh: Boolean) { private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flowWithResource { studentRepository.getCurrentStudent(false) } flow {
val semester = semesterRepository.getCurrentSemester(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles
val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh)
.map {
if (it.data == null) {
it.copy(data = LuckyNumber(0, LocalDate.now(), 0))
} else it
}
.takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowOf(null)
val messageFLow = messageRepository.getMessages(
student = student,
semester = semester,
folder = MessageFolder.RECEIVED,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowOf(null)
val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = -1,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowOf(null)
emitAll(
combine(
luckyNumberFlow,
messageFLow,
attendanceFlow
) { luckyNumberResource, messageResource, attendanceResource ->
val error =
luckyNumberResource?.error ?: messageResource?.error ?: attendanceResource?.error
error?.let { throw it }
val luckyNumber = luckyNumberResource?.data?.luckyNumber
val messageCount = messageResource?.data?.count { it.unread }
val attendancePercentage = attendanceResource?.data?.calculatePercentage()
val isLoading =
luckyNumberResource?.status == Status.LOADING || messageResource?.status == Status.LOADING || attendanceResource?.status == Status.LOADING
DashboardItem.HorizontalGroup(
isLoading = isLoading,
attendancePercentage = if (attendancePercentage == 0.0 && isLoading) -1.0 else attendancePercentage,
unreadMessagesCount = if (messageCount == 0 && isLoading) -1 else messageCount,
luckyNumber = if (luckyNumber == 0 && isLoading) -1 else luckyNumber
)
})
}
.filterNot { it.isLoading && forceRefresh }
.distinctUntilChanged()
.onEach { .onEach {
when (it.status) { updateData(it, forceRefresh)
Status.LOADING -> {
Timber.i("Loading dashboard account data started") if (it.isLoading) {
if (forceRefresh) return@onEach Timber.i("Loading horizontal group data started")
updateData(DashboardItem.Account(it.data, isLoading = true), forceRefresh)
if (it.isFullDataLoaded) {
firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP
} }
Status.SUCCESS -> { } else {
Timber.i("Loading dashboard account result: Success") Timber.i("Loading horizontal group 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)
} }
} }
.catch {
Timber.i("Loading horizontal group result: An exception occurred")
updateData(
DashboardItem.HorizontalGroup(error = it),
forceRefresh,
)
errorHandler.dispatch(it)
} }
.launch("dashboard_account") .launch("horizontal_group")
} }
private fun loadLuckyNumber(forceRefresh: Boolean) { private fun loadGrades(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { 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) val semester = semesterRepository.getCurrentSemester(student)
gradeRepository.getGrades(student, semester, forceRefresh) gradeRepository.getGrades(student, semester, forceRefresh)
@ -353,6 +328,7 @@ class DashboardPresenter @Inject constructor(
Status.LOADING -> { Status.LOADING -> {
Timber.i("Loading dashboard grades data started") Timber.i("Loading dashboard grades data started")
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData( updateData(
DashboardItem.Grades( DashboardItem.Grades(
subjectWithGrades = it.data, subjectWithGrades = it.data,
@ -360,6 +336,10 @@ class DashboardPresenter @Inject constructor(
isLoading = true isLoading = true
), forceRefresh ), forceRefresh
) )
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.GRADES
}
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading dashboard grades result: Success") Timber.i("Loading dashboard grades result: Success")
@ -367,7 +347,8 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Grades( DashboardItem.Grades(
subjectWithGrades = it.data, subjectWithGrades = it.data,
gradeTheme = preferencesRepository.gradeColorTheme gradeTheme = preferencesRepository.gradeColorTheme
), forceRefresh ),
forceRefresh
) )
} }
Status.ERROR -> { Status.ERROR -> {
@ -379,9 +360,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_grades") }.launch("dashboard_grades")
} }
private fun loadLessons(forceRefresh: Boolean) { private fun loadLessons(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay val date = LocalDate.now().nextOrSameSchoolDay
@ -398,24 +378,34 @@ class DashboardPresenter @Inject constructor(
Status.LOADING -> { Status.LOADING -> {
Timber.i("Loading dashboard lessons data started") Timber.i("Loading dashboard lessons data started")
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData(DashboardItem.Lessons(it.data, isLoading = true), forceRefresh) updateData(
DashboardItem.Lessons(it.data, isLoading = true),
forceRefresh
)
if (!it.data?.lessons.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.LESSONS
}
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading dashboard lessons result: Success") Timber.i("Loading dashboard lessons result: Success")
updateData(DashboardItem.Lessons(it.data), forceRefresh) updateData(
DashboardItem.Lessons(it.data), forceRefresh
)
} }
Status.ERROR -> { Status.ERROR -> {
Timber.i("Loading dashboard lessons result: An exception occurred") Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error!!) errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Lessons(error = it.error), forceRefresh) updateData(
DashboardItem.Lessons(error = it.error), forceRefresh
)
} }
} }
}.launch("dashboard_lessons") }.launch("dashboard_lessons")
} }
private fun loadHomework(forceRefresh: Boolean) { private fun loadHomework(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay val date = LocalDate.now().nextOrSameSchoolDay
@ -443,6 +433,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Homework(it.data ?: emptyList(), isLoading = true), DashboardItem.Homework(it.data ?: emptyList(), isLoading = true),
forceRefresh forceRefresh
) )
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.HOMEWORK
}
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading dashboard homework result: Success") Timber.i("Loading dashboard homework result: Success")
@ -457,10 +451,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_homework") }.launch("dashboard_homework")
} }
private fun loadSchoolAnnouncements(forceRefresh: Boolean) { private fun loadSchoolAnnouncements(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh) schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh)
}.onEach { }.onEach {
when (it.status) { when (it.status) {
@ -468,11 +460,13 @@ class DashboardPresenter @Inject constructor(
Timber.i("Loading dashboard announcements data started") Timber.i("Loading dashboard announcements data started")
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData( updateData(
DashboardItem.Announcements( DashboardItem.Announcements(it.data ?: emptyList(), isLoading = true),
it.data ?: emptyList(), forceRefresh
isLoading = true
), forceRefresh
) )
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
}
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading dashboard announcements result: Success") Timber.i("Loading dashboard announcements result: Success")
@ -487,9 +481,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_announcements") }.launch("dashboard_announcements")
} }
private fun loadExams(forceRefresh: Boolean) { private fun loadExams(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
examRepository.getExams( examRepository.getExams(
@ -508,6 +501,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Exams(it.data.orEmpty(), isLoading = true), DashboardItem.Exams(it.data.orEmpty(), isLoading = true),
forceRefresh forceRefresh
) )
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.EXAMS
}
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading dashboard exams result: Success") Timber.i("Loading dashboard exams result: Success")
@ -522,9 +519,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_exams") }.launch("dashboard_exams")
} }
private fun loadConferences(forceRefresh: Boolean) { private fun loadConferences(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
conferenceRepository.getConferences( conferenceRepository.getConferences(
@ -542,6 +538,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true), DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true),
forceRefresh forceRefresh
) )
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.CONFERENCES
}
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading dashboard conferences result: Success") Timber.i("Loading dashboard conferences result: Success")
@ -556,145 +556,119 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_conferences") }.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) { private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null val isForceRefreshError = forceRefresh && dashboardItem.error != null
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition val isFirstRunDataLoadedError =
dashboardItem.type in firstLoadedItemList && dashboardItem.error != null
with(dashboardItemLoadedList) { with(dashboardItemLoadedList) {
removeAll { it.type == dashboardItem.type && !isForceRefreshError } removeAll { it.type == dashboardItem.type && !isForceRefreshError && !isFirstRunDataLoadedError }
if (!isForceRefreshError) add(dashboardItem) if (!isForceRefreshError && !isFirstRunDataLoadedError) add(dashboardItem)
sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal }
} }
sortDashboardItems()
if (forceRefresh) { if (forceRefresh) {
updateForceRefreshData(dashboardItem)
} else {
updateNormalData()
}
}
private fun updateNormalData() {
val isItemsLoaded =
dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } }
val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all {
it.isDataLoaded || it.error != null
}
if (isItemsDataLoaded) {
view?.run {
showProgress(false)
showErrorView(false)
showContent(true)
updateData(dashboardItemLoadedList.toList())
}
}
showErrorIfExists(
isItemsLoaded = isItemsLoaded,
itemsLoadedList = dashboardItemLoadedList,
forceRefresh = false
)
}
private fun updateForceRefreshData(dashboardItem: DashboardItem) {
with(dashboardItemRefreshLoadedList) { with(dashboardItemRefreshLoadedList) {
removeAll { it.type == dashboardItem.type } removeAll { it.type == dashboardItem.type }
add(dashboardItem) add(dashboardItem)
} }
val isRefreshItemLoaded =
dashboardItemsToLoad.all { type -> dashboardItemRefreshLoadedList.any { it.type == type } }
val isRefreshItemsDataLoaded = isRefreshItemLoaded && dashboardItemRefreshLoadedList.all {
it.isDataLoaded || it.error != null
} }
if (isRefreshItemsDataLoaded) {
view?.run {
showRefresh(false)
showErrorView(false)
showContent(true)
updateData(dashboardItemLoadedList.toList())
}
}
showErrorIfExists(
isItemsLoaded = isRefreshItemLoaded,
itemsLoadedList = dashboardItemRefreshLoadedList,
forceRefresh = true
)
if (isRefreshItemsDataLoaded) dashboardItemRefreshLoadedList.clear()
}
private fun showErrorIfExists(
isItemsLoaded: Boolean,
itemsLoadedList: List<DashboardItem>,
forceRefresh: Boolean
) {
val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val isAccountItemError =
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError =
filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError
val errorMessage = itemsLoadedList.map { it.error?.stackTraceToString() }.toString()
val filteredOriginalLoadedList =
dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val wasAccountItemError =
dashboardItemLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val wasGeneralError =
filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError
if (isGeneralError && isItemsLoaded) {
lastError = Exception(errorMessage)
view?.run {
showProgress(false)
showRefresh(false)
if ((forceRefresh && wasGeneralError) || !forceRefresh) {
showContent(false)
showErrorView(true)
}
}
}
}
private fun sortDashboardItems() {
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
dashboardItemLoadedList.sortBy { tile -> dashboardItemLoadedList.sortBy { tile ->
dashboardItemsPosition?.getOrDefault( dashboardItemsPosition?.getOrDefault(
tile.type, tile.type,
tile.type.ordinal + 100 tile.type.ordinal + 100
) ?: tile.type.ordinal ) ?: 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)
}
}
} }
} }

View File

@ -2,7 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
@ -14,6 +14,8 @@ class SchoolAnnouncementAdapter @Inject constructor() :
var items = emptyList<SchoolAnnouncement>() var items = emptyList<SchoolAnnouncement>()
var onItemClickListener: (SchoolAnnouncement) -> Unit = {}
override fun getItemCount() = items.size override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
@ -26,9 +28,9 @@ class SchoolAnnouncementAdapter @Inject constructor() :
with(holder.binding) { with(holder.binding) {
schoolAnnouncementItemDate.text = item.date.toFormattedString() schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = HtmlCompat.fromHtml( schoolAnnouncementItemContent.text = item.content.parseAsHtml()
item.content, HtmlCompat.FROM_HTML_MODE_COMPACT
) root.setOnClickListener { onItemClickListener(item) }
} }
} }

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.schoolannouncement
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
class SchoolAnnouncementDialog : DialogFragment() {
private var binding: DialogSchoolAnnouncementBinding by lifecycleAwareVariable()
private lateinit var announcement: SchoolAnnouncement
companion object {
private const val ARGUMENT_KEY = "item"
fun newInstance(exam: SchoolAnnouncement) = SchoolAnnouncementDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
announcement = getSerializable(ARGUMENT_KEY) as SchoolAnnouncement
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
announcementDialogSubjectValue.text = announcement.subject
announcementDialogDateValue.text = announcement.date.toFormattedString()
announcementDialogDescriptionValue.text = announcement.content.parseAsHtml()
announcementDialogClose.setOnClickListener { dismiss() }
}
}
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.FragmentSchoolAnnouncementBinding import io.github.wulkanowy.databinding.FragmentSchoolAnnouncementBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
@ -43,7 +44,9 @@ class SchoolAnnouncementFragment :
override fun initView() { override fun initView() {
with(binding.directorInformationRecycler) { with(binding.directorInformationRecycler) {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = schoolAnnouncementAdapter adapter = schoolAnnouncementAdapter.apply {
onItemClickListener = presenter::onItemClickListener
}
addItemDecoration(DividerItemDecoration(context)) addItemDecoration(DividerItemDecoration(context))
} }
with(binding) { with(binding) {
@ -99,6 +102,10 @@ class SchoolAnnouncementFragment :
binding.directorInformationSwipe.isRefreshing = show binding.directorInformationSwipe.isRefreshing = show
} }
override fun openSchoolAnnouncementDialog(item: SchoolAnnouncement) {
(activity as? MainActivity)?.showDialogFragment(SchoolAnnouncementDialog.newInstance(item))
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.schoolannouncement package io.github.wulkanowy.ui.modules.schoolannouncement
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -46,6 +47,10 @@ class SchoolAnnouncementPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError) view?.showErrorDetailsDialog(lastError)
} }
fun onItemClickListener(item: SchoolAnnouncement) {
view?.openSchoolAnnouncementDialog(item)
}
private fun loadData(forceRefresh: Boolean = false) { private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading School announcement data started") Timber.i("Loading School announcement data started")

View File

@ -19,6 +19,8 @@ interface SchoolAnnouncementView : BaseView {
fun setErrorDetails(message: String) fun setErrorDetails(message: String)
fun openSchoolAnnouncementDialog(item: SchoolAnnouncement)
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun enableSwipe(enable: Boolean) fun enableSwipe(enable: Boolean)

View File

@ -13,7 +13,7 @@ class StudentInfoAdapter @Inject constructor() :
var items = listOf<StudentInfoItem>() var items = listOf<StudentInfoItem>()
var onItemClickListener: (position: Int) -> Unit = {} var onItemClickListener: (StudentInfoView.Type?) -> Unit = {}
var onItemLongClickListener: (text: String) -> Unit = {} var onItemLongClickListener: (text: String) -> Unit = {}
@ -32,7 +32,7 @@ class StudentInfoAdapter @Inject constructor() :
studentInfoItemArrow.visibility = if (item.showArrow) VISIBLE else GONE studentInfoItemArrow.visibility = if (item.showArrow) VISIBLE else GONE
with(root) { with(root) {
setOnClickListener { onItemClickListener(position) } setOnClickListener { onItemClickListener(item.viewType) }
setOnLongClickListener { setOnLongClickListener {
onItemLongClickListener(studentInfoItemSubtitle.text.toString()) onItemLongClickListener(studentInfoItemSubtitle.text.toString())
true true

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.studentinfo package io.github.wulkanowy.ui.modules.studentinfo
import android.annotation.SuppressLint
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.os.Bundle import android.os.Bundle
@ -130,9 +129,9 @@ class StudentInfoFragment :
getString(R.string.student_info_parents_name) to studentInfo.parentsNames getString(R.string.student_info_parents_name) to studentInfo.parentsNames
).map { ).map {
StudentInfoItem( StudentInfoItem(
it.first, title = it.first,
it.second.ifBlank { getString(R.string.all_no_data) }, subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
false, showArrow = false,
) )
} }
) )
@ -146,25 +145,33 @@ class StudentInfoFragment :
getString(R.string.student_info_email) to studentInfo.email getString(R.string.student_info_email) to studentInfo.email
).map { ).map {
StudentInfoItem( StudentInfoItem(
it.first, title = it.first,
it.second.ifBlank { getString(R.string.all_no_data) }, subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
false, showArrow = false,
) )
} }
) )
} }
@SuppressLint("DefaultLocale") @OptIn(ExperimentalStdlibApi::class)
override fun showFamilyTypeData(studentInfo: StudentInfo) { override fun showFamilyTypeData(studentInfo: StudentInfo) {
val items = buildList {
add(studentInfo.firstGuardian?.let {
Triple(it.kinship.capitalise(), it.fullName, StudentInfoView.Type.FIRST_GUARDIAN)
})
add(studentInfo.secondGuardian?.let {
Triple(it.kinship.capitalise(), it.fullName, StudentInfoView.Type.SECOND_GUARDIAN)
})
}.filterNotNull()
updateData( updateData(
listOfNotNull( items.map { (title, value, type) ->
studentInfo.firstGuardian?.let { it.kinship.capitalise() to it.fullName },
studentInfo.secondGuardian?.let { it.kinship.capitalise() to it.fullName },
).map { (title, value) ->
StudentInfoItem( StudentInfoItem(
title.ifBlank { getString(R.string.all_no_data) }, title = title.ifBlank { getString(R.string.all_no_data) },
value.ifBlank { getString(R.string.all_no_data) }, subtitle = value.ifBlank { getString(R.string.all_no_data) },
true, showArrow = true,
viewType = type,
) )
} }
) )
@ -178,15 +185,15 @@ class StudentInfoFragment :
getString(R.string.student_info_correspondence_address) to studentInfo.correspondenceAddress getString(R.string.student_info_correspondence_address) to studentInfo.correspondenceAddress
).map { ).map {
StudentInfoItem( StudentInfoItem(
it.first, title = it.first,
it.second.ifBlank { getString(R.string.all_no_data) }, subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
false, showArrow = false,
) )
} }
) )
} }
override fun showFirstGuardianTypeData(studentGuardian: StudentGuardian) { override fun showGuardianTypeData(studentGuardian: StudentGuardian) {
updateData( updateData(
listOf( listOf(
getString(R.string.student_info_full_name) to studentGuardian.fullName, getString(R.string.student_info_full_name) to studentGuardian.fullName,
@ -196,27 +203,9 @@ class StudentInfoFragment :
getString(R.string.student_info_email) to studentGuardian.email getString(R.string.student_info_email) to studentGuardian.email
).map { ).map {
StudentInfoItem( StudentInfoItem(
it.first, title = it.first,
it.second.ifBlank { getString(R.string.all_no_data) }, subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
false, showArrow = false,
)
}
)
}
override fun showSecondGuardianTypeData(studentGuardian: StudentGuardian) {
updateData(
listOf(
getString(R.string.student_info_full_name) to studentGuardian.fullName,
getString(R.string.student_info_kinship) to studentGuardian.kinship,
getString(R.string.student_info_guardian_address) to studentGuardian.address,
getString(R.string.student_info_phones) to studentGuardian.phones,
getString(R.string.student_info_email) to studentGuardian.email
).map {
StudentInfoItem(
it.first,
it.second.ifBlank { getString(R.string.all_no_data) },
false,
) )
} }
) )

View File

@ -3,5 +3,6 @@ package io.github.wulkanowy.ui.modules.studentinfo
data class StudentInfoItem( data class StudentInfoItem(
val title: String, val title: String,
val subtitle: String, val subtitle: String,
val showArrow: Boolean val showArrow: Boolean,
val viewType: StudentInfoView.Type? = null,
) )

View File

@ -58,13 +58,12 @@ class StudentInfoPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError) view?.showErrorDetailsDialog(lastError)
} }
fun onItemSelected(position: Int) { fun onItemSelected(viewType: StudentInfoView.Type?) {
if (infoType != StudentInfoView.Type.FAMILY) return viewType ?: return
view?.openStudentInfoView( view?.openStudentInfoView(
if (position == 0) StudentInfoView.Type.FIRST_GUARDIAN studentWithSemesters = studentWithSemesters,
else StudentInfoView.Type.SECOND_GUARDIAN, infoType = viewType,
studentWithSemesters
) )
} }
@ -76,15 +75,19 @@ class StudentInfoPresenter @Inject constructor(
flowWithResourceIn { flowWithResourceIn {
val semester = studentWithSemesters.semesters.getCurrentOrLast() val semester = studentWithSemesters.semesters.getCurrentOrLast()
studentInfoRepository.getStudentInfo( studentInfoRepository.getStudentInfo(
studentWithSemesters.student, student = studentWithSemesters.student,
semester, semester = semester,
forceRefresh forceRefresh = forceRefresh
) )
}.onEach { }.onEach {
when (it.status) { when (it.status) {
Status.LOADING -> Timber.i("Loading student info $infoType started") Status.LOADING -> Timber.i("Loading student info $infoType started")
Status.SUCCESS -> { Status.SUCCESS -> {
if (it.data != null && !(infoType == StudentInfoView.Type.FAMILY && it.data.firstGuardian == null && it.data.secondGuardian == null)) { val isFamily = infoType == StudentInfoView.Type.FAMILY
val isFirstGuardianEmpty = it.data?.firstGuardian == null
val isSecondGuardianEmpty = it.data?.secondGuardian == null
if (it.data != null && !(isFamily && isFirstGuardianEmpty && isSecondGuardianEmpty)) {
Timber.i("Loading student info $infoType result: Success") Timber.i("Loading student info $infoType result: Success")
showCorrectData(it.data) showCorrectData(it.data)
view?.run { view?.run {
@ -122,8 +125,8 @@ class StudentInfoPresenter @Inject constructor(
StudentInfoView.Type.CONTACT -> view?.showContactTypeData(studentInfo) StudentInfoView.Type.CONTACT -> view?.showContactTypeData(studentInfo)
StudentInfoView.Type.ADDRESS -> view?.showAddressTypeData(studentInfo) StudentInfoView.Type.ADDRESS -> view?.showAddressTypeData(studentInfo)
StudentInfoView.Type.FAMILY -> view?.showFamilyTypeData(studentInfo) StudentInfoView.Type.FAMILY -> view?.showFamilyTypeData(studentInfo)
StudentInfoView.Type.SECOND_GUARDIAN -> view?.showSecondGuardianTypeData(studentInfo.secondGuardian!!) StudentInfoView.Type.SECOND_GUARDIAN -> view?.showGuardianTypeData(studentInfo.secondGuardian!!)
StudentInfoView.Type.FIRST_GUARDIAN -> view?.showFirstGuardianTypeData(studentInfo.firstGuardian!!) StudentInfoView.Type.FIRST_GUARDIAN -> view?.showGuardianTypeData(studentInfo.firstGuardian!!)
} }
} }

View File

@ -25,9 +25,7 @@ interface StudentInfoView : BaseView {
fun showFamilyTypeData(studentInfo: StudentInfo) fun showFamilyTypeData(studentInfo: StudentInfo)
fun showFirstGuardianTypeData(studentGuardian: StudentGuardian) fun showGuardianTypeData(studentGuardian: StudentGuardian)
fun showSecondGuardianTypeData(studentGuardian: StudentGuardian)
fun openStudentInfoView(infoType: Type, studentWithSemesters: StudentWithSemesters) fun openStudentInfoView(infoType: Type, studentWithSemesters: StudentWithSemesters)

View File

@ -7,7 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
@ -49,7 +49,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override val titleStringId get() = R.string.timetable_title override val titleStringId get() = R.string.timetable_title
override val isViewEmpty get() = timetableAdapter.itemCount > 0 override val isViewEmpty get() = timetableAdapter.itemCount == 0
override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
@ -147,9 +147,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override fun setDayHeaderMessage(message: String?) { override fun setDayHeaderMessage(message: String?) {
binding.timetableEmptyMessage.visibility = if (message.isNullOrEmpty()) GONE else VISIBLE binding.timetableEmptyMessage.visibility = if (message.isNullOrEmpty()) GONE else VISIBLE
binding.timetableEmptyMessage.text = HtmlCompat.fromHtml( binding.timetableEmptyMessage.text = message.orEmpty().parseAsHtml()
message.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT
)
} }
override fun showErrorView(show: Boolean) { override fun showErrorView(show: Boolean) {

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
@ -58,8 +59,11 @@ fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: I
fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) {
Intent.parseUri(uri, 0).let { Intent.parseUri(uri, 0).let {
if (it.resolveActivity(packageManager) != null) startActivity(it) try {
else onActivityNotFound(uri) startActivity(it)
} catch (e: ActivityNotFoundException) {
onActivityNotFound(uri)
}
} }
} }

View File

@ -18,7 +18,7 @@ inline val Timetable.left: Duration?
get() = when { get() = when {
canceled -> null canceled -> null
!isStudentPlan -> null !isStudentPlan -> null
end.isAfter(now()) && start.isBefore(now()) -> between(now(), end) end >= now() && start <= now() -> between(now(), end)
else -> null else -> null
} }

View File

@ -1,10 +1,7 @@
Wersja 1.2.0 Wersja 1.2.1
- dodaliśmy nowy ekran startowy 🎉 - dodaliśmy brakujące okienka z podglądem szczegółów ogłoszeń szkolnych
- usprawniliśmy powiadomienia - naprawiliśmy rzucające się w oczy błędy na ekranie startowym
- dodaliśmy wersje robocze, filtrowanie oraz informację o odczytaniu przez odbiorcę w wiadomościach - naprawiliśmy też inne drobne błędy w wyglądzie i stabilności aplikacji
- dodaliśmy informacje o liczeniu średniej w podsumowaniu ocen
- dodaliśmy opcję generowania wiadomości z usprawiedliwieniem dni w szkołach pozbawionych funkcji usprawiedliwiania przez zakładkę frekwencja
- oraz wiele wiele innych ulepszeń i poprawek
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="8dp">
<View
android:layout_width="280dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/allDetailsHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="@string/all_details"
android:textSize="21sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/announcementDialogSubjectTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="24dp"
android:text="@string/all_subject"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/allDetailsHeader" />
<TextView
android:id="@+id/announcementDialogSubjectValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/announcementDialogSubjectTitle" />
<TextView
android:id="@+id/announcementDialogDateTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/exam_entry_date"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/announcementDialogSubjectValue" />
<TextView
android:id="@+id/announcementDialogDateValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/announcementDialogDateTitle" />
<TextView
android:id="@+id/announcementDialogDescriptionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/all_description"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/announcementDialogDateValue" />
<TextView
android:id="@+id/announcementDialogDescriptionValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/announcementDialogDescriptionTitle"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
android:id="@+id/announcementDialogClose"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="88dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/announcementDialogDescriptionValue" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -36,7 +36,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -44,24 +44,44 @@
android:id="@+id/gradeStatisticsProgress" android:id="@+id/gradeStatisticsProgress"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" /> tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/gradeStatisticsRecycler" android:id="@+id/gradeStatisticsRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
tools:listitem="@layout/item_grade_statistics_pie" /> app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_grade_statistics_pie"
tools:visibility="visible" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/gradeStatisticsRecycler">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:id="@+id/gradeStatisticsEmpty" android:id="@+id/gradeStatisticsEmpty"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:visibility="invisible" android:visibility="invisible"
tools:ignore="UseCompoundDrawables"> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UseCompoundDrawables"
tools:visibility="gone">
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"
@ -82,12 +102,14 @@
<LinearLayout <LinearLayout
android:id="@+id/gradeStatisticsError" android:id="@+id/gradeStatisticsError"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:visibility="invisible" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UseCompoundDrawables" tools:ignore="UseCompoundDrawables"
tools:visibility="invisible"> tools:visibility="gone">
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"
@ -129,6 +151,8 @@
android:text="@string/all_retry" /> android:text="@string/all_retry" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</FrameLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout> </LinearLayout>

View File

@ -4,14 +4,14 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:layout_marginVertical="2dp" android:layout_marginVertical="2dp"
android:clipToPadding="false"> android:clipToPadding="false"
android:paddingHorizontal="12dp">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_horizontal_group_item_lucky_container" android:id="@+id/dashboard_horizontal_group_item_lucky_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="44dp"
android:layout_marginVertical="4dp" android:layout_marginVertical="4dp"
app:cardElevation="4dp" app:cardElevation="4dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -62,7 +62,7 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_horizontal_group_item_message_container" android:id="@+id/dashboard_horizontal_group_item_message_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="44dp"
android:layout_marginVertical="4dp" android:layout_marginVertical="4dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
app:cardElevation="4dp" app:cardElevation="4dp"
@ -119,7 +119,7 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_horizontal_group_item_attendance_container" android:id="@+id/dashboard_horizontal_group_item_attendance_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="44dp"
android:layout_marginVertical="4dp" android:layout_marginVertical="4dp"
app:cardElevation="4dp" app:cardElevation="4dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -169,10 +169,8 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_horizontal_group_item_info_container" android:id="@+id/dashboard_horizontal_group_item_info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="44dp"
android:layout_marginVertical="4dp" android:layout_marginVertical="4dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:visibility="gone" android:visibility="gone"
app:cardElevation="4dp" app:cardElevation="4dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@ -496,7 +496,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Lekce</string> <string name="dashboard_timetable_title">Lekce</string>
<string name="dashboard_timetable_title_tomorrow">(Zítra)</string> <string name="dashboard_timetable_title_tomorrow">(Zítra)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">Za chvíli:</string> <string name="dashboard_timetable_first_lesson_title_moment">Za chvíli:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Brzy:</string> <string name="dashboard_timetable_first_lesson_title_soon">Brzy:</string>
<string name="dashboard_timetable_first_lesson_title_first">První:</string> <string name="dashboard_timetable_first_lesson_title_first">První:</string>
@ -566,7 +565,7 @@
<item quantity="other">Ještě %1$d dalších setkání</item> <item quantity="other">Ještě %1$d dalších setkání</item>
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">Při načítání dat došlo k chybě</string> <string name="dashboard_horizontal_group_error">Při načítání dat došlo k chybě</string>
<string name="dashboard_horizontal_group_no_lukcy_number">Žádné</string> <string name="dashboard_horizontal_group_no_data">Žádné</string>
<!--Error dialog--> <!--Error dialog-->
<string name="dialog_error_check_update">Zkontrolovat aktualizace</string> <string name="dialog_error_check_update">Zkontrolovat aktualizace</string>
<string name="dialog_error_check_update_message">Před hlášením chyby zkontrolujte, zda je k dispozici aktualizace s opravou chyb</string> <string name="dialog_error_check_update_message">Před hlášením chyby zkontrolujte, zda je k dispozici aktualizace s opravou chyb</string>

View File

@ -432,7 +432,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Lektionen</string> <string name="dashboard_timetable_title">Lektionen</string>
<string name="dashboard_timetable_title_tomorrow">(Morgen)</string> <string name="dashboard_timetable_title_tomorrow">(Morgen)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">Gleich:</string> <string name="dashboard_timetable_first_lesson_title_moment">Gleich:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Bald:</string> <string name="dashboard_timetable_first_lesson_title_soon">Bald:</string>
<string name="dashboard_timetable_first_lesson_title_first">Erstens:</string> <string name="dashboard_timetable_first_lesson_title_first">Erstens:</string>
@ -488,7 +487,7 @@
<item quantity="other">%1$d weitere Konferenzen</item> <item quantity="other">%1$d weitere Konferenzen</item>
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">Fehler beim Laden der Daten</string> <string name="dashboard_horizontal_group_error">Fehler beim Laden der Daten</string>
<string name="dashboard_horizontal_group_no_lukcy_number">Keine</string> <string name="dashboard_horizontal_group_no_data">Keine</string>
<!--Error dialog--> <!--Error dialog-->
<string name="dialog_error_check_update">Auf Updates prüfen</string> <string name="dialog_error_check_update">Auf Updates prüfen</string>
<string name="dialog_error_check_update_message">Bevor Sie einen Fehler melden, prüfen Sie zuerst, ob ein Update mit der Fehlerbehebung verfügbar ist</string> <string name="dialog_error_check_update_message">Bevor Sie einen Fehler melden, prüfen Sie zuerst, ob ein Update mit der Fehlerbehebung verfügbar ist</string>

View File

@ -53,7 +53,7 @@
<string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+</string> <string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+</string>
<string name="login_field_required">To pole jest wymagane</string> <string name="login_field_required">To pole jest wymagane</string>
<string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</string> <string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</string>
<string name="login_symbol_helper">Symbol znajdziesz na stronie dziennika w&#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nUpewnij się, że w polu <b>Dziennik UONET+</b> na poprzednim ekranie została ustawiona odpowiednia odmiana dziennika. Wulkanowy na chwilę obecną nie wykrywa uczniów przedszkolnych</string> <string name="login_symbol_helper">Symbol znajdziesz na stronie dziennika w&#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nUpewnij się, że w polu <b>Dziennik UONET+</b> na poprzednim ekranie została ustawiona odpowiednia odmiana dziennika.\n\n<b>Wulkanowy na chwilę obecną nie wykrywa uczniów przedszkolnych (z zerówki)</b></string>
<string name="login_select_student">Wybierz uczniów do zalogowania w aplikacji</string> <string name="login_select_student">Wybierz uczniów do zalogowania w aplikacji</string>
<string name="login_advanced">Inne opcje</string> <string name="login_advanced">Inne opcje</string>
<string name="login_advanced_warning_mobile_api">W tym trybie nie działa szczęśliwy numerek, uczeń na tle klasy, podsumowanie frekwencji, usprawiedliwianie nieobecności, lekcje zrealizowane, informacje o szkole i podgląd listy zarejestrowanych urządzeń</string> <string name="login_advanced_warning_mobile_api">W tym trybie nie działa szczęśliwy numerek, uczeń na tle klasy, podsumowanie frekwencji, usprawiedliwianie nieobecności, lekcje zrealizowane, informacje o szkole i podgląd listy zarejestrowanych urządzeń</string>
@ -496,7 +496,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Lekcje</string> <string name="dashboard_timetable_title">Lekcje</string>
<string name="dashboard_timetable_title_tomorrow">(Jutro)</string> <string name="dashboard_timetable_title_tomorrow">(Jutro)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">Za chwilę:</string> <string name="dashboard_timetable_first_lesson_title_moment">Za chwilę:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Wkrótce:</string> <string name="dashboard_timetable_first_lesson_title_soon">Wkrótce:</string>
<string name="dashboard_timetable_first_lesson_title_first">Pierwsza:</string> <string name="dashboard_timetable_first_lesson_title_first">Pierwsza:</string>
@ -566,7 +565,7 @@
<item quantity="other">Jeszcze %1$d dodatkowych zebrań</item> <item quantity="other">Jeszcze %1$d dodatkowych zebrań</item>
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">Wystąpił błąd podczas ładowania danych</string> <string name="dashboard_horizontal_group_error">Wystąpił błąd podczas ładowania danych</string>
<string name="dashboard_horizontal_group_no_lukcy_number">Brak</string> <string name="dashboard_horizontal_group_no_data">Brak</string>
<!--Error dialog--> <!--Error dialog-->
<string name="dialog_error_check_update">Sprawdź dostępność aktualizacji</string> <string name="dialog_error_check_update">Sprawdź dostępność aktualizacji</string>
<string name="dialog_error_check_update_message">Przed zgłoszeniem błędu sprawdź wcześniej, czy dostępna jest już aktualizacja z poprawką błędu</string> <string name="dialog_error_check_update_message">Przed zgłoszeniem błędu sprawdź wcześniej, czy dostępna jest już aktualizacja z poprawką błędu</string>

View File

@ -496,7 +496,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Уроки</string> <string name="dashboard_timetable_title">Уроки</string>
<string name="dashboard_timetable_title_tomorrow">(Завтра)</string> <string name="dashboard_timetable_title_tomorrow">(Завтра)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">Сейчас:</string> <string name="dashboard_timetable_first_lesson_title_moment">Сейчас:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Скоро:</string> <string name="dashboard_timetable_first_lesson_title_soon">Скоро:</string>
<string name="dashboard_timetable_first_lesson_title_first">Первый:</string> <string name="dashboard_timetable_first_lesson_title_first">Первый:</string>
@ -566,7 +565,7 @@
<item quantity="other">Еще %1$d конференций</item> <item quantity="other">Еще %1$d конференций</item>
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">Произошла ошибка при загрузке данных</string> <string name="dashboard_horizontal_group_error">Произошла ошибка при загрузке данных</string>
<string name="dashboard_horizontal_group_no_lukcy_number">Отсутствует</string> <string name="dashboard_horizontal_group_no_data">Отсутствует</string>
<!--Error dialog--> <!--Error dialog-->
<string name="dialog_error_check_update">Проверить наличие обновлений</string> <string name="dialog_error_check_update">Проверить наличие обновлений</string>
<string name="dialog_error_check_update_message">Прежде чем сообщать об ошибке, проверьте наличие обновлений</string> <string name="dialog_error_check_update_message">Прежде чем сообщать об ошибке, проверьте наличие обновлений</string>

View File

@ -496,7 +496,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Lekcie</string> <string name="dashboard_timetable_title">Lekcie</string>
<string name="dashboard_timetable_title_tomorrow">(Zajtra)</string> <string name="dashboard_timetable_title_tomorrow">(Zajtra)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">Za chvíľu:</string> <string name="dashboard_timetable_first_lesson_title_moment">Za chvíľu:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Čoskoro:</string> <string name="dashboard_timetable_first_lesson_title_soon">Čoskoro:</string>
<string name="dashboard_timetable_first_lesson_title_first">Prvá:</string> <string name="dashboard_timetable_first_lesson_title_first">Prvá:</string>
@ -566,7 +565,7 @@
<item quantity="other">Ešte %1$d ďalších stretnutí</item> <item quantity="other">Ešte %1$d ďalších stretnutí</item>
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">Pri načítaní dát došlo k chybe</string> <string name="dashboard_horizontal_group_error">Pri načítaní dát došlo k chybe</string>
<string name="dashboard_horizontal_group_no_lukcy_number">Žiadne</string> <string name="dashboard_horizontal_group_no_data">Žiadne</string>
<!--Error dialog--> <!--Error dialog-->
<string name="dialog_error_check_update">Skontrolovať aktualizácie</string> <string name="dialog_error_check_update">Skontrolovať aktualizácie</string>
<string name="dialog_error_check_update_message">Pred hlásením chyby skontrolujte, či je k dispozícii aktualizácia s opravou chýb</string> <string name="dialog_error_check_update_message">Pred hlásením chyby skontrolujte, či je k dispozícii aktualizácia s opravou chýb</string>

View File

@ -496,7 +496,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Уроки</string> <string name="dashboard_timetable_title">Уроки</string>
<string name="dashboard_timetable_title_tomorrow">(Завтра)</string> <string name="dashboard_timetable_title_tomorrow">(Завтра)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">Через мить:</string> <string name="dashboard_timetable_first_lesson_title_moment">Через мить:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Незабаром:</string> <string name="dashboard_timetable_first_lesson_title_soon">Незабаром:</string>
<string name="dashboard_timetable_first_lesson_title_first">Перше:</string> <string name="dashboard_timetable_first_lesson_title_first">Перше:</string>
@ -566,7 +565,7 @@
<item quantity="other">%1$d більше конференцій</item> <item quantity="other">%1$d більше конференцій</item>
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">Помилка при завантаженні даних</string> <string name="dashboard_horizontal_group_error">Помилка при завантаженні даних</string>
<string name="dashboard_horizontal_group_no_lukcy_number">Нічого</string> <string name="dashboard_horizontal_group_no_data">Нічого</string>
<!--Error dialog--> <!--Error dialog-->
<string name="dialog_error_check_update">Провірити наявність оновлень</string> <string name="dialog_error_check_update">Провірити наявність оновлень</string>
<string name="dialog_error_check_update_message">Перед тим, як повідомлювати о помілці, перевірте наявність оновлень</string> <string name="dialog_error_check_update_message">Перед тим, як повідомлювати о помілці, перевірте наявність оновлень</string>

View File

@ -495,7 +495,6 @@
<!--Dashboard--> <!--Dashboard-->
<string name="dashboard_timetable_title">Lessons</string> <string name="dashboard_timetable_title">Lessons</string>
<string name="dashboard_timetable_title_tomorrow">(Tomorrow)</string> <string name="dashboard_timetable_title_tomorrow">(Tomorrow)</string>
<string name="dashboard_timetable_lesson_value">%1$s (%2$s)</string>
<string name="dashboard_timetable_first_lesson_title_moment">In a moment:</string> <string name="dashboard_timetable_first_lesson_title_moment">In a moment:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Soon:</string> <string name="dashboard_timetable_first_lesson_title_soon">Soon:</string>
<string name="dashboard_timetable_first_lesson_title_first">First:</string> <string name="dashboard_timetable_first_lesson_title_first">First:</string>
@ -557,7 +556,7 @@
</plurals> </plurals>
<string name="dashboard_horizontal_group_error">An error occurred while loading data</string> <string name="dashboard_horizontal_group_error">An error occurred while loading data</string>
<string name="dashboard_horizontal_group_no_lukcy_number">None</string> <string name="dashboard_horizontal_group_no_data">None</string>
<!--Error dialog--> <!--Error dialog-->

View File

@ -32,7 +32,22 @@ class TimetableExtensionTest {
assertEquals(null, getTimetableEntity(canceled = true).left) assertEquals(null, getTimetableEntity(canceled = true).left)
assertEquals(null, getTimetableEntity(start = now().plusMinutes(5), end = now().plusMinutes(50)).left) assertEquals(null, getTimetableEntity(start = now().plusMinutes(5), end = now().plusMinutes(50)).left)
assertEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = false).left) assertEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = false).left)
assertNotEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = true).left) assertNotEquals(
null,
getTimetableEntity(
start = now().minusMinutes(1),
end = now().plusMinutes(44),
isStudentPlan = true
).left
)
assertNotEquals(
null,
getTimetableEntity(
start = now(),
end = now().plusMinutes(45),
isStudentPlan = true
).left
)
} }
@Test @Test

View File

@ -12,13 +12,13 @@ buildscript {
} }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:7.0.1' classpath 'com.android.tools.build:gradle:7.0.2'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.huawei.agconnect:agcp:1.6.0.300' classpath 'com.huawei.agconnect:agcp:1.6.0.300'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
classpath "com.github.triplet.gradle:play-publisher:2.8.0" classpath "com.github.triplet.gradle:play-publisher:2.8.0"
classpath "ru.cian:huawei-publish-gradle-plugin:1.2.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3"
classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries"