1
0

Compare commits

...

34 Commits
1.2.0 ... 1.2.2

Author SHA1 Message Date
6a00e75816 Merge branch 'release/1.2.2' 2021-09-13 14:53:32 +02:00
957adaf6ee Version 1.2.2 2021-09-13 14:53:27 +02:00
827fb33eeb Fix login process after was interrupted (#1505) 2021-09-13 14:36:31 +02:00
19c96ee83f Unlock sunday in navigation datepicker (#1506) 2021-09-13 14:19:46 +02:00
5a7f52c773 Update help email pre-filled content (#1507) 2021-09-13 14:19:24 +02:00
dddeff802f Fix date picker crash after saved state (#1502) 2021-09-12 17:29:46 +02:00
91f6310892 Restore lucky number in more view (#1504) 2021-09-11 19:43:05 +02:00
0389642543 Fix empty list on excuse submit (#1501) 2021-09-11 19:40:09 +02:00
8528e0beff Fix crash in school info when dialer is unavailable (#1500) 2021-09-10 09:49:22 +00:00
e665a8f18b Fix error view in attendance summary (#1492) 2021-09-10 00:48:29 +02:00
6d5acbad2c Fix overlapping error view (#1493) 2021-09-10 00:36:44 +02:00
7217d0f753 Fix NPE in timetable dashboard tile (#1498) 2021-09-10 00:27:48 +02:00
16a5d88dfb Fix overlapping shadow in dashboard (#1494) 2021-09-10 00:25:23 +02:00
646a46727f Update material chips input (#1495) 2021-09-08 09:13:52 +02:00
f5e9197f98 Bump work_manager from 2.5.0 to 2.6.0 (#1478) 2021-09-06 23:38:10 +00:00
b47f26684b Change AppGallery deploy format to aab (#1483) 2021-09-06 03:27:54 +02:00
3a03b5f1c6 Merge branch 'release/1.2.1' into develop 2021-09-05 23:29:30 +02:00
3d0dcead50 Merge branch 'release/1.2.1' 2021-09-05 23:29:23 +02:00
b64b41c11c Version 1.2.1 2021-09-05 23:29:15 +02:00
77c5330f91 Dashboard fixes (#1463) 2021-09-05 23:24:03 +02:00
2b55ec02ff New translations strings.xml (Polish) (#1474) 2021-09-05 23:06:44 +02:00
49ebae6e63 Fix overlaping empty and error view in grade statistics (#1475)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2021-09-05 19:59:03 +00:00
44a9db48a6 Bump hianalytics from 6.2.0.300 to 6.2.0.301 (#1476) 2021-09-05 18:17:18 +00:00
0008a72be1 Bump kotlinx-coroutines-test from 1.5.1 to 1.5.2 (#1477) 2021-09-05 18:10:22 +00:00
a43acaaa07 Bump kotlinx-coroutines-android from 1.5.1 to 1.5.2 (#1479) 2021-09-05 18:10:05 +00:00
e6c9abb4e5 Bump core from 1.10.0 to 1.10.1 (#1480) 2021-09-05 18:09:42 +00:00
3b9451184c Fix preview of second student guardian when first guardian is null (#1473) 2021-09-05 03:15:40 +02:00
45d1727dbe Add missing school announcement dialog (#1470) 2021-09-04 15:54:37 +02:00
8d7b611c44 Fix showing error view in timetable (#1472) 2021-09-04 15:54:05 +02:00
c3adb9b6d6 Bump agp to 7.0.2 (#1469) 2021-09-03 22:54:29 +02:00
d87283eb31 Fix opening twitter link from about on android 11 (#1460) 2021-08-30 00:20:13 +02:00
d139c22782 Bump hianalytics from 6.1.1.300 to 6.2.0.300 (#1457) 2021-08-29 19:37:18 +00:00
e557021ad9 Bump huawei-publish-gradle-plugin from 1.2.4 to 1.3.0 (#1458) 2021-08-29 19:37:01 +00:00
37af5de25c Merge branch 'release/1.2.0' into develop 2021-08-29 21:08:23 +02:00
59 changed files with 965 additions and 676 deletions

View File

@ -71,4 +71,4 @@ jobs:
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew assembleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

View File

@ -21,8 +21,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 30
versionCode 93
versionName "1.2.0"
versionCode 95
versionName "1.2.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -133,7 +133,7 @@ play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'beta'
track = 'production'
updatePriority = 3
}
@ -141,14 +141,14 @@ huaweiPublish {
instances {
hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "apk"
buildFormat = "aab"
deployType = "draft"
}
}
}
ext {
work_manager = "2.5.0"
work_manager = "2.6.0"
android_hilt = "1.0.0"
room = "2.3.0"
chucker = "3.5.2"
@ -157,11 +157,11 @@ ext {
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.2.0"
implementation "io.github.wulkanowy:sdk:1.2.2"
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.activity:activity-ktx:1.3.1"
@ -177,7 +177,7 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.github.wulkanowy:material-chips-input:2.2.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0'
@ -215,10 +215,10 @@ dependencies {
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
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'
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'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -228,7 +228,7 @@ dependencies {
testImplementation "junit:junit:4.13.2"
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.robolectric:robolectric:4.6.1'

View File

@ -119,11 +119,9 @@
<receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -14,33 +14,39 @@ import javax.inject.Singleton
@Singleton
@Dao
interface StudentDao {
abstract class StudentDao {
@Insert(onConflict = ABORT)
suspend fun insertAll(student: List<Student>): List<Long>
abstract suspend fun insertAll(student: List<Student>): List<Long>
@Delete
suspend fun delete(student: Student)
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student?
abstract suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id")
suspend fun loadById(id: Long): Student?
abstract suspend fun loadById(id: Long): Student?
@Query("SELECT * FROM Students")
suspend fun loadAll(): List<Student>
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students")
suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
abstract suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
suspend fun updateCurrent(id: Long)
abstract suspend fun updateCurrent(id: Long)
@Query("UPDATE Students SET is_current = 0")
suspend fun resetCurrent()
abstract suspend fun resetCurrent()
@Transaction
open suspend fun switchCurrent(id: Long) {
resetCurrent()
updateCurrent(id)
}
}

View File

@ -33,10 +33,16 @@ class GradeRepository @Inject constructor(
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,
shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
val isShouldBeRefreshed =
refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = {
@ -59,8 +65,14 @@ class GradeRepository @Inject constructor(
}
)
private suspend fun refreshGradeDetails(student: Student, oldGrades: List<Grade>, newDetails: List<Grade>, notify: Boolean) {
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate()
private suspend fun refreshGradeDetails(
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.insertAll((newDetails uniqueSubtract oldGrades).onEach {
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.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.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false

View File

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

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
@ -25,7 +27,8 @@ class StudentRepository @Inject constructor(
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val appInfo: AppInfo
private val appInfo: AppInfo,
private val appDatabase: AppDatabase
) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -92,7 +95,7 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>) {
val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student }
.map {
@ -104,16 +107,21 @@ class StudentRepository @Inject constructor(
}
}
}
.mapIndexed { index, student ->
if (index == 0) {
student.copy(isCurrent = true).apply { avatarColor = student.avatarColor }
} else student
}
semesterDb.insertSemesters(semesters)
return studentDb.insertAll(students)
appDatabase.withTransaction {
studentDb.resetCurrent()
semesterDb.insertSemesters(semesters)
studentDb.insertAll(students)
}
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
with(studentDb) {
resetCurrent()
updateCurrent(studentWithSemesters.student.id)
}
studentDb.switchCurrent(studentWithSemesters.student.id)
}
suspend fun logoutStudent(student: Student) = studentDb.delete(student)

View File

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

View File

@ -8,7 +8,7 @@ import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
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.ui.base.BaseFragment
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) {
(activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance(studentWithSemesters)
)
override fun openAccountDetailsView(student: Student) {
(activity as? MainActivity)?.pushView(AccountDetailsFragment.newInstance(student))
}
}

View File

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

View File

@ -1,6 +1,6 @@
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
interface AccountView : BaseView {
@ -11,5 +11,5 @@ interface AccountView : BaseView {
fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun openAccountDetailsView(student: Student)
}

View File

@ -37,9 +37,9 @@ class AccountDetailsFragment :
private const val ARGUMENT_KEY = "Data"
fun newInstance(studentWithSemesters: StudentWithSemesters) =
fun newInstance(student: Student) =
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?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as StudentWithSemesters)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
}
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.Status
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
@ -27,9 +28,9 @@ class AccountDetailsPresenter @Inject constructor(
private var studentId: Long? = null
fun onAttachView(view: AccountDetailsView, studentWithSemesters: StudentWithSemesters) {
fun onAttachView(view: AccountDetailsView, student: Student) {
super.onAttachView(view)
studentId = studentWithSemesters.student.id
studentId = student.id
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError

View File

@ -245,7 +245,9 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@AttendanceFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun showExcuseDialog() {

View File

@ -152,6 +152,8 @@ class AttendancePresenter @Inject constructor(
fun onExcuseDialogSubmit(reason: String) {
view?.finishActionMode()
if (attendanceToExcuseList.isEmpty()) return
if (isVulcanExcusedFunctionEnabled) {
excuseAbsence(
reason = reason.takeIf { it.isNotBlank() },
@ -234,6 +236,7 @@ class AttendancePresenter @Inject constructor(
enableSwipe(true)
showRefresh(true)
showProgress(false)
showErrorView(false)
showEmpty(filteredAttendance.isEmpty())
showContent(filteredAttendance.isNotEmpty())
updateData(filteredAttendance.sortedBy { item -> item.number })

View File

@ -82,7 +82,13 @@ class AttendanceSummaryPresenter @Inject constructor(
flowWithResourceIn {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
attendanceSummaryRepository.getAttendanceSummary(student, semester, subjectId, forceRefresh)
attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = subjectId,
forceRefresh = forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> {
@ -92,6 +98,7 @@ class AttendanceSummaryPresenter @Inject constructor(
showRefresh(true)
showProgress(false)
showContent(true)
showErrorView(false)
updateDataSet(sortItems(it.data))
}
}
@ -99,6 +106,7 @@ class AttendanceSummaryPresenter @Inject constructor(
Status.SUCCESS -> {
Timber.i("Loading attendance summary result: Success")
view?.apply {
showErrorView(false)
showEmpty(it.data!!.isEmpty())
showContent(it.data.isNotEmpty())
updateDataSet(sortItems(it.data))

View File

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.TimetableHeader
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding
@ -30,6 +31,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.left
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
@ -41,7 +43,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private var lessonsTimer: Timer? = null
var onAccountTileClickListener: () -> Unit = {}
var onAccountTileClickListener: (Student) -> Unit = {}
var onLuckyNumberTileClickListener: () -> Unit = {}
@ -152,7 +154,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
dashboardAccountItemName.text = student?.nickOrName.orEmpty()
dashboardAccountItemSchoolName.text = student?.schoolName.orEmpty()
root.setOnClickListener { onAccountTileClickListener() }
root.setOnClickListener { student?.let(onAccountTileClickListener) }
}
}
@ -169,39 +171,43 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val isLoading = item.isLoading
val binding = horizontalGroupViewHolder.binding
val context = binding.root.context
val isLoadingVisible =
(isLoading && !item.isDataLoaded) || (isLoading && !item.isFullDataLoaded)
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)
}
attendancePercentage ?: 0.0 <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorTimetableChange)
}
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) {
text = "%.2f%%".format(attendancePercentage)
text = attendanceString
setTextColor(attendanceColor)
}
with(binding) {
dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString()
dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == -1) {
context.getString(R.string.dashboard_horizontal_group_no_lukcy_number)
dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
} else luckyNumber?.toString()
if (dashboardHorizontalGroupItemInfoContainer.isVisible != (error != null || isLoading)) {
dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading
}
if (dashboardHorizontalGroupItemInfoProgress.isVisible != isLoading) {
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoading
}
dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoadingVisible
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoadingVisible
dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null
with(dashboardHorizontalGroupItemLuckyContainer) {
isVisible = error == null && !isLoading && luckyNumber != null
isVisible = luckyNumber != null && luckyNumber != -1 && !isLoadingVisible
setOnClickListener { onLuckyNumberTileClickListener() }
updateLayoutParams<ViewGroup.MarginLayoutParams> {
@ -216,7 +222,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
with(dashboardHorizontalGroupItemAttendanceContainer) {
isVisible = error == null && !isLoading && attendancePercentage != null
isVisible =
attendancePercentage != null && attendancePercentage != -1.0 && !isLoadingVisible
updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = when {
luckyNumber == null && unreadMessagesCount == null -> 1.0f
@ -228,7 +235,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
with(dashboardHorizontalGroupItemMessageContainer) {
isVisible = error == null && !isLoading && unreadMessagesCount != null
isVisible =
unreadMessagesCount != null && unreadMessagesCount != -1 && !isLoadingVisible
setOnClickListener { onMessageTileClickListener() }
}
}
@ -291,14 +299,14 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
updateLessonView(item, currentTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowTimetable.isNotEmpty() -> {
updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
}
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
@ -348,6 +356,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
@SuppressLint("SetTextI18n")
private fun updateFirstLessonView(
binding: ItemDashboardLessonsBinding,
firstLesson: Timetable?,
@ -367,7 +376,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstLesson ?: return
val minutesToStartLesson =
Duration.between(currentDateTime, firstLesson.start).toMinutes()
Duration.between(currentDateTime, firstLesson.start).toMinutes() + 1
val isFirstTimeVisible: Boolean
val isFirstTimeRangeVisible: Boolean
val firstTimeText: String
@ -376,12 +385,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val firstTitleAndValueTextColor: Int
val firstTitleAndValueTextFont: Typeface
if (currentDateTime.isBefore(firstLesson.start)) {
if (currentDateTime < firstLesson.start) {
if (minutesToStartLesson > 60) {
val formattedStartTime = firstLesson.start.toFormattedString("HH:mm")
val formattedEndTime = firstLesson.end.toFormattedString("HH:mm")
firstTimeRangeText = "${formattedStartTime}-${formattedEndTime}"
firstTimeRangeText = "$formattedStartTime - $formattedEndTime"
firstTimeText = ""
isFirstTimeRangeVisible = true
@ -421,7 +430,10 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
} else {
val minutesToEndLesson = firstLesson.left!!.toMinutes()
val minutesToEndLesson = firstLesson.left?.toMinutes()?.plus(1) ?: run {
Timber.e(IllegalArgumentException("Lesson left is null. START ${firstLesson.start} ; END ${firstLesson.end} ; CURRENT ${LocalDateTime.now()}"))
0
}
firstTimeText = context.resources.getQuantityString(
R.plurals.dashboard_timetable_first_lesson_time_more_minutes,
@ -454,11 +466,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
with(binding.dashboardLessonsItemFirstValue) {
setTextColor(firstTitleAndValueTextColor)
typeface = firstTitleAndValueTextFont
text = context.getString(
R.string.dashboard_timetable_lesson_value,
firstLesson.subject,
firstLesson.room
)
text =
"${firstLesson.subject} ${if (firstLesson.room.isNotBlank()) "(${firstLesson.room})" else ""}"
}
}
@ -472,13 +481,11 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val formattedStartTime = secondLesson?.start?.toFormattedString("HH:mm")
val formattedEndTime = secondLesson?.end?.toFormattedString("HH:mm")
val secondTimeText = "${formattedStartTime}-${formattedEndTime}"
val secondTimeText = "$formattedStartTime - $formattedEndTime"
val secondValueText = if (secondLesson != null) {
context.getString(
R.string.dashboard_timetable_lesson_value,
secondLesson.subject,
secondLesson.room
)
val roomString = if (secondLesson.room.isNotBlank()) "(${secondLesson.room})" else ""
"${secondLesson.subject} $roomString"
} else {
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.databinding.FragmentDashboardBinding
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.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
@ -77,7 +77,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
)
dashboardAdapter.apply {
onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) }
onAccountTileClickListener = {
mainActivity.pushView(AccountDetailsFragment.newInstance(it))
}
onLuckyNumberTileClickListener = {
mainActivity.pushView(LuckyNumberFragment.newInstance())
}

View File

@ -35,6 +35,9 @@ sealed class DashboardItem(val type: Type) {
override val isDataLoaded
get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null
val isFullDataLoaded
get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1
}
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.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.repositories.AttendanceSummaryRepository
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.ErrorHandler
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.flowWithResourceIn
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.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.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime
@ -48,9 +57,11 @@ class DashboardPresenter @Inject constructor(
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
@ -69,8 +80,10 @@ class DashboardPresenter @Inject constructor(
}
fun onDragAndDropEnd(list: List<DashboardItem>) {
dashboardItemLoadedList.clear()
dashboardItemLoadedList.addAll(list)
with(dashboardItemLoadedList) {
clear()
addAll(list)
}
val positionList =
list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap()
@ -78,87 +91,102 @@ class DashboardPresenter @Inject constructor(
preferencesRepository.dashboardItemsPosition = positionList
}
fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set<DashboardItem.Tile>) {
val oldDashboardDataToLoad = dashboardTilesToLoad
fun loadData(
tilesToLoad: Set<DashboardItem.Tile>,
forceRefresh: Boolean = false,
) {
val oldDashboardTileLoadedList = dashboardTileLoadedList
dashboardItemsToLoad = tilesToLoad.map { it.toDashboardItemType() }.toSet()
dashboardTileLoadedList = tilesToLoad
dashboardTilesToLoad = tilesToLoad
dashboardItemsToLoad = dashboardTilesToLoad.map { it.toDashboardItemType() }.toSet()
val itemsToLoad = generateDashboardTileListToLoad(
dashboardTilesToLoad = tilesToLoad,
dashboardLoadedTiles = oldDashboardTileLoadedList,
forceRefresh = forceRefresh
).map { it.toDashboardItemType() }
removeUnselectedTiles()
val newTileList = generateTileListToLoad(oldDashboardDataToLoad, forceRefresh)
loadTiles(forceRefresh, newTileList)
removeUnselectedTiles(tilesToLoad.toList())
loadTiles(tileList = itemsToLoad, forceRefresh = forceRefresh)
}
private fun removeUnselectedTiles() {
val isLuckyNumberToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER }
val isMessagesToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES }
val isAttendanceToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE }
private fun generateDashboardTileListToLoad(
dashboardTilesToLoad: Set<DashboardItem.Tile>,
dashboardLoadedTiles: Set<DashboardItem.Tile>,
forceRefresh: Boolean
) = dashboardTilesToLoad.filter { newItemToLoad ->
dashboardLoadedTiles.none { it == newItemToLoad } || forceRefresh
}
private fun removeUnselectedTiles(tilesToLoad: List<DashboardItem.Tile>) {
dashboardItemLoadedList.removeAll { loadedTile -> dashboardItemsToLoad.none { it == loadedTile.type } }
val horizontalGroup =
dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup?
if (horizontalGroup != null) {
val horizontalIndex = dashboardItemLoadedList.indexOf(horizontalGroup)
dashboardItemLoadedList.remove(horizontalGroup)
val isLuckyNumberToLoad = DashboardItem.Tile.LUCKY_NUMBER in tilesToLoad
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) {
updatedHorizontalGroup = updatedHorizontalGroup.copy(luckyNumber = null)
val newHorizontalGroup = horizontalGroup.copy(
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)
}
private fun loadTiles(forceRefresh: Boolean, tileList: List<DashboardItem.Tile>) {
tileList.forEach {
when (it) {
DashboardItem.Tile.ACCOUNT -> loadCurrentAccount(forceRefresh)
DashboardItem.Tile.LUCKY_NUMBER -> loadLuckyNumber(forceRefresh)
DashboardItem.Tile.MESSAGES -> loadMessages(forceRefresh)
DashboardItem.Tile.ATTENDANCE -> loadAttendance(forceRefresh)
DashboardItem.Tile.LESSONS -> loadLessons(forceRefresh)
DashboardItem.Tile.GRADES -> loadGrades(forceRefresh)
DashboardItem.Tile.HOMEWORK -> loadHomework(forceRefresh)
DashboardItem.Tile.ANNOUNCEMENTS -> loadSchoolAnnouncements(forceRefresh)
DashboardItem.Tile.EXAMS -> loadExams(forceRefresh)
DashboardItem.Tile.CONFERENCES -> loadConferences(forceRefresh)
DashboardItem.Tile.ADS -> TODO()
private fun 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 {
when (it) {
DashboardItem.Type.ACCOUNT -> {
updateData(DashboardItem.Account(student), forceRefresh)
}
DashboardItem.Type.HORIZONTAL_GROUP -> {
loadHorizontalGroup(student, forceRefresh)
}
DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Type.ANNOUNCEMENTS -> {
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() {
Timber.i("Force refreshing the dashboard")
loadData(true, preferencesRepository.selectedDashboardTiles)
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
}
fun onRetry() {
@ -166,7 +194,7 @@ class DashboardPresenter @Inject constructor(
showErrorView(false)
showProgress(true)
}
loadData(true, preferencesRepository.selectedDashboardTiles)
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
}
fun onViewReselected() {
@ -192,139 +220,86 @@ class DashboardPresenter @Inject constructor(
}.toSet()
}
private fun loadCurrentAccount(forceRefresh: Boolean) {
flowWithResource { studentRepository.getCurrentStudent(false) }
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
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 {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard account data started")
if (forceRefresh) return@onEach
updateData(DashboardItem.Account(it.data, isLoading = true), forceRefresh)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard account result: Success")
updateData(DashboardItem.Account(it.data), forceRefresh)
}
Status.ERROR -> {
Timber.i("Loading dashboard account result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Account(error = it.error), forceRefresh)
updateData(it, forceRefresh)
if (it.isLoading) {
Timber.i("Loading horizontal group data started")
if (it.isFullDataLoaded) {
firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP
}
} else {
Timber.i("Loading horizontal group result: Success")
}
}
.launch("dashboard_account")
}
private fun loadLuckyNumber(forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
luckyNumberRepository.getLuckyNumber(student, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard lucky number data started")
if (forceRefresh) return@onEach
processHorizontalGroupData(
luckyNumber = it.data?.luckyNumber,
isLoading = true,
forceRefresh = forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard lucky number result: Success")
processHorizontalGroupData(
luckyNumber = it.data?.luckyNumber ?: -1,
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard lucky number result: An exception occurred")
errorHandler.dispatch(it.error!!)
processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh)
}
.catch {
Timber.i("Loading horizontal group result: An exception occurred")
updateData(
DashboardItem.HorizontalGroup(error = it),
forceRefresh,
)
errorHandler.dispatch(it)
}
}.launch("dashboard_lucky_number")
.launch("horizontal_group")
}
private fun loadMessages(forceRefresh: Boolean) {
private fun loadGrades(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
messageRepository.getMessages(student, semester, MessageFolder.RECEIVED, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard messages data started")
if (forceRefresh) return@onEach
val unreadMessagesCount = it.data?.count { message -> message.unread }
processHorizontalGroupData(
unreadMessagesCount = unreadMessagesCount,
isLoading = true,
forceRefresh = forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard messages result: Success")
val unreadMessagesCount = it.data?.count { message -> message.unread }
processHorizontalGroupData(
unreadMessagesCount = unreadMessagesCount,
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard messages result: An exception occurred")
errorHandler.dispatch(it.error!!)
processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh)
}
}
}.launch("dashboard_messages")
}
private fun loadAttendance(forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
attendanceSummaryRepository.getAttendanceSummary(student, semester, -1, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard attendance data started")
if (forceRefresh) return@onEach
val attendancePercentage = it.data?.calculatePercentage()
processHorizontalGroupData(
attendancePercentage = attendancePercentage,
isLoading = true,
forceRefresh = forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard attendance result: Success")
val attendancePercentage = it.data?.calculatePercentage()
processHorizontalGroupData(
attendancePercentage = attendancePercentage,
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard attendance result: An exception occurred")
errorHandler.dispatch(it.error!!)
processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh)
}
}
}.launch("dashboard_attendance")
}
private fun loadGrades(forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
gradeRepository.getGrades(student, semester, forceRefresh)
@ -353,6 +328,7 @@ class DashboardPresenter @Inject constructor(
Status.LOADING -> {
Timber.i("Loading dashboard grades data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Grades(
subjectWithGrades = it.data,
@ -360,6 +336,10 @@ class DashboardPresenter @Inject constructor(
isLoading = true
), forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.GRADES
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard grades result: Success")
@ -367,7 +347,8 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Grades(
subjectWithGrades = it.data,
gradeTheme = preferencesRepository.gradeColorTheme
), forceRefresh
),
forceRefresh
)
}
Status.ERROR -> {
@ -379,9 +360,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_grades")
}
private fun loadLessons(forceRefresh: Boolean) {
private fun loadLessons(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay
@ -398,24 +378,34 @@ class DashboardPresenter @Inject constructor(
Status.LOADING -> {
Timber.i("Loading dashboard lessons data started")
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 -> {
Timber.i("Loading dashboard lessons result: Success")
updateData(DashboardItem.Lessons(it.data), forceRefresh)
updateData(
DashboardItem.Lessons(it.data), forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Lessons(error = it.error), forceRefresh)
updateData(
DashboardItem.Lessons(error = it.error), forceRefresh
)
}
}
}.launch("dashboard_lessons")
}
private fun loadHomework(forceRefresh: Boolean) {
private fun loadHomework(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay
@ -443,6 +433,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Homework(it.data ?: emptyList(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.HOMEWORK
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard homework result: Success")
@ -457,10 +451,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_homework")
}
private fun loadSchoolAnnouncements(forceRefresh: Boolean) {
private fun loadSchoolAnnouncements(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh)
}.onEach {
when (it.status) {
@ -468,11 +460,13 @@ class DashboardPresenter @Inject constructor(
Timber.i("Loading dashboard announcements data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Announcements(
it.data ?: emptyList(),
isLoading = true
), forceRefresh
DashboardItem.Announcements(it.data ?: emptyList(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard announcements result: Success")
@ -487,9 +481,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_announcements")
}
private fun loadExams(forceRefresh: Boolean) {
private fun loadExams(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
examRepository.getExams(
@ -508,6 +501,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Exams(it.data.orEmpty(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.EXAMS
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard exams result: Success")
@ -522,9 +519,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_exams")
}
private fun loadConferences(forceRefresh: Boolean) {
private fun loadConferences(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
conferenceRepository.getConferences(
@ -542,6 +538,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.CONFERENCES
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard conferences result: Success")
@ -556,145 +556,119 @@ class DashboardPresenter @Inject constructor(
}.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 }
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
val isFirstRunDataLoadedError =
dashboardItem.type in firstLoadedItemList && dashboardItem.error != null
if (error != null) {
updateData(DashboardItem.HorizontalGroup(error = error), forceRefresh)
return
with(dashboardItemLoadedList) {
removeAll { it.type == dashboardItem.type && !isForceRefreshError && !isFirstRunDataLoadedError }
if (!isForceRefreshError && !isFirstRunDataLoadedError) add(dashboardItem)
}
if (isLoading) {
val horizontalGroup =
dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup?
val updatedHorizontalGroup =
horizontalGroup?.copy(isLoading = true) ?: DashboardItem.HorizontalGroup(isLoading = true)
sortDashboardItems()
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)
if (forceRefresh) {
updateForceRefreshData(dashboardItem)
} else {
updateNormalData()
}
}
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
with(dashboardItemLoadedList) {
removeAll { it.type == dashboardItem.type && !isForceRefreshError }
if (!isForceRefreshError) add(dashboardItem)
sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal }
private fun updateNormalData() {
val isItemsLoaded =
dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } }
val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all {
it.isDataLoaded || it.error != null
}
if (forceRefresh) {
with(dashboardItemRefreshLoadedList) {
removeAll { it.type == dashboardItem.type }
add(dashboardItem)
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) {
removeAll { it.type == dashboardItem.type }
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 ->
dashboardItemsPosition?.getOrDefault(
tile.type,
tile.type.ordinal + 100
) ?: 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

@ -90,10 +90,10 @@ class LoginFormPresenter @Inject constructor(
flowWithResource {
studentRepository.getStudentsScrapper(
email,
password,
host,
symbol
email = email,
password = password,
scrapperBaseUrl = host,
symbol = symbol
)
}.onEach {
when (it.status) {

View File

@ -78,7 +78,9 @@ class LoginStudentSelectPresenter @Inject constructor(
when (it.status) {
Status.LOADING -> Timber.d("Login student select students load started")
Status.SUCCESS -> view?.updateData(studentsWithSemesters.map { studentWithSemesters ->
studentWithSemesters to it.data!!.any { item -> compareStudents(studentWithSemesters.student, item.student) }
studentWithSemesters to it.data!!.any { item ->
compareStudents(studentWithSemesters.student, item.student)
}
})
Status.ERROR -> {
errorHandler.dispatch(it.error!!)
@ -95,35 +97,32 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun registerStudents(studentsWithSemesters: List<StudentWithSemesters>) {
flowWithResource {
val savedStudents = studentRepository.saveStudents(studentsWithSemesters)
val firstRegistered = studentsWithSemesters.first().apply { student.id = savedStudents.first() }
studentRepository.switchStudent(firstRegistered)
}.onEach {
when (it.status) {
Status.LOADING -> view?.run {
Timber.i("Registration started")
showProgress(true)
showContent(false)
}
Status.SUCCESS -> {
Timber.i("Registration result: Success")
view?.openMainView()
logRegisterEvent(studentsWithSemesters)
}
Status.ERROR -> {
Timber.i("Registration result: An exception occurred ")
view?.apply {
showProgress(false)
showContent(true)
showContact(true)
flowWithResource { studentRepository.saveStudents(studentsWithSemesters) }
.onEach {
when (it.status) {
Status.LOADING -> view?.run {
Timber.i("Registration started")
showProgress(true)
showContent(false)
}
Status.SUCCESS -> {
Timber.i("Registration result: Success")
view?.openMainView()
logRegisterEvent(studentsWithSemesters)
}
Status.ERROR -> {
Timber.i("Registration result: An exception occurred ")
view?.apply {
showProgress(false)
showContent(true)
showContact(true)
}
lastError = it.error
loginErrorHandler.dispatch(it.error!!)
logRegisterEvent(studentsWithSemesters, it.error)
}
lastError = it.error
loginErrorHandler.dispatch(it.error!!)
logRegisterEvent(studentsWithSemesters, it.error)
}
}
}.launch("register")
}.launch("register")
}
fun onDiscordClick() {
@ -134,7 +133,10 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" })
}
private fun logRegisterEvent(studentsWithSemesters: List<StudentWithSemesters>, error: Throwable? = null) {
private fun logRegisterEvent(
studentsWithSemesters: List<StudentWithSemesters>,
error: Throwable? = null
) {
studentsWithSemesters.forEach { student ->
analytics.logEvent(
"registration_student_select",

View File

@ -131,7 +131,9 @@ class LuckyNumberHistoryFragment :
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@LuckyNumberHistoryFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun showContent(show: Boolean) {

View File

@ -117,6 +117,7 @@ class MessageTabPresenter @Inject constructor(
if (!it.data.isNullOrEmpty()) {
view?.run {
enableSwipe(true)
showErrorView(false)
showRefresh(true)
showProgress(false)
showContent(true)

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
@ -66,6 +67,9 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
override val examRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) }
override val luckyNumberRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMoreBinding.bind(view)
@ -128,6 +132,10 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
(activity as? MainActivity)?.pushView(ExamFragment.newInstance())
}
override fun openLuckyNumberView() {
(activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance())
}
override fun popView(depth: Int) {
(activity as? MainActivity)?.popView(depth)
}

View File

@ -31,6 +31,7 @@ class MorePresenter @Inject constructor(
schoolAndTeachersRes?.first -> openSchoolAndTeachersView()
mobileDevicesRes?.first -> openMobileDevicesView()
settingsRes?.first -> openSettingsView()
luckyNumberRes?.first -> openLuckyNumberView()
}
}
}
@ -48,6 +49,7 @@ class MorePresenter @Inject constructor(
examRes,
homeworkRes,
noteRes,
luckyNumberRes,
conferencesRes,
schoolAnnouncementRes,
schoolAndTeachersRes,

View File

@ -23,6 +23,8 @@ interface MoreView : BaseView {
val examRes: Pair<String, Drawable?>?
val luckyNumberRes: Pair<String, Drawable?>?
fun initView()
fun updateData(data: List<Pair<String, Drawable?>>)
@ -46,4 +48,6 @@ interface MoreView : BaseView {
fun openMobileDevicesView()
fun openExamView()
fun openLuckyNumberView()
}

View File

@ -2,7 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
@ -14,6 +14,8 @@ class SchoolAnnouncementAdapter @Inject constructor() :
var items = emptyList<SchoolAnnouncement>()
var onItemClickListener: (SchoolAnnouncement) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
@ -26,9 +28,9 @@ class SchoolAnnouncementAdapter @Inject constructor() :
with(holder.binding) {
schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = HtmlCompat.fromHtml(
item.content, HtmlCompat.FROM_HTML_MODE_COMPACT
)
schoolAnnouncementItemContent.text = item.content.parseAsHtml()
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.databinding.FragmentSchoolAnnouncementBinding
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.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
@ -43,7 +44,9 @@ class SchoolAnnouncementFragment :
override fun initView() {
with(binding.directorInformationRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = schoolAnnouncementAdapter
adapter = schoolAnnouncementAdapter.apply {
onItemClickListener = presenter::onItemClickListener
}
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
@ -99,6 +102,10 @@ class SchoolAnnouncementFragment :
binding.directorInformationSwipe.isRefreshing = show
}
override fun openSchoolAnnouncementDialog(item: SchoolAnnouncement) {
(activity as? MainActivity)?.showDialogFragment(SchoolAnnouncementDialog.newInstance(item))
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.schoolannouncement
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.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -46,6 +47,10 @@ class SchoolAnnouncementPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError)
}
fun onItemClickListener(item: SchoolAnnouncement) {
view?.openSchoolAnnouncementDialog(item)
}
private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading School announcement data started")
@ -59,6 +64,7 @@ class SchoolAnnouncementPresenter @Inject constructor(
view?.run {
enableSwipe(true)
showRefresh(true)
showErrorView(false)
showProgress(false)
showContent(true)
updateData(it.data)

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.studentinfo
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
@ -130,9 +129,9 @@ class StudentInfoFragment :
getString(R.string.student_info_parents_name) to studentInfo.parentsNames
).map {
StudentInfoItem(
it.first,
it.second.ifBlank { getString(R.string.all_no_data) },
false,
title = it.first,
subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
showArrow = false,
)
}
)
@ -146,25 +145,33 @@ class StudentInfoFragment :
getString(R.string.student_info_email) to studentInfo.email
).map {
StudentInfoItem(
it.first,
it.second.ifBlank { getString(R.string.all_no_data) },
false,
title = it.first,
subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
showArrow = false,
)
}
)
}
@SuppressLint("DefaultLocale")
@OptIn(ExperimentalStdlibApi::class)
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(
listOfNotNull(
studentInfo.firstGuardian?.let { it.kinship.capitalise() to it.fullName },
studentInfo.secondGuardian?.let { it.kinship.capitalise() to it.fullName },
).map { (title, value) ->
items.map { (title, value, type) ->
StudentInfoItem(
title.ifBlank { getString(R.string.all_no_data) },
value.ifBlank { getString(R.string.all_no_data) },
true,
title = title.ifBlank { getString(R.string.all_no_data) },
subtitle = value.ifBlank { getString(R.string.all_no_data) },
showArrow = true,
viewType = type,
)
}
)
@ -178,15 +185,15 @@ class StudentInfoFragment :
getString(R.string.student_info_correspondence_address) to studentInfo.correspondenceAddress
).map {
StudentInfoItem(
it.first,
it.second.ifBlank { getString(R.string.all_no_data) },
false,
title = it.first,
subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
showArrow = false,
)
}
)
}
override fun showFirstGuardianTypeData(studentGuardian: StudentGuardian) {
override fun showGuardianTypeData(studentGuardian: StudentGuardian) {
updateData(
listOf(
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
).map {
StudentInfoItem(
it.first,
it.second.ifBlank { getString(R.string.all_no_data) },
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,
title = it.first,
subtitle = it.second.ifBlank { getString(R.string.all_no_data) },
showArrow = false,
)
}
)

View File

@ -3,5 +3,6 @@ package io.github.wulkanowy.ui.modules.studentinfo
data class StudentInfoItem(
val title: 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)
}
fun onItemSelected(position: Int) {
if (infoType != StudentInfoView.Type.FAMILY) return
fun onItemSelected(viewType: StudentInfoView.Type?) {
viewType ?: return
view?.openStudentInfoView(
if (position == 0) StudentInfoView.Type.FIRST_GUARDIAN
else StudentInfoView.Type.SECOND_GUARDIAN,
studentWithSemesters
studentWithSemesters = studentWithSemesters,
infoType = viewType,
)
}
@ -76,15 +75,19 @@ class StudentInfoPresenter @Inject constructor(
flowWithResourceIn {
val semester = studentWithSemesters.semesters.getCurrentOrLast()
studentInfoRepository.getStudentInfo(
studentWithSemesters.student,
semester,
forceRefresh
student = studentWithSemesters.student,
semester = semester,
forceRefresh = forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading student info $infoType started")
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")
showCorrectData(it.data)
view?.run {
@ -122,8 +125,8 @@ class StudentInfoPresenter @Inject constructor(
StudentInfoView.Type.CONTACT -> view?.showContactTypeData(studentInfo)
StudentInfoView.Type.ADDRESS -> view?.showAddressTypeData(studentInfo)
StudentInfoView.Type.FAMILY -> view?.showFamilyTypeData(studentInfo)
StudentInfoView.Type.SECOND_GUARDIAN -> view?.showSecondGuardianTypeData(studentInfo.secondGuardian!!)
StudentInfoView.Type.FIRST_GUARDIAN -> view?.showFirstGuardianTypeData(studentInfo.firstGuardian!!)
StudentInfoView.Type.SECOND_GUARDIAN -> view?.showGuardianTypeData(studentInfo.secondGuardian!!)
StudentInfoView.Type.FIRST_GUARDIAN -> view?.showGuardianTypeData(studentInfo.firstGuardian!!)
}
}

View File

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

View File

@ -7,7 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.datepicker.CalendarConstraints
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 isViewEmpty get() = timetableAdapter.itemCount > 0
override val isViewEmpty get() = timetableAdapter.itemCount == 0
override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
@ -147,9 +147,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override fun setDayHeaderMessage(message: String?) {
binding.timetableEmptyMessage.visibility = if (message.isNullOrEmpty()) GONE else VISIBLE
binding.timetableEmptyMessage.text = HtmlCompat.fromHtml(
message.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT
)
binding.timetableEmptyMessage.text = message.orEmpty().parseAsHtml()
}
override fun showErrorView(show: Boolean) {
@ -204,7 +202,9 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@TimetableFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun openAdditionalLessonsView() {

View File

@ -149,6 +149,7 @@ class TimetablePresenter @Inject constructor(
view?.run {
enableSwipe(true)
showRefresh(true)
showErrorView(false)
showProgress(false)
showContent(true)
updateData(it.data!!.lessons)

View File

@ -152,7 +152,9 @@ class AdditionalLessonsFragment :
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@AdditionalLessonsFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@ -173,7 +173,9 @@ class CompletedLessonsFragment :
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@CompletedLessonsFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.utils
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
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 = {}) {
Intent.parseUri(uri, 0).let {
if (it.resolveActivity(packageManager) != null) startActivity(it)
else onActivityNotFound(uri)
try {
startActivity(it)
} catch (e: ActivityNotFoundException) {
onActivityNotFound(uri)
}
}
}
@ -98,7 +102,9 @@ fun Context.openNavigation(location: String) {
fun Context.openDialer(phone: String) {
val intentUri = Uri.parse("tel:$phone")
val intent = Intent(Intent.ACTION_DIAL, intentUri)
startActivity(intent)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
}
}
fun Context.shareText(text: String, subject: String?) {

View File

@ -2,7 +2,6 @@ package io.github.wulkanowy.utils
import com.google.android.material.datepicker.CalendarConstraints
import kotlinx.parcelize.Parcelize
import java.time.DayOfWeek
import java.time.temporal.ChronoUnit
@Parcelize
@ -12,7 +11,6 @@ class SchoolDaysValidator(val start: Long, val end: Long) : CalendarConstraints.
val date = dateLong.toLocalDateTime()
return date.until(end.toLocalDateTime(), ChronoUnit.DAYS) >= 0 &&
date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0 &&
date.dayOfWeek != DayOfWeek.SUNDAY
date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0
}
}
}

View File

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

View File

@ -1,10 +1,8 @@
Wersja 1.2.0
Wersja 1.2.2
- dodaliśmy nowy ekran startowy 🎉
- usprawniliśmy powiadomienia
- dodaliśmy wersje robocze, filtrowanie oraz informację o odczytaniu przez odbiorcę w wiadomościach
- 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
- naprawiliśmy problem z widocznością zadań w aplikacji gdy widoczne są one na stronie www dziennika (nadal pozostaje błąd z zadaniami widocznymi tylko w oficjalnej aplikacji - czekamy na poprawkę po stronie VULCANa)
- odblokowaliśmy niedzielę w wyborze daty w planie lekcji i innych zakładkach
- przywróciliśmy odnośnik do szczęśliwego numerka w menu Więcej
- naprawiliśmy drobne błędy ze stabilnością i wyglądem
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_height="match_parent">
<FrameLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -44,91 +44,115 @@
android:id="@+id/gradeStatisticsProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
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" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gradeStatisticsRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_grade_statistics_pie" />
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_grade_statistics_pie"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/gradeStatisticsEmpty"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables">
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/gradeStatisticsRecycler">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_main_grade"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/grade_no_items"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/gradeStatisticsError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/gradeStatisticsErrorMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/gradeStatisticsErrorDetails"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/gradeStatisticsEmpty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UseCompoundDrawables"
tools:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/gradeStatisticsErrorRetry"
android:layout_width="wrap_content"
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_main_grade"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/grade_no_items"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/gradeStatisticsError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UseCompoundDrawables"
tools:visibility="gone">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/gradeStatisticsErrorMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/gradeStatisticsErrorDetails"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/gradeStatisticsErrorRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

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

View File

@ -496,7 +496,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Lekce</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_soon">Brzy:</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>
</plurals>
<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-->
<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>

View File

@ -432,7 +432,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Lektionen</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_soon">Bald:</string>
<string name="dashboard_timetable_first_lesson_title_first">Erstens:</string>
@ -488,7 +487,7 @@
<item quantity="other">%1$d weitere Konferenzen</item>
</plurals>
<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-->
<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>

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_field_required">To pole jest wymagane</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_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>
@ -496,7 +496,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Lekcje</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_soon">Wkrótce:</string>
<string name="dashboard_timetable_first_lesson_title_first">Pierwsza:</string>
@ -566,7 +565,7 @@
<item quantity="other">Jeszcze %1$d dodatkowych zebrań</item>
</plurals>
<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-->
<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>

View File

@ -496,7 +496,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Уроки</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_soon">Скоро:</string>
<string name="dashboard_timetable_first_lesson_title_first">Первый:</string>
@ -566,7 +565,7 @@
<item quantity="other">Еще %1$d конференций</item>
</plurals>
<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-->
<string name="dialog_error_check_update">Проверить наличие обновлений</string>
<string name="dialog_error_check_update_message">Прежде чем сообщать об ошибке, проверьте наличие обновлений</string>

View File

@ -496,7 +496,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Lekcie</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_soon">Čoskoro:</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>
</plurals>
<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-->
<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>

View File

@ -496,7 +496,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Уроки</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_soon">Незабаром:</string>
<string name="dashboard_timetable_first_lesson_title_first">Перше:</string>
@ -566,7 +565,7 @@
<item quantity="other">%1$d більше конференцій</item>
</plurals>
<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-->
<string name="dialog_error_check_update">Провірити наявність оновлень</string>
<string name="dialog_error_check_update_message">Перед тим, як повідомлювати о помілці, перевірте наявність оновлень</string>

View File

@ -69,7 +69,7 @@
<string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Send email</string>
<string name="login_email_subject" translatable="false">Zgłoszenie: Problemy z logowaniem</string>
<string name="login_email_text" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nOpis problemu: </string>
<string name="login_email_text" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nOpis problemu (pełna nazwa szkoły, klasa ucznia): </string>
<string name="login_recover_warning">Make sure you select the correct UONET+ register variation!</string>
<string name="login_recover_button">I forgot my password</string>
<string name="login_recover_title">Recover your account</string>
@ -495,7 +495,6 @@
<!--Dashboard-->
<string name="dashboard_timetable_title">Lessons</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_soon">Soon:</string>
<string name="dashboard_timetable_first_lesson_title_first">First:</string>
@ -557,7 +556,7 @@
</plurals>
<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-->

View File

@ -37,7 +37,8 @@ class StudentTest {
studentDb,
semesterDb,
mockSdk,
AppInfo()
AppInfo(),
mockk()
)
}

View File

@ -89,24 +89,11 @@ class LoginStudentSelectPresenterTest {
@Test
fun onSelectedStudentTest() {
coEvery {
studentRepository.saveStudents(
listOf(
StudentWithSemesters(
testStudent,
emptyList()
)
)
)
} returns listOf(1L)
coEvery {
studentRepository.switchStudent(
StudentWithSemesters(
testStudent,
emptyList()
)
)
studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList())))
} just Runs
every { loginStudentSelectView.openMainView() } just Runs
presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false)
presenter.onSignIn()
@ -118,18 +105,14 @@ class LoginStudentSelectPresenterTest {
@Test
fun onSelectedStudentErrorTest() {
coEvery {
studentRepository.saveStudents(
listOf(
StudentWithSemesters(
testStudent,
emptyList()
)
)
)
studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList())))
} throws testException
coEvery { studentRepository.logoutStudent(testStudent) } just Runs
presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false)
presenter.onSignIn()
verify { loginStudentSelectView.showContent(false) }
verify { loginStudentSelectView.showProgress(true) }
verify { errorHandler.dispatch(match { testException.message == it.message }) }

View File

@ -32,7 +32,22 @@ class TimetableExtensionTest {
assertEquals(null, getTimetableEntity(canceled = true).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)
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

View File

@ -12,13 +12,13 @@ buildscript {
}
dependencies {
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.gms:google-services:4.3.10'
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
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 "gradle.plugin.com.star-zero.gradle:githook:1.2.0"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries"