From 368028c6f4a3a7a3868a5769b546d1297aaf3e9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 14:11:53 +0000 Subject: [PATCH 01/77] Bump kotlin_version from 1.7.21 to 1.8.0 (#2092) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 87e201ac..31252efa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.7.21' + kotlin_version = '1.8.0' about_libraries = '10.5.2' hilt_version = "2.44.2" } From 2293e8c1e6b7f6a75dd230e2491c5045763b31bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 07:17:17 +0000 Subject: [PATCH 02/77] Bump huawei-publish-gradle-plugin from 1.3.4 to 1.3.5 (#2101) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 31252efa..8d04bf03 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { classpath 'com.huawei.agconnect:agcp:1.7.3.302' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' classpath "com.github.triplet.gradle:play-publisher:3.6.0" - classpath "ru.cian:huawei-publish-gradle-plugin:1.3.4" + classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" From 84812fb048814e033d3693304ea7efb460ab97d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 07:17:38 +0000 Subject: [PATCH 03/77] Bump hianalytics from 6.9.0.301 to 6.9.1.200 (#2100) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fff19ebe..13b36ac7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -250,7 +250,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:21.4.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.9.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.302' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 0306e381305663cce7a7e6eac88b986ee2de7278 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 07:17:58 +0000 Subject: [PATCH 04/77] Bump runner from 1.5.1 to 1.5.2 (#2099) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 13b36ac7..61429772 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -265,7 +265,7 @@ dependencies { testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation 'org.robolectric:robolectric:4.9.2' - testImplementation "androidx.test:runner:1.5.1" + testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test.ext:junit:1.1.4" testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.room:room-testing:$room" From af8bb53c1769097acd4f9d2052bfffaf9a3a55f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 07:28:02 +0000 Subject: [PATCH 05/77] Bump junit from 1.1.4 to 1.1.5 (#2098) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 61429772..aaa4b6af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -266,7 +266,7 @@ dependencies { testImplementation 'org.robolectric:robolectric:4.9.2' testImplementation "androidx.test:runner:1.5.2" - testImplementation "androidx.test.ext:junit:1.1.4" + testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.room:room-testing:$room" testImplementation "com.google.dagger:hilt-android-testing:$hilt_version" From e1bffabf1063f1ea67009b5bbe14c3669f50644c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 14 Jan 2023 15:48:58 +0100 Subject: [PATCH 06/77] Fix marking message as read (#2102) --- app/build.gradle | 2 +- .../github/wulkanowy/data/repositories/MessageRepository.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index aaa4b6af..067a8a47 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.9.1" + implementation "io.github.wulkanowy:sdk:1.9.2-SNAPSHOT" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 6dfc3ab7..53d9bead 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -103,7 +103,10 @@ class MessageRepository @Inject constructor( messagesDb.loadMessageWithAttachment(message.messageGlobalKey) }, fetch = { - sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead) + sdk.init(student).getMessageDetails( + messageKey = it!!.message.messageGlobalKey, + markAsRead = message.unread && markAsRead, + ) }, saveFetchResult = { old, new -> checkNotNull(old) { "Fetched message no longer exist!" } From 89678c22767f333215d4bd440e70501f645c357b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:13:06 +0000 Subject: [PATCH 07/77] Bump agcp from 1.7.3.302 to 1.8.0.300 (#2108) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8d04bf03..2831fa73 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.3.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.14' - classpath 'com.huawei.agconnect:agcp:1.7.3.302' + classpath 'com.huawei.agconnect:agcp:1.8.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" From ac4a822930b1a72058407588f7e51b9179829968 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:13:34 +0000 Subject: [PATCH 08/77] Bump agconnect-crash from 1.7.3.302 to 1.8.0.300 (#2104) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 067a8a47..e15341f8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -251,7 +251,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:21.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.302' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From ef9e4b7ad960014484d444c6352a48b0befad014 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:14:15 +0000 Subject: [PATCH 09/77] Bump appcompat from 1.5.1 to 1.6.0 (#2107) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e15341f8..5219fed3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -196,7 +196,7 @@ dependencies { implementation "androidx.core:core-ktx:1.9.0" implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.activity:activity-ktx:1.6.1" - implementation "androidx.appcompat:appcompat:1.5.1" + implementation "androidx.appcompat:appcompat:1.6.0" implementation "androidx.fragment:fragment-ktx:1.5.5" implementation "androidx.annotation:annotation:1.5.0" From 95cf521f632160fd81996d3f7f6090c871f402ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:21:22 +0000 Subject: [PATCH 10/77] Bump gradle from 7.3.1 to 7.4.0 (#2106) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2831fa73..5abbe57d 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.0' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.14' classpath 'com.huawei.agconnect:agcp:1.8.0.300' From 6df3f22c7db756e771e8ea45898f3fcd24323d70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:30:53 +0000 Subject: [PATCH 11/77] Bump room from 2.4.3 to 2.5.0 (#2105) --- app/build.gradle | 2 +- .../main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5219fed3..33e80058 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -179,7 +179,7 @@ huaweiPublish { ext { work_manager = "2.7.1" android_hilt = "1.0.0" - room = "2.4.3" + room = "2.5.0" chucker = "3.5.2" mockk = "1.13.3" coroutines = "1.6.4" diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index 87b3e0b3..cfa7a72a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.db.dao import androidx.room.* -import androidx.room.OnConflictStrategy.ABORT import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters @@ -11,7 +10,7 @@ import javax.inject.Singleton @Dao abstract class StudentDao { - @Insert(onConflict = ABORT) + @Insert(onConflict = OnConflictStrategy.ABORT) abstract suspend fun insertAll(student: List): List @Delete From 32d6b4a7a6cc4c83c378aa132d16bcede347418c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 14 Jan 2023 17:06:47 +0100 Subject: [PATCH 12/77] Add menu order settings (#1924) --- .../repositories/PreferencesRepository.kt | 19 +- .../wulkanowy/ui/modules/Destination.kt | 30 ++- .../modules/conference/ConferenceFragment.kt | 10 +- .../modules/conference/ConferencePresenter.kt | 7 + .../ui/modules/conference/ConferenceView.kt | 2 + .../wulkanowy/ui/modules/exam/ExamFragment.kt | 14 +- .../ui/modules/exam/ExamPresenter.kt | 13 ++ .../wulkanowy/ui/modules/exam/ExamView.kt | 2 + .../ui/modules/homework/HomeworkFragment.kt | 10 +- .../ui/modules/homework/HomeworkPresenter.kt | 15 +- .../ui/modules/homework/HomeworkView.kt | 2 + .../luckynumber/LuckyNumberFragment.kt | 10 +- .../luckynumber/LuckyNumberPresenter.kt | 5 + .../ui/modules/luckynumber/LuckyNumberView.kt | 2 + .../wulkanowy/ui/modules/main/MainActivity.kt | 35 +-- .../ui/modules/main/MainPresenter.kt | 17 +- .../wulkanowy/ui/modules/main/MainView.kt | 7 +- .../ui/modules/message/MessageFragment.kt | 20 +- .../ui/modules/message/MessagePresenter.kt | 8 + .../ui/modules/message/MessageView.kt | 4 + .../modules/message/tab/MessageTabFragment.kt | 4 + .../message/tab/MessageTabPresenter.kt | 8 + .../mobiledevice/MobileDeviceFragment.kt | 10 +- .../mobiledevice/MobileDevicePresenter.kt | 6 + .../modules/mobiledevice/MobileDeviceView.kt | 2 + .../wulkanowy/ui/modules/more/MoreAdapter.kt | 14 +- .../wulkanowy/ui/modules/more/MoreFragment.kt | 88 +------- .../wulkanowy/ui/modules/more/MoreItem.kt | 12 + .../ui/modules/more/MorePresenter.kt | 58 +++-- .../wulkanowy/ui/modules/more/MoreView.kt | 44 +--- .../wulkanowy/ui/modules/note/NoteFragment.kt | 10 +- .../ui/modules/note/NotePresenter.kt | 6 + .../wulkanowy/ui/modules/note/NoteView.kt | 2 + .../appearance/menuorder/AppMenuItem.kt | 206 ++++++++++++++++++ .../menuorder/MenuItemMoveCallback.kt | 44 ++++ .../appearance/menuorder/MenuOrderAdapter.kt | 58 +++++ .../MenuOrderDividerItemDecoration.kt | 50 +++++ .../appearance/menuorder/MenuOrderFragment.kt | 101 +++++++++ .../appearance/menuorder/MenuOrderItem.kt | 6 + .../menuorder/MenuOrderPresenter.kt | 64 ++++++ .../appearance/menuorder/MenuOrderView.kt | 16 ++ .../main/res/drawable/ic_menu_order_drag.xml | 10 + .../main/res/layout/fragment_menu_order.xml | 12 + app/src/main/res/layout/item_menu_order.xml | 46 ++++ app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 8 + .../res/xml/scheme_preferences_appearance.xml | 14 +- .../ui/modules/main/MainPresenterTest.kt | 2 +- 48 files changed, 918 insertions(+), 216 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt create mode 100644 app/src/main/res/drawable/ic_menu_order_drag.xml create mode 100644 app/src/main/res/layout/fragment_menu_order.xml create mode 100644 app/src/main/res/layout/item_menu_order.xml diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index afc26286..29a65a96 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.* import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode +import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString @@ -28,9 +29,6 @@ class PreferencesRepository @Inject constructor( private val json: Json, ) { - val startMenuIndex: Int - get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() - val isShowPresent: Boolean get() = getBoolean( R.string.pref_key_attendance_present, @@ -315,6 +313,20 @@ class PreferencesRepository @Inject constructor( putBoolean(context.getString(R.string.pref_key_ads_enabled), value) } + var appMenuItemOrder: List + get() { + val value = sharedPref.getString(PREF_KEY_APP_MENU_ITEM_ORDER, null) + ?: return AppMenuItem.defaultAppMenuItemList + + return json.decodeFromString(value) + } + set(value) = sharedPref.edit { + putString( + PREF_KEY_APP_MENU_ITEM_ORDER, + json.encodeToString(value) + ) + } + var installationId: String get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } @@ -341,6 +353,7 @@ class PreferencesRepository @Inject constructor( sharedPref.getBoolean(id, context.resources.getBoolean(default)) private companion object { + private const val PREF_KEY_APP_MENU_ITEM_ORDER = "app_menu_item_order" private const val PREF_KEY_INSTALLATION_ID = "installation_id" private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt index 561419a0..958be5a7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -10,10 +10,12 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment import io.github.wulkanowy.ui.modules.more.MoreFragment import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment +import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import kotlinx.serialization.Serializable import java.time.LocalDate @@ -39,10 +41,12 @@ sealed class Destination { NOTE(Note), CONFERENCE(Conference), SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), - SCHOOL(School), - LUCKY_NUMBER(More), + SCHOOL_AND_TEACHERS(SchoolAndTeachers), + LUCKY_NUMBER(LuckyNumber), MORE(More), - MESSAGE(Message); + MESSAGE(Message), + MOBILE_DEVICE(MobileDevice), + SETTINGS(Settings); } @Serializable @@ -103,9 +107,9 @@ sealed class Destination { } @Serializable - object School : Destination() { - override val destinationType get() = Type.SCHOOL - override val destinationFragment get() = SchoolFragment.newInstance() + object SchoolAndTeachers : Destination() { + override val destinationType get() = Type.SCHOOL_AND_TEACHERS + override val destinationFragment get() = SchoolAndTeachersFragment.newInstance() } @Serializable @@ -125,4 +129,16 @@ sealed class Destination { override val destinationType get() = Type.MESSAGE override val destinationFragment get() = MessageFragment.newInstance() } + + @Serializable + object MobileDevice : Destination() { + override val destinationType get() = Type.MOBILE_DEVICE + override val destinationFragment get() = MobileDeviceFragment.newInstance() + } + + @Serializable + object Settings : Destination() { + override val destinationType get() = Type.SETTINGS + override val destinationFragment get() = SettingsFragment.newInstance() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt index b9642b1c..0cd3150c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt @@ -16,7 +16,7 @@ import javax.inject.Inject @AndroidEntryPoint class ConferenceFragment : BaseFragment(R.layout.fragment_conference), - ConferenceView, MainView.TitledView { + ConferenceView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: ConferencePresenter @@ -109,6 +109,14 @@ class ConferenceFragment : BaseFragment(R.layout.frag (activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference)) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun resetView() { + binding.conferenceRecycler.smoothScrollToPosition(0) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt index f5364893..1178c720 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt @@ -96,4 +96,11 @@ class ConferencePresenter @Inject constructor( .onResourceError(errorHandler::dispatch) .launch() } + + fun onFragmentReselected() { + Timber.i("Conference is reselected") + if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt index 4f73394d..3299a1f0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt @@ -28,4 +28,6 @@ interface ConferenceView : BaseView { fun showContent(show: Boolean) fun openConferenceDialog(conference: Conference) + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index ddd0e4a1..3d42bd00 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -2,9 +2,7 @@ package io.github.wulkanowy.ui.modules.exam import android.os.Bundle import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE +import android.view.View.* import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -20,7 +18,7 @@ import javax.inject.Inject @AndroidEntryPoint class ExamFragment : BaseFragment(R.layout.fragment_exam), ExamView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: ExamPresenter @@ -126,6 +124,14 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), (activity as? MainActivity)?.showDialogFragment(ExamDialog.newInstance(exam)) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun resetView() { + binding.examRecycler.smoothScrollToPosition(0) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt index 99b0bcb8..85814072 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt @@ -175,4 +175,17 @@ class ExamPresenter @Inject constructor( ) } } + + fun onViewReselected() { + Timber.i("Exam view is reselected") + + baseDate = now().nextOrSameSchoolDay + + if (currentDate != baseDate) { + reloadView(baseDate) + loadData() + } else if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt index 45b9e788..677fac40 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt @@ -34,4 +34,6 @@ interface ExamView : BaseView { fun showPreButton(show: Boolean) fun showExamDialog(exam: Exam) + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index d4eaade2..9d5130e4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -21,7 +21,7 @@ import javax.inject.Inject @AndroidEntryPoint class HomeworkFragment : BaseFragment(R.layout.fragment_homework), - HomeworkView, MainView.TitledView { + HomeworkView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: HomeworkPresenter @@ -133,6 +133,14 @@ class HomeworkFragment : BaseFragment(R.layout.fragment (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun resetView() { + binding.homeworkRecycler.smoothScrollToPosition(0) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index 2ac552b4..6b263e26 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt @@ -177,8 +177,21 @@ class HomeworkPresenter @Inject constructor( showNextButton(!currentDate.plusDays(7).isHolidays) updateNavigationWeek( "${currentDate.monday.toFormattedString("dd.MM")} - " + - currentDate.sunday.toFormattedString("dd.MM") + currentDate.sunday.toFormattedString("dd.MM") ) } } + + fun onViewReselected() { + Timber.i("Homework view is reselected") + + baseDate = LocalDate.now().nextOrSameSchoolDay + + if (currentDate != baseDate) { + reloadView(baseDate) + loadData() + } else if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index 7c05ab86..56ba6c89 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -36,4 +36,6 @@ interface HomeworkView : BaseView { fun showHomeworkDialog(homework: Homework) fun showAddHomeworkDialog() + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt index 0a73fe15..a6c75c1d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberFragment.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @AndroidEntryPoint class LuckyNumberFragment : BaseFragment(R.layout.fragment_lucky_number), LuckyNumberView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: LuckyNumberPresenter @@ -86,6 +86,14 @@ class LuckyNumberFragment : (activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance()) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onViewReselected() + } + + override fun popView() { + (activity as? MainActivity)?.popView() + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt index 6f5c8e74..2039624b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberPresenter.kt @@ -99,4 +99,9 @@ class LuckyNumberPresenter @Inject constructor( fun onDetailsClick() { view?.showErrorDetailsDialog(lastError) } + + fun onViewReselected() { + Timber.i("Luckynumber view is reselected") + view?.popView() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt index 0c05a156..3d34ebc8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/LuckyNumberView.kt @@ -26,4 +26,6 @@ interface LuckyNumberView : BaseView { fun showContent(show: Boolean) fun openLuckyNumberHistory() + + fun popView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index d332ee35..51092376 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -27,6 +27,7 @@ import io.github.wulkanowy.databinding.DialogAdsConsentBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog +import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import io.github.wulkanowy.utils.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -124,13 +125,20 @@ class MainActivity : BaseActivity(), MainVie return true } - override fun initView(startMenuIndex: Int, rootDestinations: List) { + override fun initView( + startMenuIndex: Int, + rootAppMenuItems: List, + rootUpdatedDestinations: List + ) { initializeToolbar() - initializeBottomNavigation(startMenuIndex) - initializeNavController(startMenuIndex, rootDestinations) + initializeBottomNavigation(startMenuIndex, rootAppMenuItems) + initializeNavController(startMenuIndex, rootUpdatedDestinations) } - private fun initializeNavController(startMenuIndex: Int, rootDestinations: List) { + private fun initializeNavController( + startMenuIndex: Int, + rootUpdatedDestinations: List + ) { with(navController) { setOnViewChangeListener { destinationView -> presenter.onViewChange(destinationView) @@ -140,7 +148,7 @@ class MainActivity : BaseActivity(), MainVie ) } fragmentHideStrategy = HIDE - rootFragments = rootDestinations.map { it.destinationFragment } + rootFragments = rootUpdatedDestinations.map { it.destinationFragment } initialize(startMenuIndex, savedInstanceState) } @@ -156,17 +164,16 @@ class MainActivity : BaseActivity(), MainVie } } - private fun initializeBottomNavigation(startMenuIndex: Int) { + private fun initializeBottomNavigation( + startMenuIndex: Int, + rootAppMenuItems: List + ) { with(binding.mainBottomNav) { with(menu) { - add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) - .setIcon(R.drawable.ic_main_dashboard) - add(Menu.NONE, 1, Menu.NONE, R.string.grade_title) - .setIcon(R.drawable.ic_main_grade) - add(Menu.NONE, 2, Menu.NONE, R.string.attendance_title) - .setIcon(R.drawable.ic_main_attendance) - add(Menu.NONE, 3, Menu.NONE, R.string.timetable_title) - .setIcon(R.drawable.ic_main_timetable) + rootAppMenuItems.forEachIndexed { index, item -> + add(Menu.NONE, index, Menu.NONE, item.title) + .setIcon(item.icon) + } add(Menu.NONE, 4, Menu.NONE, R.string.more_title) .setIcon(R.drawable.ic_main_more) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 458e966d..d51cdac6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -42,17 +42,16 @@ class MainPresenter @Inject constructor( private var studentsWitSemesters: List? = null - private val rootDestinationTypeList = listOf( - Destination.Type.DASHBOARD, - Destination.Type.GRADE, - Destination.Type.ATTENDANCE, - Destination.Type.TIMETABLE, - Destination.Type.MORE - ) + private val rootAppMenuItems = preferencesRepository.appMenuItemOrder + .sortedBy { it.order } + .take(4) + + private val rootDestinationTypeList = rootAppMenuItems.map { it.destinationType } + .plus(Destination.Type.MORE) private val Destination?.startMenuIndex get() = when { - this == null -> preferencesRepository.startMenuIndex + this == null -> 0 destinationType in rootDestinationTypeList -> { rootDestinationTypeList.indexOf(destinationType) } @@ -69,7 +68,7 @@ class MainPresenter @Inject constructor( if (it == initDestination?.destinationType) initDestination else it.defaultDestination } - view.initView(startMenuIndex, destinations) + view.initView(startMenuIndex, rootAppMenuItems, destinations) if (initDestination != null && startMenuIndex == 4) { view.openMoreDestination(initDestination) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 3d018e3d..03f9641d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -4,6 +4,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem interface MainView : BaseView { @@ -15,7 +16,11 @@ interface MainView : BaseView { val currentStackSize: Int? - fun initView(startMenuIndex: Int, rootDestinations: List) + fun initView( + startMenuIndex: Int, + rootAppMenuItems: List, + rootUpdatedDestinations: List + ) fun switchMenuView(position: Int) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 4607793c..4317fb7f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -15,6 +15,7 @@ import io.github.wulkanowy.data.enums.MessageFolder.* import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter +import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity import io.github.wulkanowy.ui.modules.message.tab.MessageTabFragment @@ -24,7 +25,7 @@ import javax.inject.Inject @AndroidEntryPoint class MessageFragment : BaseFragment(R.layout.fragment_message), - MessageView, MainView.TitledView { + MessageView, MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: MessagePresenter @@ -123,8 +124,12 @@ class MessageFragment : BaseFragment(R.layout.fragment_m presenter.onChildViewShowNewMessage(show) } - fun onFragmentChanged() { - presenter.onFragmentChanged() + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun onFragmentChanged() { + if (::presenter.isInitialized) presenter.onFragmentChanged() } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { @@ -139,10 +144,19 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } } + override fun notifyChildParentReselected(index: Int) { + (pagerAdapter.getFragmentInstance(index) as? MessageTabFragment) + ?.onParentReselected() + } + override fun openSendMessage() { context?.let { it.startActivity(SendMessageActivity.getStartIntent(it)) } } + override fun popView() { + (activity as? MainActivity)?.popView() + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index 68bdc4b7..cf6bad19 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -39,6 +39,14 @@ class MessagePresenter @Inject constructor( view?.notifyChildrenFinishActionMode() } + fun onFragmentReselected() { + Timber.i("Message view is reselected") + view?.run { + popView() + notifyChildParentReselected(currentPageIndex) + } + } + fun onChildViewLoaded() { view?.apply { showContent(true) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt index e0cc5098..def4a275 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt @@ -20,5 +20,9 @@ interface MessageView : BaseView { fun notifyChildrenFinishActionMode() + fun notifyChildParentReselected(index: Int) + fun openSendMessage() + + fun popView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index c78ccc6e..592cbd60 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -235,6 +235,10 @@ class MessageTabFragment : BaseFragment(R.layout.frag presenter.onParentFinishActionMode() } + fun onParentReselected() { + presenter.onParentReselected() + } + private fun onChipChecked(chip: CompoundButton, isChecked: Boolean) { when (chip.id) { R.id.chip_unread -> presenter.onUnreadFilterSelected(isChecked) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index ea142db2..ec92e9c2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -83,6 +83,14 @@ class MessageTabPresenter @Inject constructor( view?.showActionMode(false) } + fun onParentReselected() { + view?.run { + if (!isViewEmpty) { + resetListPosition() + } + } + } + fun onDestroyActionMode() { isActionMode = false messagesToDelete.clear() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt index f8e367c5..1afe773c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceFragment.kt @@ -22,7 +22,7 @@ import javax.inject.Inject @AndroidEntryPoint class MobileDeviceFragment : BaseFragment(R.layout.fragment_mobile_device), MobileDeviceView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: MobileDevicePresenter @@ -135,6 +135,14 @@ class MobileDeviceFragment : (activity as? MainActivity)?.showDialogFragment(MobileDeviceTokenDialog.newInstance()) } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun resetView() { + binding.mobileDevicesRecycler.smoothScrollToPosition(0) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt index 36a720e5..56785dbf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt @@ -129,4 +129,10 @@ class MobileDevicePresenter @Inject constructor( .onResourceError(errorHandler::dispatch) .launch("unregister") } + + fun onFragmentReselected() { + if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt index b94646a7..973cb123 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDeviceView.kt @@ -32,4 +32,6 @@ interface MobileDeviceView : BaseView { fun setErrorDetails(message: String) fun showTokenDialog() + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt index 70587b0c..697eab33 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreAdapter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.more -import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView @@ -9,9 +8,9 @@ import javax.inject.Inject class MoreAdapter @Inject constructor() : RecyclerView.Adapter() { - var items = emptyList>() + var items = emptyList() - var onClickListener: (name: String) -> Unit = {} + var onClickListener: (moreItem: MoreItem) -> Unit = {} override fun getItemCount() = items.size @@ -20,13 +19,14 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragment_more), override val titleStringId: Int get() = R.string.more_title - override val messagesRes: Pair? - get() = context?.run { getString(R.string.message_title) to getCompatDrawable(R.drawable.ic_more_messages) } - - override val homeworkRes: Pair? - get() = context?.run { getString(R.string.homework_title) to getCompatDrawable(R.drawable.ic_more_homework) } - - override val noteRes: Pair? - get() = context?.run { getString(R.string.note_title) to getCompatDrawable(R.drawable.ic_more_note) } - - override val conferencesRes: Pair? - get() = context?.run { getString(R.string.conferences_title) to getCompatDrawable(R.drawable.ic_more_conferences) } - - override val schoolAnnouncementRes: Pair? - get() = context?.run { getString(R.string.school_announcement_title) to getCompatDrawable(R.drawable.ic_all_about) } - - override val schoolAndTeachersRes: Pair? - get() = context?.run { getString(R.string.schoolandteachers_title) to getCompatDrawable((R.drawable.ic_more_schoolandteachers)) } - - override val mobileDevicesRes: Pair? - get() = context?.run { getString(R.string.mobile_devices_title) to getCompatDrawable(R.drawable.ic_more_mobile_devices) } - - override val settingsRes: Pair? - get() = context?.run { getString(R.string.settings_title) to getCompatDrawable(R.drawable.ic_more_settings) } - - override val examRes: Pair? - get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) } - - override val luckyNumberRes: Pair? - 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) @@ -94,57 +54,21 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), ?.onFragmentChanged() } - override fun updateData(data: List>) { + override fun updateData(data: List) { with(moreAdapter) { items = data notifyDataSetChanged() } } - override fun openMessagesView() { - (activity as? MainActivity)?.pushView(MessageFragment.newInstance()) - } - - override fun openHomeworkView() { - (activity as? MainActivity)?.pushView(HomeworkFragment.newInstance()) - } - - override fun openNoteView() { - (activity as? MainActivity)?.pushView(NoteFragment.newInstance()) - } - - override fun openSchoolAnnouncementView() { - (activity as? MainActivity)?.pushView(SchoolAnnouncementFragment.newInstance()) - } - - override fun openConferencesView() { - (activity as? MainActivity)?.pushView(ConferenceFragment.newInstance()) - } - - override fun openSchoolAndTeachersView() { - (activity as? MainActivity)?.pushView(SchoolAndTeachersFragment.newInstance()) - } - - override fun openMobileDevicesView() { - (activity as? MainActivity)?.pushView(MobileDeviceFragment.newInstance()) - } - - override fun openSettingsView() { - (activity as? MainActivity)?.pushView(SettingsFragment.newInstance()) - } - - override fun openExamView() { - (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) } + override fun openView(destination: Destination) { + (activity as? MainActivity)?.pushView(destination.destinationFragment) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt new file mode 100644 index 00000000..d544f041 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreItem.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.ui.modules.more + +import io.github.wulkanowy.ui.modules.Destination + +data class MoreItem( + + val icon: Int, + + val title: Int, + + val destination: Destination +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index 92551d6e..0ebaf4c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -1,16 +1,24 @@ package io.github.wulkanowy.ui.modules.more +import io.github.wulkanowy.R +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.ui.modules.Destination import timber.log.Timber import javax.inject.Inject class MorePresenter @Inject constructor( errorHandler: ErrorHandler, - studentRepository: StudentRepository + studentRepository: StudentRepository, + preferencesRepository: PreferencesRepository ) : BasePresenter(errorHandler, studentRepository) { + private val moreAppMenuItem = preferencesRepository.appMenuItemOrder + .sortedBy { it.order } + .drop(4) + override fun onAttachView(view: MoreView) { super.onAttachView(view) view.initView() @@ -18,22 +26,10 @@ class MorePresenter @Inject constructor( loadData() } - fun onItemSelected(title: String) { - Timber.i("Select more item \"${title}\"") - view?.run { - when (title) { - messagesRes?.first -> openMessagesView() - examRes?.first -> openExamView() - homeworkRes?.first -> openHomeworkView() - noteRes?.first -> openNoteView() - conferencesRes?.first -> openConferencesView() - schoolAnnouncementRes?.first -> openSchoolAnnouncementView() - schoolAndTeachersRes?.first -> openSchoolAndTeachersView() - mobileDevicesRes?.first -> openMobileDevicesView() - settingsRes?.first -> openSettingsView() - luckyNumberRes?.first -> openLuckyNumberView() - } - } + fun onItemSelected(moreItem: MoreItem) { + Timber.i("Select more item \"${moreItem.destination.destinationType}\"") + + view?.openView(moreItem.destination) } fun onViewReselected() { @@ -43,19 +39,21 @@ class MorePresenter @Inject constructor( private fun loadData() { Timber.i("Load items for more view") - view?.run { - updateData(listOfNotNull( - messagesRes, - examRes, - homeworkRes, - noteRes, - luckyNumberRes, - conferencesRes, - schoolAnnouncementRes, - schoolAndTeachersRes, - mobileDevicesRes, - settingsRes - )) + val moreItems = moreAppMenuItem.map { + MoreItem( + icon = it.icon, + title = it.title, + destination = it.destinationType.defaultDestination + ) } + .plus( + MoreItem( + icon = R.drawable.ic_more_settings, + title = R.string.settings_title, + destination = Destination.Settings + ) + ) + + view?.updateData(moreItems) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index cb895de2..fbca97ed 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -1,53 +1,15 @@ package io.github.wulkanowy.ui.modules.more -import android.graphics.drawable.Drawable import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface MoreView : BaseView { - val messagesRes: Pair? - - val homeworkRes: Pair? - - val noteRes: Pair? - - val conferencesRes: Pair? - - val schoolAnnouncementRes: Pair? - - val schoolAndTeachersRes: Pair? - - val mobileDevicesRes: Pair? - - val settingsRes: Pair? - - val examRes: Pair? - - val luckyNumberRes: Pair? - fun initView() - fun updateData(data: List>) - - fun openSettingsView() + fun updateData(data: List) fun popView(depth: Int) - fun openMessagesView() - - fun openHomeworkView() - - fun openNoteView() - - fun openSchoolAnnouncementView() - - fun openConferencesView() - - fun openSchoolAndTeachersView() - - fun openMobileDevicesView() - - fun openExamView() - - fun openLuckyNumberView() + fun openView(destination: Destination) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt index dd622344..cb54b384 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteFragment.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @AndroidEntryPoint class NoteFragment : BaseFragment(R.layout.fragment_note), NoteView, - MainView.TitledView { + MainView.TitledView, MainView.MainChildView { @Inject lateinit var presenter: NotePresenter @@ -112,6 +112,14 @@ class NoteFragment : BaseFragment(R.layout.fragment_note), binding.noteSwipe.isRefreshing = show } + override fun onFragmentReselected() { + if (::presenter.isInitialized) presenter.onFragmentReselected() + } + + override fun resetView() { + binding.noteRecycler.smoothScrollToPosition(0) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt index 440565e1..62ad347f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -121,4 +121,10 @@ class NotePresenter @Inject constructor( } .launch("update_note") } + + fun onFragmentReselected() { + if (view?.isViewEmpty == false) { + view?.resetView() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt index 9fc0be94..b813b315 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteView.kt @@ -30,4 +30,6 @@ interface NoteView : BaseView { fun showRefresh(show: Boolean) fun showNoteDialog(note: Note) + + fun resetView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt new file mode 100644 index 00000000..f522913a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/AppMenuItem.kt @@ -0,0 +1,206 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.Destination +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +sealed class AppMenuItem { + + companion object { + val defaultAppMenuItemList = setOf( + DashboardAppMenuItem(), + GradeAppMenuItem(), + TimetableAppMenuItem(), + AttendanceAppMenuItem(), + ExamsAppMenuItem(), + HomeworkAppMenuItem(), + NoteAppMenuItem(), + LuckyNumberAppMenuItem(), + SchoolAnnouncementsAppMenuItem(), + SchoolAndTeachersAppMenuItem(), + MobileDevicesAppMenuItem(), + ConferenceAppMenuItem(), + MessageAppMenuItem() + ).sortedBy { it.order } + } + + // https://youtrack.jetbrains.com/issue/KT-38958 + abstract var order: Int + + abstract val icon: Int + + abstract val title: Int + + abstract val destinationType: Destination.Type + + @Serializable + data class DashboardAppMenuItem(override var order: Int = 0) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_dashboard + + @Transient + override val title = R.string.dashboard_title + + @Transient + override val destinationType = Destination.Type.DASHBOARD + } + + @Serializable + data class GradeAppMenuItem(override var order: Int = 1) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_grade + + @Transient + override val title = R.string.grade_title + + @Transient + override val destinationType = Destination.Type.GRADE + } + + @Serializable + data class AttendanceAppMenuItem(override var order: Int = 2) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_attendance + + @Transient + override val title = R.string.attendance_title + + @Transient + override val destinationType = Destination.Type.ATTENDANCE + } + + @Serializable + data class TimetableAppMenuItem(override var order: Int = 3) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_timetable + + @Transient + override val title = R.string.timetable_title + + @Transient + override val destinationType = Destination.Type.TIMETABLE + } + + @Serializable + data class MessageAppMenuItem(override var order: Int = 4) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_messages + + @Transient + override val title = R.string.message_title + + @Transient + override val destinationType = Destination.Type.MESSAGE + } + + @Serializable + data class ExamsAppMenuItem(override var order: Int = 5) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_main_exam + + @Transient + override val title = R.string.exam_title + + @Transient + override val destinationType = Destination.Type.EXAM + } + + @Serializable + data class HomeworkAppMenuItem(override var order: Int = 6) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_homework + + @Transient + override val title = R.string.homework_title + + @Transient + override val destinationType = Destination.Type.HOMEWORK + } + + @Serializable + data class NoteAppMenuItem(override var order: Int = 7) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_note + + @Transient + override val title = R.string.note_title + + @Transient + override val destinationType = Destination.Type.NOTE + } + + @Serializable + data class LuckyNumberAppMenuItem(override var order: Int = 8) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_lucky_number + + @Transient + override val title = R.string.lucky_number_title + + @Transient + override val destinationType = Destination.Type.LUCKY_NUMBER + } + + @Serializable + data class ConferenceAppMenuItem(override var order: Int = 9) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_conferences + + @Transient + override val title = R.string.conferences_title + + @Transient + override val destinationType = Destination.Type.CONFERENCE + } + + @Serializable + data class SchoolAnnouncementsAppMenuItem(override var order: Int = 10) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_all_about + + @Transient + override val title = R.string.school_announcement_title + + @Transient + override val destinationType = Destination.Type.SCHOOL_ANNOUNCEMENT + } + + @Serializable + data class SchoolAndTeachersAppMenuItem(override var order: Int = 11) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_schoolandteachers + + @Transient + override val title = R.string.schoolandteachers_title + + @Transient + override val destinationType = Destination.Type.SCHOOL_AND_TEACHERS + } + + @Serializable + data class MobileDevicesAppMenuItem(override var order: Int = 12) : AppMenuItem() { + + @Transient + override val icon = R.drawable.ic_more_mobile_devices + + @Transient + override val title = R.string.mobile_devices_title + + @Transient + override val destinationType = Destination.Type.MOBILE_DEVICE + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt new file mode 100644 index 00000000..49196fad --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuItemMoveCallback.kt @@ -0,0 +1,44 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import java.util.* + +class MenuItemMoveCallback( + private val menuOrderAdapter: MenuOrderAdapter, + private var onUserInteractionEndListener: (List) -> Unit = {} +) : ItemTouchHelper.Callback() { + + override fun isLongPressDragEnabled() = true + + override fun isItemViewSwipeEnabled() = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + //Not implemented + } + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val list = menuOrderAdapter.items.toMutableList() + + Collections.swap(list, viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + + menuOrderAdapter.submitList(list) + return true + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + onUserInteractionEndListener(menuOrderAdapter.items.toList()) + } +} + diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt new file mode 100644 index 00000000..6bdd2fe0 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderAdapter.kt @@ -0,0 +1,58 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.databinding.ItemMenuOrderBinding +import javax.inject.Inject + +class MenuOrderAdapter @Inject constructor() : + RecyclerView.Adapter() { + + val items = mutableListOf() + + fun submitList(newItems: List) { + val diffResult = DiffUtil.calculateDiff(DiffCallback(newItems, items.toMutableList())) + + with(items) { + clear() + addAll(newItems) + } + + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemMenuOrderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position].appMenuItem + + with(holder.binding) { + menuOrderItemTitle.setText(item.title) + menuOrderItemIcon.setImageResource(item.icon) + } + } + + class ViewHolder(val binding: ItemMenuOrderBinding) : RecyclerView.ViewHolder(binding.root) + + private class DiffCallback( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + + override fun getNewListSize() = newList.size + + override fun getOldListSize() = oldList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].appMenuItem.destinationType == newList[newItemPosition].appMenuItem.destinationType + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt new file mode 100644 index 00000000..21a7f5e8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderDividerItemDecoration.kt @@ -0,0 +1,50 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.ShapeDrawable +import android.view.View +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.forEach +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.utils.getThemeAttrColor + +class MenuOrderDividerItemDecoration(private val context: Context) : + DividerItemDecoration(context, VERTICAL) { + + private val dividerDrawable = ShapeDrawable() + .apply { + DrawableCompat.setTint(this, context.getThemeAttrColor(R.attr.colorDivider)) + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + canvas.save() + val dividerLeft = parent.paddingLeft + val dividerRight = parent.width - parent.paddingRight + + parent.forEach { + if (parent.getChildAdapterPosition(it) == 3) { + val params = it.layoutParams as RecyclerView.LayoutParams + val dividerTop = it.bottom + params.bottomMargin + val dividerBottom = dividerTop + dividerDrawable.intrinsicHeight + + dividerDrawable.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom + 30) + dividerDrawable.draw(canvas) + } + } + + canvas.restore() + } + + override fun getItemOffsets( + outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State + ) { + if (parent.getChildAdapterPosition(view) == 3) { + outRect.bottom = dividerDrawable.intrinsicHeight + 30 + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt new file mode 100644 index 00000000..e08fc5dd --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderFragment.kt @@ -0,0 +1,101 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.activity.addCallback +import androidx.core.view.MenuProvider +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.FragmentMenuOrderBinding +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 javax.inject.Inject + + +@AndroidEntryPoint +class MenuOrderFragment : BaseFragment(R.layout.fragment_menu_order), + MenuOrderView, MainView.TitledView { + + @Inject + lateinit var presenter: MenuOrderPresenter + + @Inject + lateinit var menuOrderAdapter: MenuOrderAdapter + + override val titleStringId = R.string.menu_order_title + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentMenuOrderBinding.bind(view) + presenter.onAttachView(this) + } + + override fun initView() { + val itemTouchHelper = ItemTouchHelper( + MenuItemMoveCallback(menuOrderAdapter, presenter::onDragAndDropEnd) + ) + + itemTouchHelper.attachToRecyclerView(binding.menuOrderRecycler) + + with(binding.menuOrderRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = menuOrderAdapter + addItemDecoration(MenuOrderDividerItemDecoration(context)) + addItemDecoration(DividerItemDecoration(context)) + (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + presenter.onBackSelected() + } + + initializeToolbar() + } + + private fun initializeToolbar() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId == android.R.id.home) { + presenter.onBackSelected() + return true + } + return false + } + + }, viewLifecycleOwner) + } + + override fun updateData(data: List) { + menuOrderAdapter.submitList(data) + } + + override fun restartApp() { + startActivity(MainActivity.getStartIntent(requireContext())) + requireActivity().finishAffinity() + } + + override fun popView() { + (activity as? MainActivity?)?.popView() + } + + override fun showRestartConfirmationDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.menu_order_confirm_title) + .setMessage(R.string.menu_order_confirm_content) + .setPositiveButton(R.string.menu_order_confirm_restart) { _, _ -> presenter.onConfirmRestart() } + .setNegativeButton(R.string.all_cancel) { _, _ -> presenter.onCancelRestart() } + .show() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt new file mode 100644 index 00000000..b82bc1bd --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderItem.kt @@ -0,0 +1,6 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +data class MenuOrderItem( + val appMenuItem: AppMenuItem, + val order: Int +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt new file mode 100644 index 00000000..1c90cc5e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderPresenter.kt @@ -0,0 +1,64 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import timber.log.Timber +import javax.inject.Inject + +class MenuOrderPresenter @Inject constructor( + studentRepository: StudentRepository, + errorHandler: ErrorHandler, + private val preferencesRepository: PreferencesRepository +) : BasePresenter(errorHandler, studentRepository) { + + private var updatedMenuOrderItems = emptyList() + + override fun onAttachView(view: MenuOrderView) { + super.onAttachView(view) + view.initView() + Timber.i("Menu order view was initialized") + loadData() + } + + private fun loadData() { + val savedMenuItemList = (preferencesRepository.appMenuItemOrder) + .sortedBy { it.order } + .map { MenuOrderItem(it, it.order) } + + view?.updateData(savedMenuItemList) + } + + fun onDragAndDropEnd(list: List) { + val updatedList = list.mapIndexed { index, menuOrderItem -> + menuOrderItem.copy(order = index) + } + + updatedMenuOrderItems = updatedList + view?.updateData(updatedList) + } + + fun onBackSelected() { + if (updatedMenuOrderItems.isNotEmpty()) { + view?.showRestartConfirmationDialog() + } else { + view?.popView() + } + } + + fun onConfirmRestart() { + updatedMenuOrderItems.forEach { + it.appMenuItem.apply { + order = it.order + } + } + + preferencesRepository.appMenuItemOrder = updatedMenuOrderItems.map { it.appMenuItem } + view?.restartApp() + } + + fun onCancelRestart() { + view?.popView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt new file mode 100644 index 00000000..264e68cc --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/menuorder/MenuOrderView.kt @@ -0,0 +1,16 @@ +package io.github.wulkanowy.ui.modules.settings.appearance.menuorder + +import io.github.wulkanowy.ui.base.BaseView + +interface MenuOrderView : BaseView { + + fun initView() + + fun updateData(data: List) + + fun restartApp() + + fun showRestartConfirmationDialog() + + fun popView() +} diff --git a/app/src/main/res/drawable/ic_menu_order_drag.xml b/app/src/main/res/drawable/ic_menu_order_drag.xml new file mode 100644 index 00000000..623c67d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_order_drag.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_menu_order.xml b/app/src/main/res/layout/fragment_menu_order.xml new file mode 100644 index 00000000..1773b45a --- /dev/null +++ b/app/src/main/res/layout/fragment_menu_order.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/item_menu_order.xml b/app/src/main/res/layout/item_menu_order.xml new file mode 100644 index 00000000..84510d1e --- /dev/null +++ b/app/src/main/res/layout/item_menu_order.xml @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 80a71bc7..739c1877 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -40,4 +40,5 @@ ads_privacy_policy ads_consent_data_processing ads_over_eighteen + appearance_menu_order diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38997054..d8fbe9b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ Student info Dashboard Notifications center + Menu configuartion Semester %1$d, %2$d/%3$d @@ -595,6 +596,7 @@ Undo Change Add to calendar + Cancel No lessons Choose theme @@ -616,6 +618,8 @@ Grades color scheme Subjects sorting Language + Menu configuration + Set the order of functions in the menu Notifications Other Show notifications @@ -717,6 +721,10 @@ An update has just been downloaded. Restart Update failed! Wulkanowy may not function properly. Consider updating + + Application restart + The application must restart for the changes to be saved + Restart No internet connection An error occurred. Check your device clock diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index b2da0287..62216c76 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -20,23 +20,21 @@ app:key="@string/pref_key_app_theme" app:title="@string/pref_view_app_theme" app:useSimpleSummaryProvider="true" /> - + app:key="@string/pref_key_menu_order" + app:summary="@string/pref_view_menu_order_summary" + app:title="@string/pref_view_menu_order_title" /> diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 6cfab199..460c8385 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -46,7 +46,7 @@ class MainPresenterTest { MockKAnnotations.init(this) clearMocks(mainView) - every { mainView.initView(any(), any()) } just Runs + every { mainView.initView(any(), any(), any()) } just Runs presenter = MainPresenter( errorHandler = errorHandler, studentRepository = studentRepository, From f7d12670e7903dbaffd4dc3f2b13e4eecd351679 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jan 2023 17:54:58 +0000 Subject: [PATCH 13/77] Bump firebase-bom from 31.1.1 to 31.2.0 (#2111) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 33e80058..d4e93990 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.8.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.1.1') + playImplementation platform('com.google.firebase:firebase-bom:31.2.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 277c3c7f0b641bf3c48ecfe49cff525fd45bc6bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jan 2023 17:55:16 +0000 Subject: [PATCH 14/77] Bump google-services from 4.3.14 to 4.3.15 (#2110) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5abbe57d..dfe7f927 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'com.android.tools.build:gradle:7.4.0' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.3.14' + classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.8.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' classpath "com.github.triplet.gradle:play-publisher:3.6.0" From d1b198222de672b13316edf6e95b95b2cb793efa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:52:36 +0000 Subject: [PATCH 15/77] Bump material from 1.7.0 to 1.8.0 (#2113) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d4e93990..afc0d871 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,7 +206,7 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" - implementation "com.google.android.material:material:1.7.0" + implementation "com.google.android.material:material:1.8.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.3.0' From 22dd16d278feb2db9a61f21ad3c2ebf16d33436e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:53:30 +0000 Subject: [PATCH 16/77] Bump mockk from 1.13.3 to 1.13.4 (#2112) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index afc0d871..30f75c2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ ext { android_hilt = "1.0.0" room = "2.5.0" chucker = "3.5.2" - mockk = "1.13.3" + mockk = "1.13.4" coroutines = "1.6.4" } From 6596f3226b50787737059ac61ae7afaedb188b55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:39:48 +0000 Subject: [PATCH 17/77] Bump com.google.firebase:firebase-bom from 31.2.0 to 31.2.1 (#2123) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 30f75c2a..f279c800 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.8.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.2.0') + playImplementation platform('com.google.firebase:firebase-bom:31.2.1') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 2760318f3d0744bbf713892a36200c6bc19dd25c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:40:06 +0000 Subject: [PATCH 18/77] Bump androidx.appcompat:appcompat from 1.6.0 to 1.6.1 (#2122) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f279c800..b96f38af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -196,7 +196,7 @@ dependencies { implementation "androidx.core:core-ktx:1.9.0" implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.activity:activity-ktx:1.6.1" - implementation "androidx.appcompat:appcompat:1.6.0" + implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.5" implementation "androidx.annotation:annotation:1.5.0" From 87b8989dc81c355fd08b3d9862e46b0a959b87d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:40:26 +0000 Subject: [PATCH 19/77] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.2 to 2.9.4 (#2121) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dfe7f927..633554c3 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.8.0.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730" From af6d5c3063510cf49d9d5e68aa37bb5ae26c3b1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:44:16 +0000 Subject: [PATCH 20/77] Bump work_manager from 2.7.1 to 2.8.0 (#2119) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b96f38af..e24718fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,7 +177,7 @@ huaweiPublish { } ext { - work_manager = "2.7.1" + work_manager = "2.8.0" android_hilt = "1.0.0" room = "2.5.0" chucker = "3.5.2" From 4d237d3672ae7535b15738f2b6aa8991d1a3e2cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:44:37 +0000 Subject: [PATCH 21/77] Bump com.android.tools.build:gradle from 7.4.0 to 7.4.1 (#2115) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 633554c3..50f80406 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.8.0.300' From 6f819bcb8084ce9948c8ed49c91c047b515e8d1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:44:57 +0000 Subject: [PATCH 22/77] Bump com.google.android.gms:play-services-ads from 21.4.0 to 21.5.0 (#2116) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e24718fe..edf5c7cc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -248,7 +248,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:21.4.0' + playImplementation 'com.google.android.gms:play-services-ads:21.5.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.0.300' From d21e4afad2f0aacd934eb9dba53dc83406120593 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:46:31 +0000 Subject: [PATCH 23/77] Bump com.android.tools:desugar_jdk_libs from 1.1.8 to 2.0.2 (#2118) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index edf5c7cc..5d6c1c78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -188,7 +188,7 @@ ext { dependencies { implementation "io.github.wulkanowy:sdk:1.9.2-SNAPSHOT" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" From 78ae23df684132a71c1362a573bcbae21335cecd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:57:25 +0000 Subject: [PATCH 24/77] Bump kotlin_version from 1.8.0 to 1.8.10 (#2117) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 50f80406..059540fe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.0' + kotlin_version = '1.8.10' about_libraries = '10.5.2' hilt_version = "2.44.2" } From ac446e4f91ab95bef194aec766805f7b65a7955b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:09:18 +0000 Subject: [PATCH 25/77] Bump hilt_version from 2.44.2 to 2.45 (#2120) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 059540fe..8dc23844 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.8.10' about_libraries = '10.5.2' - hilt_version = "2.44.2" + hilt_version = "2.45" } repositories { mavenCentral() From ea26a6c1c900c88d39bb84476e6ffa1578363396 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 20:57:36 +0000 Subject: [PATCH 26/77] Bump com.huawei.agconnect:agconnect-crash from 1.8.0.300 to 1.8.1.300 (#2136) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5d6c1c78..e9461c26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -251,7 +251,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:21.5.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 811f839949071cdcc7bfa61cbea7313568264e6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 20:58:33 +0000 Subject: [PATCH 27/77] Bump androidx.test.ext:junit from 1.1.4 to 1.1.5 (#2135) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e9461c26..e6f3acf9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -274,7 +274,7 @@ dependencies { androidTestImplementation "androidx.test:core:1.5.0" androidTestImplementation "androidx.test:runner:1.5.1" - androidTestImplementation "androidx.test.ext:junit:1.1.4" + androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" } From 0c2fd1d2db55c748aecf16dbf559c9b7ac7c991a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 20:58:55 +0000 Subject: [PATCH 28/77] Bump androidx.annotation:annotation from 1.5.0 to 1.6.0 (#2134) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e6f3acf9..729efc12 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -198,7 +198,7 @@ dependencies { implementation "androidx.activity:activity-ktx:1.6.1" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.5" - implementation "androidx.annotation:annotation:1.5.0" + implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.recyclerview:recyclerview:1.2.1" From 3d2816874999d7df25f4ce1e4d0adc486ccbdde8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 20:59:36 +0000 Subject: [PATCH 29/77] Bump com.huawei.agconnect:agcp from 1.8.0.300 to 1.8.1.300 (#2131) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8dc23844..ee597b0c 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.4.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.huawei.agconnect:agcp:1.8.0.300' + classpath 'com.huawei.agconnect:agcp:1.8.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" From 531c7592b2d3d43506d8c0bd1de5be2077570556 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:00:51 +0000 Subject: [PATCH 30/77] Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2132) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 729efc12..49685eee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -190,7 +190,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.9.0" From 63bd5f95cb3fb8e46bdd6e0c3a07871d5f3e5caa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:02:04 +0000 Subject: [PATCH 31/77] Bump about_libraries from 10.5.2 to 10.6.1 (#2130) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ee597b0c..33f5be3c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.10' - about_libraries = '10.5.2' + about_libraries = '10.6.1' hilt_version = "2.45" } repositories { From 3e09a1dcee5d4a711081be4d5351c682ad403555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:14:08 +0000 Subject: [PATCH 32/77] Bump com.fredporciuncula:flow-preferences from 1.8.0 to 1.9.0 (#2126) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 49685eee..1525b21c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { implementation "io.coil-kt:coil:2.2.2" implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' - implementation 'com.fredporciuncula:flow-preferences:1.8.0' + implementation 'com.fredporciuncula:flow-preferences:1.9.0' implementation 'org.apache.commons:commons-text:1.10.0' playImplementation platform('com.google.firebase:firebase-bom:31.2.1') From 5b0e47b1c5fead141ef34da28a439dde349d7120 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:14:29 +0000 Subject: [PATCH 33/77] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2127) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 33f5be3c..eed6655a 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From 9ae2ffe7aecd8bacb615a5fa900cbc42eb9afd2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:17:58 +0000 Subject: [PATCH 34/77] Bump androidx.test:runner from 1.5.1 to 1.5.2 (#2133) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1525b21c..1732d6e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -273,7 +273,7 @@ dependencies { kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version" androidTestImplementation "androidx.test:core:1.5.0" - androidTestImplementation "androidx.test:runner:1.5.1" + androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" From 6ddaeb99da22c8b33ad944487a6efbf6044c3dd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 21:29:48 +0000 Subject: [PATCH 35/77] Bump com.google.firebase:firebase-bom from 31.2.1 to 31.2.2 (#2125) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1732d6e6..a470dae8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.2.1') + playImplementation platform('com.google.firebase:firebase-bom:31.2.2') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 12a54278fc3aef03bba4afcf4f63ba67765f39dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 1 Mar 2023 22:53:42 +0100 Subject: [PATCH 36/77] New Crowdin updates (#2109) --- app/src/main/res/values-cs/strings.xml | 8 ++++ app/src/main/res/values-da-rDK/strings.xml | 8 ++++ app/src/main/res/values-de/strings.xml | 8 ++++ app/src/main/res/values-es-rES/strings.xml | 8 ++++ app/src/main/res/values-pl/strings.xml | 8 ++++ app/src/main/res/values-ru/strings.xml | 46 +++++++++++++--------- app/src/main/res/values-sk/strings.xml | 8 ++++ app/src/main/res/values-uk/strings.xml | 30 ++++++++------ 8 files changed, 94 insertions(+), 30 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4a91cc85..47f7707f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -26,6 +26,7 @@ Informace o žáku Domů Centrum oznámení + Menu configuartion Semestr %1$d, %2$d/%3$d @@ -678,6 +679,7 @@ Vrátit Změnit Přidat do kalendáře + Cancel Žádné lekce Vybrat motiv @@ -699,6 +701,8 @@ Známky barevné schéma Třídění předmětů Jazyk + Menu configuration + Set the order of functions in the menu Oznámení Jiné Zobrazit oznámení @@ -800,6 +804,10 @@ Aktualizace byla stažena. Restartovat Aktualizace selhala! Wulkanowy nemusí fungovat správně. Zvažte aktualizaci + + Application restart + The application must restart for the changes to be saved + Restart Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 66723127..3875b3d9 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -26,6 +26,7 @@ Student info Dashboard Notifications center + Menu configuartion Semester %1$d, %2$d/%3$d @@ -590,6 +591,7 @@ Undo Change Add to calendar + Cancel No lessons Choose theme @@ -611,6 +613,8 @@ Grades color scheme Subjects sorting Language + Menu configuration + Set the order of functions in the menu Notifications Other Show notifications @@ -712,6 +716,10 @@ An update has just been downloaded. Restart Update failed! Wulkanowy may not function properly. Consider updating + + Application restart + The application must restart for the changes to be saved + Restart No internet connection An error occurred. Check your device clock diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f7b8e7c4..c03181e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -26,6 +26,7 @@ Schülerinfo Übersicht Benachrichtigungszentrum + Menu configuartion Semester %1$d, %2$d/%3$d @@ -590,6 +591,7 @@ lösen Ändern Zum Kalender hinzufügen + Cancel Keine Lektionen Thema wählen @@ -611,6 +613,8 @@ Farbschema der Noten Schulfachen sortieren Sprache + Menu configuration + Set the order of functions in the menu Benachrichtigungen Sonstiges Benachrichtigungen anzeigen @@ -712,6 +716,10 @@ Ein Update wurde gerade heruntergeladen. Neustart Update fehlgeschlagen! Wulkanowy funktioniert möglicherweise nicht richtig. Überlegen Sie die Aktualisierung + + Application restart + The application must restart for the changes to be saved + Restart Keine Internetverbindung Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 66723127..3875b3d9 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -26,6 +26,7 @@ Student info Dashboard Notifications center + Menu configuartion Semester %1$d, %2$d/%3$d @@ -590,6 +591,7 @@ Undo Change Add to calendar + Cancel No lessons Choose theme @@ -611,6 +613,8 @@ Grades color scheme Subjects sorting Language + Menu configuration + Set the order of functions in the menu Notifications Other Show notifications @@ -712,6 +716,10 @@ An update has just been downloaded. Restart Update failed! Wulkanowy may not function properly. Consider updating + + Application restart + The application must restart for the changes to be saved + Restart No internet connection An error occurred. Check your device clock diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4891015d..7f0c3291 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -26,6 +26,7 @@ Informacje o uczniu Start Centrum powiadomień + Konfiguracja menu Semestr %1$d, %2$d/%3$d @@ -678,6 +679,7 @@ Cofnij Zmień Dodaj do kalendarza + Anuluj Brak lekcji Wybierz motyw @@ -699,6 +701,8 @@ Schemat kolorów ocen Sortowanie przedmiotów Język + Konfiguracja menu + Ustaw kolejność funkcji w menu Powiadomienia Inne Pokazuj powiadomienia @@ -800,6 +804,10 @@ Aktualizacja została pobrana. Uruchom ponownie Aktualizacja nie powiodła się! Wulkanowy może nie działać prawidłowo. Rozważ aktualizację + + Ponowne uruchomienie aplikacji + W celu zapisania zmian aplikacja musi zostać ponownie uruchomiona + Uruchom ponownie Brak połączenia z internetem Wystąpił błąd. Sprawdź poprawność daty w urządzeniu diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 01e43183..42e1e0bb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,8 +15,8 @@ Отладка уведомлений Разработчики Лицензии - Письма - Написать + Сообщения + Новое сообщение Новое домашнее задание Замечания и свершения Домашние задания @@ -26,6 +26,7 @@ Информация о ученике Главная Центр уведомлений + Настройка меню %1$d семестр, %2$d/%3$d @@ -55,7 +56,7 @@ Неверный symbol Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+ Данный ученик уже авторизован - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen + Symbol можно найти на странице регистрации в  Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nУбедитесь, что вы выбрали соответствующий тип дневника в поле Тип дневника UONET+ на первом экране входа Выберите учеников для авторизации в приложении Другие варианты В этом режиме не работают: счастливый номер, статистика класса по оценкам, статистика посещаемости и уроков, информация о школе и список зарегистрированных устройств @@ -72,14 +73,14 @@ Восстановить Ученик уже авторизован Стандартный - Other search locations - No active students found - Enter a different symbol + Другие места поиска + Не найдено активных учеников + Введите другой symbol - Enable notifications - Enable notifications so you don\'t miss message from teacher or new grade - Skip - Enable + Включить уведомления + Включить уведомления, чтобы вы не пропустили сообщение от учителя или новую оценку + Пропустить + Включить Менеджер аккаунтов Войти @@ -294,7 +295,7 @@ Отправленные Корзина (нет темы) - Нет писем + Нет сообщений От: Кому: Дата: %1$s @@ -304,7 +305,7 @@ Отменить выбор Перенести в корзину Удалить навсегда - Письмо успешно удалено + Сообщение успешно удалено ученик родитель опекун @@ -313,8 +314,8 @@ Печать Тема Содержание - Письмо успешно отправлено - Письма не существует + Сообщение успешно отправлено + Сообщения не существует Вы должны выбрать как минимум одного получателя Текст сообщения должен содержать как минимум 3 знака Все почтовые ящики @@ -334,8 +335,8 @@ Новые сообщения Новые сообщения - Вы хотите восстановить черновик письма? - Вы хотите восстановить черновик письма с получателями: %s? + Вы хотите восстановить черновик сообщения? + Вы хотите восстановить черновик сообщения с получателями: %s? Вы получили %1$d новое сообщение Вы получили %1$d новых сообщения @@ -493,8 +494,8 @@ Присутствует на встрече Повестка дня - Place - Topic + Место + Тема Объявления школы Нет школьных объявлений @@ -678,6 +679,7 @@ Отменить Изменить Добавить в календарь + Отменить Нет уроков Выбрать тему @@ -699,6 +701,8 @@ Цветовая схема оценок Сортировка уроков Язык + Настройка меню + Установить порядок функций в меню Уведомления Прочее Показывать уведомления @@ -780,7 +784,7 @@ Новые встречи Новые тесты Счастливый номер - Новые письма + Новые сообщения Новые замечания Новые школьные объявления Push-уведомления @@ -800,6 +804,10 @@ Только что было скачано обновление. Перезапустить Не удалось обновить! Wulkanowy может работать некорректно. Рассмотрите возможность обновления + + Перезапуск приложение + Для сохранения изменений необходимо перезапустить приложение + Перезапустить Интернет-соединение отсутствует Произошла ошибка. Проверьте время на вашем устройстве diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 4189c534..915e2ef4 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -26,6 +26,7 @@ Informácie o žiakovi Domov Centrum oznámení + Menu configuartion Semester %1$d, %2$d/%3$d @@ -678,6 +679,7 @@ Vrátiť Zmeniť Pridať do kalendára + Cancel Žiadne lekcie Vybrať motív @@ -699,6 +701,8 @@ Známky farebnú schému Triedenie predmetov Jazyk + Menu configuration + Set the order of functions in the menu Oznámenia Iné Zobraziť oznámenia @@ -800,6 +804,10 @@ Aktualizácia bola stiahnutá. Reštartovať Aktualizácia zlyhala! Wulkanowy nemusí fungovať správne. Zvážte aktualizáciu + + Application restart + The application must restart for the changes to be saved + Restart Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 99c34065..0c736904 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -26,6 +26,7 @@ Інформація про учня Головна Центр сповіщень + Конфігурація меню %1$d семестр, %2$d/%3$d @@ -55,7 +56,7 @@ Неправильний symbol Студента не знайдено. Перевірте symbol та обранний тип щоденника UONET+ Цього учня вже авторизовано - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen + Symbol можно знайти на сторінці щоденника у Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nПереконайтеся, що ви вказали відповідний щоденник у полі Тип щоденника UONET+ на першому екрані логування Виберіть учнів для авторизації в додатку Інші варіанти У цьому режимі не працюють: щасливий номер, статистика класу по оцінкам, статистика відвідуваності та уроків, інформація про школу та список зареєстрованих пристроїв @@ -72,14 +73,14 @@ Відновити Учня вже авторизовано Стандартний - Other search locations - No active students found - Enter a different symbol + Інші розташування пошуку + Активних учнів не знайдено + Введіть інший symbol - Enable notifications - Enable notifications so you don\'t miss message from teacher or new grade - Skip - Enable + Увімкнути сповіщення + Увімкнути сповіщення, щоб не пропустити лист від вчителя або нову оцінку + Пропустити + Увімкнути Змінити облікові записи Увійти @@ -493,8 +494,8 @@ Присутність на зустрічі Порядок денний - Place - Topic + Місце + Тема Оголошення школи Немає шкільних оголошень @@ -522,7 +523,7 @@ Ви впевнені, що хочете вийти з цього облікового запису? Вийти з облікового запису учня Обліковий запис учня - Обліковий запис батька + Обліковий запис родителя Змінити дані Змінити облікові записи Вибрати учня @@ -678,6 +679,7 @@ Відмінити Змінити Додати у календар + Скасувати Немаэ уроків Увібрати тему @@ -699,6 +701,8 @@ Схема кольорів оцінок Сортування предметів Мова + Конфігурація меню + Встановити порядок функцій в меню Повідомлення Інше Показувати повідомлення @@ -800,6 +804,10 @@ Щойно завантажено оновлення. Перезавантажити Помилка оновлення! Wulkanowy може не працювати належним чином. Подумайте про оновлення + + Перезавантаження додатку + Додаток потрібно перезавантажити для збереження змін + Перезавантажити Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою From fba86930feb8dea805950d2e094426b5a7c37f11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 21:46:18 +0000 Subject: [PATCH 37/77] Bump com.android.tools.build:gradle from 7.4.1 to 7.4.2 (#2140) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eed6655a..c14e0dbd 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.4.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.8.1.300' From 9ce1ba75b2ffb50baf4d1a6f44b295d998c2c690 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 21:46:37 +0000 Subject: [PATCH 38/77] Bump com.google.firebase:firebase-bom from 31.2.2 to 31.2.3 (#2141) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a470dae8..d583891a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.2.2') + playImplementation platform('com.google.firebase:firebase-bom:31.2.3') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From cc2079f4c93030d440b2e12cfeb6dc5a3b879dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 8 Mar 2023 21:36:57 +0100 Subject: [PATCH 39/77] New Crowdin updates (#2138) --- app/src/main/res/values-cs/strings.xml | 14 +++++++------- app/src/main/res/values-sk/strings.xml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 47f7707f..1897a48b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -26,7 +26,7 @@ Informace o žáku Domů Centrum oznámení - Menu configuartion + Konfigurace menu Semestr %1$d, %2$d/%3$d @@ -679,7 +679,7 @@ Vrátit Změnit Přidat do kalendáře - Cancel + Zrušit Žádné lekce Vybrat motiv @@ -701,8 +701,8 @@ Známky barevné schéma Třídění předmětů Jazyk - Menu configuration - Set the order of functions in the menu + Konfigurace menu + Nastavit pořadí funkcí v menu Oznámení Jiné Zobrazit oznámení @@ -805,9 +805,9 @@ Restartovat Aktualizace selhala! Wulkanowy nemusí fungovat správně. Zvažte aktualizaci - Application restart - The application must restart for the changes to be saved - Restart + Restartování aplikace + Pro uložení změn je nutné aplikaci restartovat + Restartovat Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 915e2ef4..8979a95f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -26,7 +26,7 @@ Informácie o žiakovi Domov Centrum oznámení - Menu configuartion + Konfigurácia menu Semester %1$d, %2$d/%3$d @@ -679,7 +679,7 @@ Vrátiť Zmeniť Pridať do kalendára - Cancel + Zrušiť Žiadne lekcie Vybrať motív @@ -701,8 +701,8 @@ Známky farebnú schému Triedenie predmetov Jazyk - Menu configuration - Set the order of functions in the menu + Konfigurácia menu + Nastaviť poradie funkcií v menu Oznámenia Iné Zobraziť oznámenia @@ -805,9 +805,9 @@ Reštartovať Aktualizácia zlyhala! Wulkanowy nemusí fungovať správne. Zvážte aktualizáciu - Application restart - The application must restart for the changes to be saved - Restart + Reštartovanie aplikácie + Pre uloženie zmien je nutné aplikáciu reštartovať + Reštartovať Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia From a350a167f3611d1b3f77b6d199a61db4f21d80a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:14:31 +0000 Subject: [PATCH 40/77] Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.5.1 to 2.6.0 (#2146) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1e4de10e..9ca1c9d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" From 060bab46e210f554759d7e3340598b225ff57739 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:14:50 +0000 Subject: [PATCH 41/77] Bump androidx.recyclerview:recyclerview from 1.2.1 to 1.3.0 (#2145) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9ca1c9d4..1eddaecc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -201,7 +201,7 @@ dependencies { implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" - implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation "androidx.recyclerview:recyclerview:1.3.0" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" From a2a31df98ee4b425c90a7c9a75a7323b28361192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 18 Mar 2023 00:41:45 +0100 Subject: [PATCH 42/77] Fix crash on deserialize parcelable array (#2149) --- .../main/java/io/github/wulkanowy/utils/BundleExtension.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt index d0d47025..d3c9f800 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.utils import android.content.Intent import android.os.Build import android.os.Bundle +import android.os.Parcelable import java.io.Serializable inline fun Bundle.serializable(key: String): T = when { @@ -15,10 +16,10 @@ inline fun Bundle.nullableSerializable(key: String): else -> @Suppress("DEPRECATION") getSerializable(key) as T? } -@Suppress("DEPRECATION", "UNCHECKED_CAST") -inline fun Bundle.parcelableArray(key: String): Array? = when { +@Suppress("UNCHECKED_CAST") +inline fun Bundle.parcelableArray(key: String): Array? = when { Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java) - else -> getParcelableArray(key) as Array? + else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array? } inline fun Intent.serializable(key: String): T = when { From b3c6e2004bd59d80c013236304612c083fe5f6cc Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 18 Mar 2023 15:10:12 +0100 Subject: [PATCH 43/77] Make GradeAverageProvider reactive to configuration changes (#1698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- .../repositories/PreferencesRepository.kt | 68 +- .../ui/modules/grade/GradeAverageProvider.kt | 148 ++-- .../modules/grade/GradeAverageProviderTest.kt | 691 +++++++++++++----- 3 files changed, 632 insertions(+), 275 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 29a65a96..f6da6a63 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -2,9 +2,11 @@ package io.github.wulkanowy.data.repositories import android.content.Context import android.content.SharedPreferences +import androidx.annotation.StringRes import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference +import com.fredporciuncula.flow.preferences.Serializer import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.* @@ -35,20 +37,29 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_attendance_present ) - val gradeAverageMode: GradeAverageMode - get() = GradeAverageMode.getByValue( - getString( - R.string.pref_key_grade_average_mode, - R.string.pref_default_grade_average_mode - ) + private val gradeAverageModePref: Preference + get() = getObjectFlow( + R.string.pref_key_grade_average_mode, + R.string.pref_default_grade_average_mode, + object : Serializer { + override fun serialize(value: GradeAverageMode) = value.value + override fun deserialize(serialized: String) = + GradeAverageMode.getByValue(serialized) + }, ) - val gradeAverageForceCalc: Boolean - get() = getBoolean( - R.string.pref_key_grade_average_force_calc, - R.bool.pref_default_grade_average_force_calc + val gradeAverageModeFlow: Flow + get() = gradeAverageModePref.asFlow() + + private val gradeAverageForceCalcPref: Preference + get() = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_grade_average_force_calc), + context.resources.getBoolean(R.bool.pref_default_grade_average_force_calc) ) + val gradeAverageForceCalcFlow: Flow + get() = gradeAverageForceCalcPref.asFlow() + val gradeExpandMode: GradeExpandMode get() = GradeExpandMode.getByValue( getString( @@ -138,12 +149,24 @@ class PreferencesRepository @Inject constructor( R.string.pref_default_grade_modifier_plus ).toDouble() + val gradePlusModifierFlow: Flow + get() = getStringFlow( + R.string.pref_key_grade_modifier_plus, + R.string.pref_default_grade_modifier_plus + ).asFlow().map { it.toDouble() } + val gradeMinusModifier: Double get() = getString( R.string.pref_key_grade_modifier_minus, R.string.pref_default_grade_modifier_minus ).toDouble() + val gradeMinusModifierFlow: Flow + get() = getStringFlow( + R.string.pref_key_grade_modifier_minus, + R.string.pref_default_grade_modifier_minus + ).asFlow().map { it.toDouble() } + val fillMessageContent: Boolean get() = getBoolean( R.string.pref_key_fill_message_content, @@ -191,11 +214,11 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_subjects_without_grades ) - val isOptionalArithmeticAverage: Boolean - get() = getBoolean( - R.string.pref_key_optional_arithmetic_average, - R.bool.pref_default_optional_arithmetic_average - ) + val isOptionalArithmeticAverageFlow: Flow + get() = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_optional_arithmetic_average), + context.resources.getBoolean(R.bool.pref_default_optional_arithmetic_average) + ).asFlow() var lasSyncDate: Instant? get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date) @@ -342,6 +365,21 @@ class PreferencesRepository @Inject constructor( private fun getLong(id: String, default: Int) = sharedPref.getLong(id, context.resources.getString(default).toLong()) + private fun getStringFlow(id: Int, default: Int) = + flowSharedPref.getString(context.getString(id), context.getString(default)) + + private fun getObjectFlow( + @StringRes id: Int, + @StringRes default: Int, + serializer: Serializer + ): Preference = flowSharedPref.getObject( + key = context.getString(id), + serializer = serializer, + defaultValue = serializer.deserialize( + flowSharedPref.getString(context.getString(default)).get() + ) + ) + private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: String, default: Int) = diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index b6733d4f..38bae376 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -12,70 +12,91 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject -@OptIn(FlowPreview::class) +@OptIn(ExperimentalCoroutinesApi::class) class GradeAverageProvider @Inject constructor( private val semesterRepository: SemesterRepository, private val gradeRepository: GradeRepository, private val preferencesRepository: PreferencesRepository ) { - private val plusModifier get() = preferencesRepository.gradePlusModifier + private data class AverageCalcParams( + val gradeAverageMode: GradeAverageMode, + val forceAverageCalc: Boolean, + val isOptionalArithmeticAverage: Boolean, + val plusModifier: Double, + val minusModifier: Double, + ) - private val minusModifier get() = preferencesRepository.gradeMinusModifier - - private val isOptionalArithmeticAverage get() = preferencesRepository.isOptionalArithmeticAverage - - fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = + fun getGradesDetailsWithAverage( + student: Student, + semesterId: Int, + forceRefresh: Boolean + ): Flow>> = combine( + flow = preferencesRepository.gradeAverageModeFlow, + flow2 = preferencesRepository.gradeAverageForceCalcFlow, + flow3 = preferencesRepository.isOptionalArithmeticAverageFlow, + flow4 = preferencesRepository.gradePlusModifierFlow, + flow5 = preferencesRepository.gradeMinusModifierFlow, + ) { gradeAverageMode, forceAverageCalc, isOptionalArithmeticAverage, plusModifier, minusModifier -> + AverageCalcParams( + gradeAverageMode = gradeAverageMode, + forceAverageCalc = forceAverageCalc, + isOptionalArithmeticAverage = isOptionalArithmeticAverage, + plusModifier = plusModifier, + minusModifier = minusModifier, + ) + }.flatMapLatest { params -> flatResourceFlow { val semesters = semesterRepository.getSemesters(student) - - when (preferencesRepository.gradeAverageMode) { + when (params.gradeAverageMode) { ONE_SEMESTER -> getGradeSubjects( student = student, semester = semesters.single { it.semesterId == semesterId }, - forceRefresh = forceRefresh + forceRefresh = forceRefresh, + params = params, ) BOTH_SEMESTERS -> calculateCombinedAverage( student = student, semesters = semesters, semesterId = semesterId, forceRefresh = forceRefresh, - averageMode = BOTH_SEMESTERS + config = params, ) ALL_YEAR -> calculateCombinedAverage( student = student, semesters = semesters, semesterId = semesterId, forceRefresh = forceRefresh, - averageMode = ALL_YEAR + config = params, ) } - }.distinctUntilChanged() + } + } private fun calculateCombinedAverage( student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean, - averageMode: GradeAverageMode + config: AverageCalcParams, ): Flow>> { - val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc val selectedSemester = semesters.single { it.semesterId == semesterId } val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } val selectedSemesterGradeSubjects = - getGradeSubjects(student, selectedSemester, forceRefresh) + getGradeSubjects(student, selectedSemester, forceRefresh, config) if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects - val firstSemesterGradeSubjects = getGradeSubjects(student, firstSemester, forceRefresh) + val firstSemesterGradeSubjects = + getGradeSubjects(student, firstSemester, forceRefresh, config) return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject -> if (firstSemesterGradeSubject.errorOrNull != null) { @@ -91,21 +112,21 @@ class GradeAverageProvider @Inject constructor( val firstSemesterSubject = firstSemesterGradeSubject.dataOrNull.orEmpty() .singleOrNull { it.subject == secondSemesterSubject.subject } - val updatedAverage = if (averageMode == ALL_YEAR) { + val updatedAverage = if (config.gradeAverageMode == ALL_YEAR) { calculateAllYearAverage( student = student, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester, - isGradeAverageForceCalc = isGradeAverageForceCalc, secondSemesterSubject = secondSemesterSubject, - firstSemesterSubject = firstSemesterSubject + firstSemesterSubject = firstSemesterSubject, + config = config, ) } else { calculateBothSemestersAverage( student = student, isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester, - isGradeAverageForceCalc = isGradeAverageForceCalc, secondSemesterSubject = secondSemesterSubject, - firstSemesterSubject = firstSemesterSubject + firstSemesterSubject = firstSemesterSubject, + config = config ) } secondSemesterSubject.copy(average = updatedAverage) @@ -117,17 +138,17 @@ class GradeAverageProvider @Inject constructor( private fun calculateAllYearAverage( student: Student, isAnyVulcanAverage: Boolean, - isGradeAverageForceCalc: Boolean, secondSemesterSubject: GradeSubject, - firstSemesterSubject: GradeSubject? - ) = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { - val updatedSecondSemesterGrades = - secondSemesterSubject.grades.updateModifiers(student) - val updatedFirstSemesterGrades = - firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() + firstSemesterSubject: GradeSubject?, + config: AverageCalcParams, + ) = if (!isAnyVulcanAverage || config.forceAverageCalc) { + val updatedSecondSemesterGrades = secondSemesterSubject.grades + .updateModifiers(student, config) + val updatedFirstSemesterGrades = firstSemesterSubject?.grades + ?.updateModifiers(student, config).orEmpty() (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( - isOptionalArithmeticAverage + config.isOptionalArithmeticAverage ) } else { secondSemesterSubject.average @@ -136,32 +157,35 @@ class GradeAverageProvider @Inject constructor( private fun calculateBothSemestersAverage( student: Student, isAnyVulcanAverage: Boolean, - isGradeAverageForceCalc: Boolean, secondSemesterSubject: GradeSubject, - firstSemesterSubject: GradeSubject? - ): Double = if (!isAnyVulcanAverage || isGradeAverageForceCalc) { - val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 + firstSemesterSubject: GradeSubject?, + config: AverageCalcParams, + ): Double { + return if (!isAnyVulcanAverage || config.forceAverageCalc) { + val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 + val secondSemesterAverage = secondSemesterSubject.grades + .updateModifiers(student, config) + .calcAverage(config.isOptionalArithmeticAverage) + val firstSemesterAverage = firstSemesterSubject?.grades + ?.updateModifiers(student, config) + ?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage - val secondSemesterAverage = secondSemesterSubject.grades.updateModifiers(student) - .calcAverage(isOptionalArithmeticAverage) - val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) - ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage + (secondSemesterAverage + firstSemesterAverage) / divider + } else { + val divider = if (secondSemesterSubject.average > 0) 2 else 1 - (secondSemesterAverage + firstSemesterAverage) / divider - } else { - val divider = if (secondSemesterSubject.average > 0) 2 else 1 - - (secondSemesterSubject.average + (firstSemesterSubject?.average - ?: secondSemesterSubject.average)) / divider + secondSemesterSubject.average.plus( + (firstSemesterSubject?.average ?: secondSemesterSubject.average) + ) / divider + } } private fun getGradeSubjects( student: Student, semester: Semester, - forceRefresh: Boolean + forceRefresh: Boolean, + params: AverageCalcParams, ): Flow>> { - val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc - return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) .mapResourceData { res -> val (details, summaries) = res @@ -172,13 +196,15 @@ class GradeAverageProvider @Inject constructor( student = student, semester = semester, grades = allGrades.toList(), - calcAverage = isAnyAverage + calcAverage = isAnyAverage, + params = params, ).map { summary -> val grades = allGrades[summary.subject].orEmpty() GradeSubject( subject = summary.subject, - average = if (!isAnyAverage || isGradeAverageForceCalc) { - grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) + average = if (!isAnyAverage || params.forceAverageCalc) { + grades.updateModifiers(student, params) + .calcAverage(params.isOptionalArithmeticAverage) } else summary.average, points = summary.pointsSum, summary = summary, @@ -195,7 +221,8 @@ class GradeAverageProvider @Inject constructor( student: Student, semester: Semester, grades: List>>, - calcAverage: Boolean + calcAverage: Boolean, + params: AverageCalcParams, ): List { if (isNotEmpty() && size > grades.size) return this @@ -211,15 +238,16 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student) - .calcAverage(isOptionalArithmeticAverage) else .0 + average = if (calcAverage) details.updateModifiers(student, params) + .calcAverage(params.isOptionalArithmeticAverage) else .0 ) } } - private fun List.updateModifiers(student: Student): List { - return if (student.loginMode == Sdk.Mode.SCRAPPER.name) { - map { it.changeModifier(plusModifier, minusModifier) } - } else this - } + private fun List.updateModifiers( + student: Student, + params: AverageCalcParams, + ): List = if (student.loginMode == Sdk.Mode.SCRAPPER.name) { + map { it.changeModifier(params.plusModifier, params.minusModifier) } + } else this } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index a6ecdc26..10c84efc 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -1,14 +1,12 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository -import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.Status @@ -20,6 +18,7 @@ import io.mockk.impl.annotations.MockK import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -31,7 +30,9 @@ import java.time.LocalDate.of class GradeAverageProviderTest { - private suspend fun Flow>.getResult() = toList()[1].dataOrNull!! + private suspend fun Flow>.getResult() = toFirstResult().let { + it.dataOrNull ?: throw it.errorOrNull ?: error("Unknown state") + } @MockK lateinit var preferencesRepository: PreferencesRepository @@ -133,19 +134,23 @@ class GradeAverageProviderTest { fun setUp() { MockKAnnotations.init(this) - every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) coEvery { semesterRepository.getSemesters(student) } returns semesters - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) - gradeAverageProvider = GradeAverageProvider(semesterRepository, gradeRepository, preferencesRepository) + gradeAverageProvider = + GradeAverageProvider(semesterRepository, gradeRepository, preferencesRepository) } @Test fun `calc current semester standard average with no weights`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, @@ -154,16 +159,26 @@ class GradeAverageProviderTest { ) } returns resourceFlow { noWeightGrades to noWeightGradesSummary } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(0.0, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 0,0 + assertEquals( + 0.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 0,0 } @Test fun `calc current semester arithmetic average with no weights`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns true - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(true) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, @@ -172,16 +187,26 @@ class GradeAverageProviderTest { ) } returns resourceFlow { noWeightGrades to noWeightGradesArithmeticSummary } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(4.0, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 4,0 + assertEquals( + 4.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 4,0 } @Test fun `calc current semester average with load from cache sequence`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) @@ -189,7 +214,13 @@ class GradeAverageProviderTest { emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -204,14 +235,18 @@ class GradeAverageProviderTest { assertEquals(1, dataOrNull?.size) } - assertEquals(3.5, items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, .0) // from details and after set custom plus/minus + assertEquals( + 3.5, + items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, + .0 + ) // from details and after set custom plus/minus } @Test - fun `calc all year semester average with delayed emit`(){ - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + fun `calc all year semester average with delayed emit`() { + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flow { @@ -224,7 +259,13 @@ class GradeAverageProviderTest { emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + false + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -235,14 +276,18 @@ class GradeAverageProviderTest { assertEquals(1, dataOrNull?.size) } - assertEquals(3.5, items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, .0) // from details and after set custom plus/minus + assertEquals( + 3.5, + items[1].dataOrNull?.single { it.subject == "Język polski" }?.average ?: 0.0, + .0 + ) // from details and after set custom plus/minus } @Test fun `calc both semesters average with grade without grade in second semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -251,7 +296,13 @@ class GradeAverageProviderTest { false ) } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } - coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + false + ) + } returns resourceFlow { listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)) to listOf( getSummary(semesters[2].semesterId, "Język polski", 2.5) ) @@ -270,9 +321,9 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average with no grade in second semester but with average in first semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -287,7 +338,15 @@ class GradeAverageProviderTest { semesters[2], false ) - } returns resourceFlow { emptyList() to listOf(getSummary(24, "Język polski", .0)) } + } returns resourceFlow { + emptyList() to listOf( + getSummary( + 24, + "Język polski", + .0 + ) + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -302,9 +361,9 @@ class GradeAverageProviderTest { @Test fun `force calc average on no grades`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -334,9 +393,9 @@ class GradeAverageProviderTest { @Test fun `force calc current semester average with default modifiers in scraper mode`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -346,21 +405,31 @@ class GradeAverageProviderTest { ) } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) // from details and after set custom plus/minus + assertEquals( + 3.5, + items.single { it.subject == "Język polski" }.average, + .0 + ) // from details and after set custom plus/minus } @Test fun `force calc current semester average with custom modifiers in scraper mode`() { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -370,21 +439,31 @@ class GradeAverageProviderTest { ) } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) // from details and after set custom plus/minus + assertEquals( + 3.5, + items.single { it.subject == "Język polski" }.average, + .0 + ) // from details and after set custom plus/minus } @Test fun `force calc current semester average with custom modifiers in api mode`() { val student = student.copy(loginMode = Sdk.Mode.API.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 // useless in this mode - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) // useless in this mode + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -394,21 +473,31 @@ class GradeAverageProviderTest { ) } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) // (from details): 3.375 + assertEquals( + 3.375, + items.single { it.subject == "Język polski" }.average, + .0 + ) // (from details): 3.375 } @Test fun `force calc current semester average with custom modifiers in hybrid mode`() { val student = student.copy(loginMode = Sdk.Mode.HYBRID.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 // useless in this mode - every { preferencesRepository.gradePlusModifier } returns .33 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) // useless in this mode + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.33) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades( @@ -418,16 +507,26 @@ class GradeAverageProviderTest { ) } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(3.375, items.single { it.subject == "Język polski" }.average, .0) // (from details): 3.375 + assertEquals( + 3.375, + items.single { it.subject == "Język polski" }.average, + .0 + ) // (from details): 3.375 } @Test fun `calc current semester average`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, @@ -436,18 +535,28 @@ class GradeAverageProviderTest { ) } returns resourceFlow { secondGrades to secondSummaries } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(2.9, items.single { it.subject == "Matematyka" }.average, .0) // from summary: 2,9 + assertEquals( + 2.9, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 2,9 assertEquals(3.4, items.single { it.subject == "Fizyka" }.average, .0) // from details: 3,4 } @Test fun `force calc current semester average`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ONE_SEMESTER) coEvery { gradeRepository.getGrades( student, @@ -456,18 +565,28 @@ class GradeAverageProviderTest { ) } returns resourceFlow { secondGrades to secondSummaries } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(2.5, items.single { it.subject == "Matematyka" }.average, .0) // from details: 2,5 + assertEquals( + 2.5, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from details: 2,5 assertEquals(3.0, items.single { it.subject == "Fizyka" }.average, .0) // from details: 3,0 } @Test fun `force calc full year average when current is first`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( student, @@ -476,18 +595,32 @@ class GradeAverageProviderTest { ) } returns resourceFlow { firstGrades to firstSummaries } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[1].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) // (from summary): 3,5 - assertEquals(3.5, items.single { it.subject == "Fizyka" }.average, .0) // (from summary): 3,5 + assertEquals( + 3.5, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summary): 3,5 + assertEquals( + 3.5, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from summary): 3,5 } @Test fun `calc full year average when current is first with load from cache sequence`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) @@ -495,7 +628,13 @@ class GradeAverageProviderTest { emit(Resource.Success(firstGrades to firstSummaries)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[1].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[1].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -511,40 +650,74 @@ class GradeAverageProviderTest { } assertEquals(2, items[2].dataOrNull?.size) - assertEquals(3.5, items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, .0) // (from summary): 3,5 - assertEquals(3.5, items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, .0) // (from summary): 3,5 + assertEquals( + 3.5, + items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, + .0 + ) // (from summary): 3,5 + assertEquals( + 3.5, + items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, + .0 + ) // (from summary): 3,5 } @Test fun `calc both semesters average`() { - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to listOf( getSummary(22, "Matematyka", 3.0), getSummary(22, "Fizyka", 3.5) ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to listOf( getSummary(22, "Matematyka", 3.5), getSummary(22, "Fizyka", 4.0) ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.25, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 3,0 + 3,5 → 3,25 - assertEquals(3.75, items.single { it.subject == "Fizyka" }.average, .0) // (from summaries ↑): 3,5 + 4,0 → 3,75 + assertEquals( + 3.25, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summaries ↑): 3,0 + 3,5 → 3,25 + assertEquals( + 3.75, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from summaries ↑): 3,5 + 4,0 → 3,75 } @Test fun `calc both semesters average when current is second with load from cache sequence`() { - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) emit( @@ -584,7 +757,13 @@ class GradeAverageProviderTest { ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -600,15 +779,23 @@ class GradeAverageProviderTest { } assertEquals(2, items[2].dataOrNull?.size) - assertEquals(3.25, items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, .0) // (from summaries ↑): 3,0 + 3,5 → 3,25 - assertEquals(3.75, items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, .0) // (from summaries ↑): 3,5 + 4,0 → 3,75 + assertEquals( + 3.25, + items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, + .0 + ) // (from summaries ↑): 3,0 + 3,5 → 3,25 + assertEquals( + 3.75, + items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, + .0 + ) // (from summaries ↑): 3,5 + 4,0 → 3,75 } @Test fun `force calc full year average`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( student, @@ -616,7 +803,13 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { firstGrades to firstSummaries } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to listOf( getSummary(22, "Matematyka", 1.1), getSummary(22, "Fizyka", 7.26) @@ -646,9 +839,9 @@ class GradeAverageProviderTest { @Test fun `calc all year average`() { - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) coEvery { gradeRepository.getGrades( student, @@ -689,9 +882,9 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when current is second with load from cache sequence`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) @@ -718,7 +911,13 @@ class GradeAverageProviderTest { ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).toList() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).toList() + } with(items[0]) { assertEquals(Status.LOADING, status) @@ -734,15 +933,23 @@ class GradeAverageProviderTest { } assertEquals(2, items[2].dataOrNull?.size) - assertEquals(3.0, items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, .0) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals( + 3.0, + items[2].dataOrNull?.single { it.subject == "Matematyka" }?.average ?: 0.0, + .0 + ) // (from details): 3,5 + 2,5 → 3,0 + assertEquals( + 3.25, + items[2].dataOrNull?.single { it.subject == "Fizyka" }?.average ?: 0.0, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 } @Test fun `force calc both semesters average when no summaries`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -773,14 +980,18 @@ class GradeAverageProviderTest { items.single { it.subject == "Matematyka" }.average, .0 ) // (from details): 3,5 + 2,5 → 3,0 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals( + 3.25, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 } @Test fun `force calc full year average when no summaries`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( @@ -820,33 +1031,59 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summaries in both semesters`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { firstGrades to listOf( getSummary(22, "Matematyka", 4.0) ) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { secondGrades to listOf( getSummary(23, "Matematyka", 3.0) ) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } assertEquals(2, items.size) - assertEquals(3.5, items.single { it.subject == "Matematyka" }.average, .0) // (from summaries ↑): 4,0 + 3,0 → 3,5 - assertEquals(3.25, items.single { it.subject == "Fizyka" }.average, .0) // (from details): 3,5 + 3,0 → 3,25 + assertEquals( + 3.5, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // (from summaries ↑): 4,0 + 3,0 → 3,5 + assertEquals( + 3.25, + items.single { it.subject == "Fizyka" }.average, + .0 + ) // (from details): 3,5 + 3,0 → 3,25 } @Test fun `calc both semesters average when missing summary in second semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -886,9 +1123,9 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summary in first semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { gradeRepository.getGrades( @@ -928,9 +1165,9 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when missing summary in first semester`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades( @@ -970,11 +1207,17 @@ class GradeAverageProviderTest { @Test fun `force calc both semesters average with different average from all grades and from two semesters`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - every { preferencesRepository.isOptionalArithmeticAverage } returns false + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { listOf( getGrade(22, "Fizyka", 5.0, weight = 2.0), getGrade(22, "Fizyka", 6.0, weight = 2.0), @@ -986,7 +1229,13 @@ class GradeAverageProviderTest { getGrade(22, "Fizyka", 6.0, weight = 2.0) ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { listOf( getGrade(23, "Fizyka", 5.0, weight = 1.0), getGrade(23, "Fizyka", 5.0, weight = 2.0), @@ -994,16 +1243,26 @@ class GradeAverageProviderTest { ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(5.2296, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,732 → 5.229636363636364 + assertEquals( + 5.2296, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,732 → 5.229636363636364 } @Test fun `force calc full year average with different average from all grades and from two semesters`() { - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { listOf( @@ -1025,58 +1284,31 @@ class GradeAverageProviderTest { ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) } - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } - assertEquals(5.5429, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,732 → .average() + assertEquals( + 5.5429, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,732 → .average() } @Test fun `force calc both semesters average with different average from all grades and from two semesters with custom modifiers`() { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .5 + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.5) - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - coEvery { semesterRepository.getSemesters(student) } returns semesters - - coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) - } - coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) - } - - val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, true).getResult() } - - assertEquals(5.2636, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,8 → 5.26363636 - } - - @Test - fun `force calc full year average with different average from all grades and from two semesters with custom modifiers`() { - val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) - - every { preferencesRepository.gradeAverageForceCalc } returns true - every { preferencesRepository.isOptionalArithmeticAverage } returns false - every { preferencesRepository.gradeMinusModifier } returns .33 - every { preferencesRepository.gradePlusModifier } returns .5 - - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { @@ -1107,6 +1339,65 @@ class GradeAverageProviderTest { ).getResult() } + assertEquals( + 5.2636, + items.single { it.subject == "Fizyka" }.average, + .0001 + ) // (from details): 5.72727272 + 4,8 → 5.26363636 + } + + @Test + fun `force calc full year average with different average from all grades and from two semesters with custom modifiers`() { + val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) + + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) + every { preferencesRepository.gradeMinusModifierFlow } returns flowOf(.33) + every { preferencesRepository.gradePlusModifierFlow } returns flowOf(.5) + + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) + coEvery { semesterRepository.getSemesters(student) } returns semesters + + coEvery { + gradeRepository.getGrades( + student, + semesters[1], + true + ) + } returns resourceFlow { + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + } + coEvery { + gradeRepository.getGrades( + student, + semesters[2], + true + ) + } returns resourceFlow { + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + } + + val items = runBlocking { + gradeAverageProvider.getGradesDetailsWithAverage( + student, + semesters[2].semesterId, + true + ).getResult() + } + assertEquals( 5.5555, items.single { it.subject == "Fizyka" }.average, @@ -1116,20 +1407,20 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when both summary have same average from vulcan and second semester has no grades`() { - every { preferencesRepository.gradeAverageForceCalc } returns false - every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS - every { preferencesRepository.isOptionalArithmeticAverage } returns false + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns - resourceFlow { firstGrades to firstSummaries } + resourceFlow { firstGrades to firstSummaries } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { listOf() to firstSummaries } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( - student, - semesters[2].semesterId, - true + student = student, + semesterId = semesters[2].semesterId, + forceRefresh = true, ).getResult() } From 220395622874c4115f39ff463b77dcfe0c23266f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:25:26 +0000 Subject: [PATCH 44/77] Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.6.0 to 2.6.1 (#2156) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1eddaecc..1d7a3bb8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" From 597d1d763e96d3523a549adbbdb344cbbe2aca8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:25:50 +0000 Subject: [PATCH 45/77] Bump androidx.activity:activity-ktx from 1.6.1 to 1.7.0 (#2155) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1d7a3bb8..55ef9cb0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ dependencies { implementation "androidx.core:core-ktx:1.9.0" implementation 'androidx.core:core-splashscreen:1.0.0' - implementation "androidx.activity:activity-ktx:1.6.1" + implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.5" implementation "androidx.annotation:annotation:1.6.0" From 6398c9a09754bdc443231bc6a1a2844777141e84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:26:39 +0000 Subject: [PATCH 46/77] Bump io.coil-kt:coil from 2.2.2 to 2.3.0 (#2153) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 55ef9cb0..64414ff1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -236,7 +236,7 @@ dependencies { implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" - implementation "io.coil-kt:coil:2.2.2" + implementation "io.coil-kt:coil:2.3.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'com.fredporciuncula:flow-preferences:1.9.0' From 97a7b34b99abd4d06b444ec2fe7abc4abcea7fdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:27:20 +0000 Subject: [PATCH 47/77] Bump com.google.firebase:firebase-bom from 31.2.3 to 31.4.0 (#2151) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 64414ff1..cce704dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.2.3') + playImplementation platform('com.google.firebase:firebase-bom:31.4.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 9afb38d5e2286a598686344f2f349891dfa9e310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:33:48 +0000 Subject: [PATCH 48/77] Bump room from 2.5.0 to 2.5.1 (#2150) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index cce704dd..fdf6fad5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -179,7 +179,7 @@ huaweiPublish { ext { work_manager = "2.8.0" android_hilt = "1.0.0" - room = "2.5.0" + room = "2.5.1" chucker = "3.5.2" mockk = "1.13.4" coroutines = "1.6.4" From a1b9ae2826947583bb2d00126a7437506560869a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:35:15 +0000 Subject: [PATCH 49/77] Bump work_manager from 2.8.0 to 2.8.1 (#2152) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fdf6fad5..ce7927bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,7 +177,7 @@ huaweiPublish { } ext { - work_manager = "2.8.0" + work_manager = "2.8.1" android_hilt = "1.0.0" room = "2.5.1" chucker = "3.5.2" From 9981f458d00f481bcaf17742a02cc0537912a0fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 02:39:18 +0000 Subject: [PATCH 50/77] Bump androidx.fragment:fragment-ktx from 1.5.5 to 1.5.6 (#2154) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ce7927bb..3fd11ccc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,7 +197,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.5.5" + implementation "androidx.fragment:fragment-ktx:1.5.6" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From 349307b6a3f186d3baa952cc8e1c977c9b6a049d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 29 Mar 2023 09:50:36 +0200 Subject: [PATCH 51/77] Remove deprecated code (#2157) --- .../wulkanowy/services/sync/SyncManager.kt | 12 +++--------- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 61574 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 18 ++++++++++++++---- gradlew.bat | 15 +++++++++------ 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index c1bed4dd..e0a136f9 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -4,18 +4,12 @@ import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.O import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.asFlow +import androidx.work.* import androidx.work.BackoffPolicy.EXPONENTIAL -import androidx.work.Constraints -import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy.KEEP -import androidx.work.ExistingPeriodicWorkPolicy.REPLACE -import androidx.work.ExistingWorkPolicy +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import androidx.work.NetworkType.CONNECTED import androidx.work.NetworkType.UNMETERED -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkInfo -import androidx.work.WorkManager import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY import io.github.wulkanowy.data.repositories.PreferencesRepository @@ -60,7 +54,7 @@ class SyncManager @Inject constructor( val serviceInterval = preferencesRepository.servicesInterval workManager.enqueueUniquePeriodicWork( - SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, + SyncWorker::class.java.simpleName, if (restart) UPDATE else KEEP, PeriodicWorkRequestBuilder(serviceInterval, MINUTES) .setInitialDelay(10, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 36987 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 z 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 VBfu2+*qg&~f-ihLS3eum{s(|${^$Sz diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f42e62f3..e8be595e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..65dcd68d 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From bb7e927065446a9e4b7d6ded4736ec61485fdba3 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Wed, 29 Mar 2023 22:14:29 +0200 Subject: [PATCH 52/77] Migrate to material3 (#1660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Borcz Co-authored-by: doteq Co-authored-by: Bartosz Bieniek --- .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- app/src/main/AndroidManifest.xml | 2 +- .../repositories/PreferencesRepository.kt | 7 - .../data/repositories/TimetableRepository.kt | 6 + .../widgets/TimetableWidgetService.kt | 8 +- .../github/wulkanowy/ui/base/BaseActivity.kt | 15 +- .../wulkanowy/ui/base/BaseDialogFragment.kt | 19 + .../github/wulkanowy/ui/base/ErrorDialog.kt | 45 +-- .../github/wulkanowy/ui/base/ThemeManager.kt | 17 +- .../wulkanowy/ui/modules/Destination.kt | 8 + .../accountdetails/AccountDetailsFragment.kt | 3 +- .../account/accountedit/AccountEditDialog.kt | 18 +- .../accountquick/AccountQuickDialog.kt | 22 +- .../ui/modules/attendance/AttendanceDialog.kt | 24 +- .../modules/attendance/AttendanceFragment.kt | 6 +- .../ui/modules/conference/ConferenceDialog.kt | 24 +- .../ui/modules/dashboard/DashboardFragment.kt | 3 +- .../adapters/DashboardGradesAdapter.kt | 6 +- .../wulkanowy/ui/modules/exam/ExamDialog.kt | 24 +- .../wulkanowy/ui/modules/exam/ExamFragment.kt | 2 +- .../ui/modules/grade/GradeFragment.kt | 3 +- .../grade/details/GradeDetailsAdapter.kt | 8 +- .../grade/details/GradeDetailsDialog.kt | 40 ++- .../grade/summary/GradeSummaryFragment.kt | 5 +- .../ui/modules/homework/HomeworkFragment.kt | 2 +- .../modules/homework/add/HomeworkAddDialog.kt | 19 +- .../details/HomeworkDetailsAdapter.kt | 18 - .../homework/details/HomeworkDetailsDialog.kt | 32 +- .../details/HomeworkDetailsPresenter.kt | 8 - .../history/LuckyNumberHistoryFragment.kt | 2 +- .../LuckyNumberWidgetConfigureActivity.kt | 23 +- .../LuckyNumberWidgetConfigurePresenter.kt | 17 +- .../LuckyNumberWidgetConfigureView.kt | 2 - .../LuckyNumberWidgetProvider.kt | 196 +++++------ .../wulkanowy/ui/modules/main/MainActivity.kt | 38 +- .../ui/modules/main/MainPresenter.kt | 11 - .../wulkanowy/ui/modules/main/MainView.kt | 2 - .../mailboxchooser/MailboxChooserDialog.kt | 20 +- .../message/send/SendMessageActivity.kt | 27 +- .../modules/message/tab/MessageTabAdapter.kt | 15 +- .../token/MobileDeviceTokenDialog.kt | 19 +- .../wulkanowy/ui/modules/note/NoteDialog.kt | 24 +- .../notifications/NotificationsFragment.kt | 4 +- .../SchoolAnnouncementDialog.kt | 27 +- .../notifications/NotificationsFragment.kt | 10 +- .../ui/modules/settings/sync/SyncFragment.kt | 7 +- .../ui/modules/timetable/TimetableAdapter.kt | 8 +- .../ui/modules/timetable/TimetableDialog.kt | 27 +- .../ui/modules/timetable/TimetableFragment.kt | 2 +- .../additional/AdditionalLessonsFragment.kt | 12 +- .../add/AdditionalLessonAddDialog.kt | 20 +- .../completed/CompletedLessonDialog.kt | 26 +- .../completed/CompletedLessonsFragment.kt | 13 +- .../TimetableWidgetConfigureActivity.kt | 27 -- .../TimetableWidgetConfigurePresenter.kt | 19 +- .../TimetableWidgetConfigureView.kt | 2 - .../timetablewidget/TimetableWidgetFactory.kt | 328 ++++++++---------- .../TimetableWidgetProvider.kt | 161 ++++----- .../wulkanowy/utils/LifecycleAwareVariable.kt | 13 +- .../io/github/wulkanowy/utils/RefreshUtils.kt | 5 + .../drawable-night/background_header_note.xml | 5 - .../background_grade_details_rounded.xml | 5 + ...ackground_grade_details_weight_rounded.xml | 5 + ..._dark.xml => background_grade_rounded.xml} | 4 +- ...xml => background_grade_small_rounded.xml} | 6 +- .../res/drawable/background_header_note.xml | 2 +- ... background_luckynumber_widget_button.xml} | 4 +- .../background_luckynumber_widget_dark.xml | 6 - .../background_material_alert_dialog.xml | 26 ++ .../background_timetable_widget_avatar.xml | 6 + .../background_widget_header_timetable.xml | 7 - ...ackground_widget_header_timetable_dark.xml | 7 - .../background_widget_item_timetable.xml | 6 +- .../drawable/background_widget_timetable.xml | 6 +- app/src/main/res/drawable/ic_chevron_left.xml | 13 +- .../main/res/drawable/ic_chevron_right.xml | 13 +- app/src/main/res/drawable/ic_history.xml | 9 + .../main/res/drawable/ic_scale_balance.xml | 7 + .../res/drawable/ic_timetable_widget_swap.xml | 9 + .../main/res/drawable/ic_widget_chevron.png | Bin 130 -> 0 bytes .../main/res/drawable/ic_widget_chevron.xml | 9 + .../img_luckynumber_widget_preview.png | Bin 19550 -> 3702 bytes .../drawable/img_timetable_widget_preview.png | Bin 25538 -> 21111 bytes app/src/main/res/drawable/shape_badge.xml | 9 + .../layout-v31/widget_timetable_preview.xml | 96 +++++ app/src/main/res/layout/activity_main.xml | 19 +- .../main/res/layout/activity_send_message.xml | 24 +- .../main/res/layout/dialog_account_edit.xml | 31 +- .../main/res/layout/dialog_account_quick.xml | 2 +- .../main/res/layout/dialog_additional_add.xml | 115 +++--- .../main/res/layout/dialog_ads_consent.xml | 2 +- app/src/main/res/layout/dialog_attendance.xml | 58 ++-- app/src/main/res/layout/dialog_conference.xml | 68 ++-- app/src/main/res/layout/dialog_exam.xml | 68 ++-- app/src/main/res/layout/dialog_excuse.xml | 2 +- app/src/main/res/layout/dialog_grade.xml | 105 +++--- app/src/main/res/layout/dialog_homework.xml | 97 +++--- .../main/res/layout/dialog_homework_add.xml | 115 +++--- .../res/layout/dialog_lesson_completed.xml | 78 ++--- .../main/res/layout/dialog_mobile_device.xml | 19 +- app/src/main/res/layout/dialog_note.xml | 71 ++-- .../res/layout/dialog_school_announcement.xml | 53 ++- app/src/main/res/layout/dialog_timetable.xml | 95 ++--- .../res/layout/fragment_account_details.xml | 4 +- .../main/res/layout/fragment_attendance.xml | 3 +- .../layout/fragment_attendance_summary.xml | 2 +- .../main/res/layout/fragment_conference.xml | 2 +- .../main/res/layout/fragment_contributor.xml | 2 +- .../main/res/layout/fragment_dashboard.xml | 2 +- app/src/main/res/layout/fragment_exam.xml | 2 +- app/src/main/res/layout/fragment_grade.xml | 3 +- .../res/layout/fragment_grade_details.xml | 2 +- .../res/layout/fragment_grade_statistics.xml | 2 +- .../res/layout/fragment_grade_summary.xml | 2 +- app/src/main/res/layout/fragment_homework.xml | 4 +- .../res/layout/fragment_login_advanced.xml | 14 +- .../main/res/layout/fragment_login_form.xml | 16 +- .../res/layout/fragment_login_recover.xml | 6 +- .../main/res/layout/fragment_login_symbol.xml | 8 +- .../main/res/layout/fragment_logviewer.xml | 2 +- .../main/res/layout/fragment_lucky_number.xml | 4 +- .../layout/fragment_lucky_number_history.xml | 2 +- app/src/main/res/layout/fragment_message.xml | 2 +- .../res/layout/fragment_message_preview.xml | 2 +- .../main/res/layout/fragment_message_tab.xml | 2 +- .../res/layout/fragment_mobile_device.xml | 3 +- app/src/main/res/layout/fragment_note.xml | 2 +- .../layout/fragment_notifications_center.xml | 2 +- app/src/main/res/layout/fragment_school.xml | 2 +- .../layout/fragment_school_announcement.xml | 2 +- .../main/res/layout/fragment_student_info.xml | 2 +- app/src/main/res/layout/fragment_teacher.xml | 2 +- .../main/res/layout/fragment_timetable.xml | 2 +- .../layout/fragment_timetable_additional.xml | 12 +- .../layout/fragment_timetable_completed.xml | 2 +- .../res/layout/item_dashboard_account.xml | 3 +- .../layout/item_dashboard_admin_message.xml | 7 +- .../layout/item_dashboard_announcements.xml | 5 +- .../res/layout/item_dashboard_conferences.xml | 5 +- .../main/res/layout/item_dashboard_exams.xml | 5 +- .../main/res/layout/item_dashboard_grades.xml | 9 +- .../res/layout/item_dashboard_homework.xml | 5 +- .../item_dashboard_horizontal_group.xml | 4 - .../res/layout/item_dashboard_lessons.xml | 8 +- .../main/res/layout/item_grade_details.xml | 5 +- .../layout/item_homework_dialog_details.xml | 38 +- .../layout/item_login_student_select_help.xml | 7 +- .../main/res/layout/item_message_chips.xml | 16 +- .../res/layout/item_notifications_center.xml | 5 +- .../main/res/layout/item_widget_timetable.xml | 173 +++++---- .../res/layout/item_widget_timetable_dark.xml | 114 ------ .../layout/item_widget_timetable_footer.xml | 12 + .../layout/item_widget_timetable_small.xml | 98 ------ .../item_widget_timetable_small_dark.xml | 97 ------ .../res/layout/layout_preference_switch.xml | 5 + .../res/layout/subitem_dashboard_grades.xml | 4 +- .../layout/subitem_dashboard_small_grade.xml | 3 +- .../main/res/layout/widget_luckynumber.xml | 83 ++--- .../res/layout/widget_luckynumber_dark.xml | 67 ---- app/src/main/res/layout/widget_timetable.xml | 168 +++++---- .../main/res/layout/widget_timetable_dark.xml | 99 ------ .../main/res/menu/action_menu_dashboard.xml | 6 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- app/src/main/res/values-night-v31/styles.xml | 65 ++++ app/src/main/res/values-night/styles.xml | 57 ++- app/src/main/res/values-v23/styles.xml | 9 +- app/src/main/res/values-v26/styles.xml | 8 - app/src/main/res/values-v27/styles.xml | 16 + app/src/main/res/values-v28/styles.xml | 9 - app/src/main/res/values-v29/styles.xml | 9 - app/src/main/res/values-v31/styles.xml | 60 ++++ app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 53 ++- .../main/res/values/preferences_defaults.xml | 1 - app/src/main/res/values/preferences_keys.xml | 1 - app/src/main/res/values/strings.xml | 83 +++++ app/src/main/res/values/styles.xml | 49 ++- .../res/xml/provider_widget_lucky_number.xml | 8 +- .../res/xml/provider_widget_timetable.xml | 5 +- 179 files changed, 2019 insertions(+), 2372 deletions(-) delete mode 100644 app/src/main/res/drawable-night/background_header_note.xml create mode 100644 app/src/main/res/drawable/background_grade_details_rounded.xml create mode 100644 app/src/main/res/drawable/background_grade_details_weight_rounded.xml rename app/src/main/res/drawable/{background_widget_timetable_dark.xml => background_grade_rounded.xml} (74%) rename app/src/main/res/drawable/{background_widget_item_timetable_dark.xml => background_grade_small_rounded.xml} (51%) rename app/src/main/res/drawable/{background_luckynumber_widget.xml => background_luckynumber_widget_button.xml} (65%) delete mode 100644 app/src/main/res/drawable/background_luckynumber_widget_dark.xml create mode 100644 app/src/main/res/drawable/background_material_alert_dialog.xml create mode 100644 app/src/main/res/drawable/background_timetable_widget_avatar.xml delete mode 100644 app/src/main/res/drawable/background_widget_header_timetable.xml delete mode 100644 app/src/main/res/drawable/background_widget_header_timetable_dark.xml create mode 100644 app/src/main/res/drawable/ic_history.xml create mode 100644 app/src/main/res/drawable/ic_scale_balance.xml create mode 100644 app/src/main/res/drawable/ic_timetable_widget_swap.xml delete mode 100644 app/src/main/res/drawable/ic_widget_chevron.png create mode 100644 app/src/main/res/drawable/ic_widget_chevron.xml create mode 100644 app/src/main/res/drawable/shape_badge.xml create mode 100644 app/src/main/res/layout-v31/widget_timetable_preview.xml delete mode 100644 app/src/main/res/layout/item_widget_timetable_dark.xml create mode 100644 app/src/main/res/layout/item_widget_timetable_footer.xml delete mode 100644 app/src/main/res/layout/item_widget_timetable_small.xml delete mode 100644 app/src/main/res/layout/item_widget_timetable_small_dark.xml create mode 100644 app/src/main/res/layout/layout_preference_switch.xml delete mode 100644 app/src/main/res/layout/widget_luckynumber_dark.xml delete mode 100644 app/src/main/res/layout/widget_timetable_dark.xml create mode 100644 app/src/main/res/values-night-v31/styles.xml delete mode 100644 app/src/main/res/values-v26/styles.xml create mode 100644 app/src/main/res/values-v27/styles.xml delete mode 100644 app/src/main/res/values-v28/styles.xml delete mode 100644 app/src/main/res/values-v29/styles.xml create mode 100644 app/src/main/res/values-v31/styles.xml diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml index b7b756b9..9c21d49d 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3773093b..174c9a1f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -72,7 +72,7 @@ android:name=".ui.modules.message.send.SendMessageActivity" android:configChanges="orientation|screenSize" android:label="@string/send_message_title" - android:theme="@style/WulkanowyTheme.MessageSend" + android:theme="@style/WulkanowyTheme.NoActionBar" android:windowSoftInputMode="adjustResize" /> ) = timetableAdditionalDb.insertAll(additionalList) diff --git a/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt b/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt index 45cd2b04..d48556fa 100644 --- a/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt +++ b/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.widget.RemoteViewsService import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.SharedPrefProvider -import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository @@ -24,14 +23,13 @@ class TimetableWidgetService : RemoteViewsService() { @Inject lateinit var semesterRepo: SemesterRepository - @Inject - lateinit var prefRepository: PreferencesRepository - @Inject lateinit var sharedPref: SharedPrefProvider override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { Timber.d("TimetableWidgetFactory created") - return TimetableWidgetFactory(timetableRepo, studentRepo, semesterRepo, prefRepository, sharedPref, applicationContext, intent) + return TimetableWidgetFactory( + timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 075557a5..7914df81 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -4,9 +4,9 @@ import android.app.ActivityManager import android.os.Bundle import android.view.View import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R @@ -30,6 +30,8 @@ abstract class BaseActivity, VB : ViewBinding> : protected var messageContainer: View? = null + protected var messageAnchor: View? = null + abstract var presenter: T override fun onCreate(savedInstanceState: Bundle?) { @@ -48,6 +50,7 @@ abstract class BaseActivity, VB : ViewBinding> : if (messageContainer != null) { Snackbar.make(messageContainer!!, text, LENGTH_LONG) .setAction(R.string.all_details) { showErrorDetailsDialog(error) } + .apply { messageAnchor?.let { anchorView = it } } .show() } else showMessage(text) } @@ -57,12 +60,15 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun showMessage(text: String) { - if (messageContainer != null) Snackbar.make(messageContainer!!, text, LENGTH_LONG).show() - else Toast.makeText(this, text, Toast.LENGTH_LONG).show() + if (messageContainer != null) { + Snackbar.make(messageContainer!!, text, LENGTH_LONG) + .apply { messageAnchor?.let { anchorView = it } } + .show() + } else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } override fun showExpiredDialog() { - AlertDialog.Builder(this) + MaterialAlertDialogBuilder(this) .setTitle(R.string.main_session_expired) .setMessage(R.string.main_session_relogin) .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } @@ -74,6 +80,7 @@ abstract class BaseActivity, VB : ViewBinding> : messageContainer?.let { Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG) .setAction(R.string.all_change) { openInternetBrowser(redirectUrl) } + .apply { messageAnchor?.let { anchorView = it } } .show() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 25a53395..561d181a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -1,8 +1,14 @@ package io.github.wulkanowy.ui.base +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.widget.Toast +import androidx.annotation.CallSuper import androidx.fragment.app.DialogFragment import androidx.viewbinding.ViewBinding +import com.google.android.material.elevation.SurfaceColors import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.lifecycleAwareVariable import javax.inject.Inject @@ -38,6 +44,19 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + view.setBackgroundColor(SurfaceColors.SURFACE_3.getColor(requireContext())) + } + + @CallSuper + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = binding.root + override fun onResume() { super.onResume() analyticsHelper.setCurrentScreen(requireActivity(), this::class.simpleName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt index fe0e6469..679d904a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt @@ -4,13 +4,13 @@ import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.os.Bundle +import android.view.View import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService import androidx.core.os.bundleOf import androidx.core.view.isGone -import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -20,7 +20,7 @@ import io.github.wulkanowy.utils.* import javax.inject.Inject @AndroidEntryPoint -class ErrorDialog : DialogFragment() { +class ErrorDialog : BaseDialogFragment() { @Inject lateinit var appInfo: AppInfo @@ -28,6 +28,8 @@ class ErrorDialog : DialogFragment() { @Inject lateinit var preferencesRepository: PreferencesRepository + private lateinit var error: Throwable + companion object { private const val ARGUMENT_KEY = "error" @@ -36,32 +38,31 @@ class ErrorDialog : DialogFragment() { } } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val error = requireArguments().serializable(ARGUMENT_KEY) - - val binding = DialogErrorBinding.inflate(layoutInflater) - binding.bindErrorDetails(error) - - return getAlertDialog(binding, error).apply { - enableReportButtonIfErrorIsReportable(error) - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + error = requireArguments().serializable(ARGUMENT_KEY) } - private fun getAlertDialog(binding: DialogErrorBinding, error: Throwable): AlertDialog { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireContext()).apply { val errorStacktrace = error.stackTraceToString() setTitle(R.string.all_details) - setView(binding.root) + setView(DialogErrorBinding.inflate(layoutInflater).apply { binding = this }.root) setNeutralButton(R.string.about_feedback) { _, _ -> openConfirmDialog { openEmailClient(errorStacktrace) } } setNegativeButton(android.R.string.cancel) { _, _ -> } setPositiveButton(android.R.string.copy) { _, _ -> copyErrorToClipboard(errorStacktrace) } - }.create() + }.create().apply { + setOnShowListener { + getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = error.isShouldBeReported() + } + } } - private fun DialogErrorBinding.bindErrorDetails(error: Throwable) { - return with(this) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(binding) { errorDialogHumanizedMessage.text = resources.getErrorString(error) errorDialogErrorMessage.text = error.localizedMessage errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() @@ -70,12 +71,6 @@ class ErrorDialog : DialogFragment() { } } - private fun AlertDialog.enableReportButtonIfErrorIsReportable(error: Throwable) { - setOnShowListener { - getButton(AlertDialog.BUTTON_NEUTRAL).isEnabled = error.isShouldBeReported() - } - } - private fun copyErrorToClipboard(errorStacktrace: String) { val clip = ClipData.newPlainText("Error details", errorStacktrace) requireActivity().getSystemService()?.setPrimaryClip(clip) @@ -83,7 +78,7 @@ class ErrorDialog : DialogFragment() { } private fun openConfirmDialog(callback: () -> Unit) { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.dialog_error_check_update) .setMessage(R.string.dialog_error_check_update_message) .setNeutralButton(R.string.about_feedback) { _, _ -> callback() } @@ -113,8 +108,4 @@ class ErrorDialog : DialogFragment() { } ) } - - private fun showMessage(text: String) { - Toast.makeText(requireContext(), text, LENGTH_LONG).show() - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index e1c23457..f42f315c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -6,15 +6,14 @@ import android.content.pm.PackageManager.GET_ACTIVITIES import android.os.Build import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES +import com.google.android.material.color.DynamicColors import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.AppTheme import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.ui.modules.login.LoginActivity +import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity import javax.inject.Inject import javax.inject.Singleton @@ -28,18 +27,19 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer when (activity) { is MainActivity -> activity.setTheme(R.style.WulkanowyTheme_Black) is LoginActivity -> activity.setTheme(R.style.WulkanowyTheme_Login_Black) - is SendMessageActivity -> activity.setTheme(R.style.WulkanowyTheme_MessageSend_Black) } } + } else if (activity is TimetableWidgetConfigureActivity || activity is LuckyNumberWidgetConfigureActivity) { + DynamicColors.applyToActivityIfAvailable(activity) } } fun applyDefaultTheme() { AppCompatDelegate.setDefaultNightMode( when (preferencesRepository.appTheme) { - AppTheme.LIGHT -> MODE_NIGHT_NO - AppTheme.DARK, AppTheme.BLACK -> MODE_NIGHT_YES - AppTheme.SYSTEM -> MODE_NIGHT_FOLLOW_SYSTEM + AppTheme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + AppTheme.DARK, AppTheme.BLACK -> AppCompatDelegate.MODE_NIGHT_YES + AppTheme.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } ) } @@ -52,7 +52,6 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black - || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black } @Suppress("DEPRECATION") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt index 958be5a7..f0969fac 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.luckynumber.history.LuckyNumberHistoryFragment import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment import io.github.wulkanowy.ui.modules.more.MoreFragment @@ -43,6 +44,7 @@ sealed class Destination { SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), SCHOOL_AND_TEACHERS(SchoolAndTeachers), LUCKY_NUMBER(LuckyNumber), + LUCKY_NUMBER_HISTORY(LuckyNumberHistory), MORE(More), MESSAGE(Message), MOBILE_DEVICE(MobileDevice), @@ -118,6 +120,12 @@ sealed class Destination { override val destinationFragment get() = LuckyNumberFragment.newInstance() } + @Serializable + object LuckyNumberHistory : Destination() { + override val destinationType get() = Type.LUCKY_NUMBER_HISTORY + override val destinationFragment get() = LuckyNumberHistoryFragment.newInstance() + } + @Serializable object More : Destination() { override val destinationType get() = Type.MORE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index 41b97b07..d6bc6154 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import androidx.core.view.get import androidx.core.view.isVisible +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student @@ -114,7 +115,7 @@ class AccountDetailsFragment : override fun showLogoutConfirmDialog() { context?.let { - AlertDialog.Builder(it) + MaterialAlertDialogBuilder(it) .setTitle(R.string.account_logout_student) .setMessage(R.string.account_confirm) .setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt index 6e2bc8c4..4229579c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditDialog.kt @@ -1,11 +1,11 @@ package io.github.wulkanowy.ui.modules.account.accountedit +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.databinding.DialogAccountEditBinding @@ -31,16 +31,12 @@ class AccountEditDialog : BaseDialogFragment(), Accoun } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = DialogAccountEditBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogAccountEditBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt index d23978f5..2d2dccec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountquick/AccountQuickDialog.kt @@ -1,11 +1,11 @@ package io.github.wulkanowy.ui.modules.account.accountquick +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.DialogAccountQuickBinding @@ -36,19 +36,17 @@ class AccountQuickDialog : BaseDialogFragment(), Acco } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogAccountQuickBinding.inflate(layoutInflater) + .apply { binding = this }.root + ) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root - - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) val studentsWithSemesters = requireArguments() .serializable>(STUDENTS_ARGUMENT_KEY).toList() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index eab24f91..c0026bee 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -1,21 +1,20 @@ package io.github.wulkanowy.ui.modules.attendance +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.descriptionRes -import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class AttendanceDialog : DialogFragment() { - - private var binding: DialogAttendanceBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class AttendanceDialog : BaseDialogFragment() { private lateinit var attendance: Attendance @@ -30,15 +29,14 @@ class AttendanceDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) attendance = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogAttendanceBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 21f30b04..a73c2606 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -4,10 +4,10 @@ import android.content.DialogInterface.BUTTON_POSITIVE import android.os.Bundle import android.view.* import android.view.View.* -import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ActionMode import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance @@ -124,7 +124,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.elevation = requireContext().dpToPx(8f) + attendanceNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -228,7 +228,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag override fun showExcuseDialog() { val dialogBinding = DialogExcuseBinding.inflate(LayoutInflater.from(context)) - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.attendance_excuse_title) .setView(dialogBinding.root) .setNegativeButton(android.R.string.cancel) { _, _ -> } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt index 7834b6e8..c532377e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt @@ -1,21 +1,20 @@ package io.github.wulkanowy.ui.modules.conference +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.core.view.isVisible -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.databinding.DialogConferenceBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class ConferenceDialog : DialogFragment() { - - private var binding: DialogConferenceBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class ConferenceDialog : BaseDialogFragment() { private lateinit var conference: Conference @@ -30,15 +29,14 @@ class ConferenceDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) conference = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogConferenceBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index cd66d6c2..ce17c763 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentDashboardBinding @@ -148,7 +149,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme val values = requireContext().resources.getStringArray(R.array.dashboard_tile_values) val selectedItemsState = values.map { value -> selectedItems.any { it.name == value } } - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.pref_dashboard_appearance_tiles_title) .setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> } .setPositiveButton(android.R.string.ok) { dialog, _ -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt index d00df9d4..d821de53 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardGradesAdapter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard.adapters +import android.content.res.ColorStateList import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView @@ -8,6 +9,7 @@ import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.databinding.SubitemDashboardGradesBinding import io.github.wulkanowy.databinding.SubitemDashboardSmallGradeBinding import io.github.wulkanowy.utils.getBackgroundColor +import io.github.wulkanowy.utils.getCompatColor class DashboardGradesAdapter : RecyclerView.Adapter() { @@ -37,7 +39,9 @@ class DashboardGradesAdapter : RecyclerView.Adapter() { private lateinit var exam: Exam @@ -32,15 +31,14 @@ class ExamDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) exam = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogExamBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogExamBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index 3d42bd00..0123e234 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -62,7 +62,7 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.elevation = requireContext().dpToPx(8f) + examNavContainer.elevation = requireContext().dpToPx(3f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index 15df47a1..7ce07eb6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -141,7 +142,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) } .toTypedArray() - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setSingleChoiceItems(choices, selectedIndex) { dialog, which -> presenter.onSemesterSelected(which) dialog.dismiss() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt index e5c3bb63..15b5db03 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.details import android.annotation.SuppressLint +import android.content.res.ColorStateList import android.content.res.Resources import android.view.LayoutInflater import android.view.View @@ -17,9 +18,10 @@ import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseExpandableAdapter import io.github.wulkanowy.utils.getBackgroundColor +import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber -import java.util.BitSet +import java.util.* import javax.inject.Inject class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { @@ -203,7 +205,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter grade.description diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt index a1ef2ec5..39f72f8b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt @@ -1,22 +1,23 @@ package io.github.wulkanowy.ui.modules.grade.details +import android.app.Dialog +import android.content.res.ColorStateList import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE -import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.os.bundleOf -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.databinding.DialogGradeBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.* - -class GradeDetailsDialog : DialogFragment() { - - private var binding: DialogGradeBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class GradeDetailsDialog : BaseDialogFragment() { private lateinit var grade: Grade @@ -38,16 +39,15 @@ class GradeDetailsDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) grade = requireArguments().serializable(ARGUMENT_KEY) gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogGradeBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogGradeBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -55,10 +55,9 @@ class GradeDetailsDialog : DialogFragment() { with(binding) { gradeDialogSubject.text = grade.subject - gradeDialogColorAndWeightValue.run { - text = context.getString(R.string.grade_weight_value, grade.weight) - setBackgroundResource(grade.getGradeColor()) - } + gradeDialogWeightValue.text = grade.weight + gradeDialogWeightLayout.backgroundTintList = + ColorStateList.valueOf(requireContext().getCompatColor(grade.getGradeColor())) gradeDialogDateValue.text = grade.date.toFormattedString() gradeDialogColorValue.text = getString(grade.colorStringId) @@ -72,7 +71,12 @@ class GradeDetailsDialog : DialogFragment() { gradeDialogValue.run { text = grade.entry - setBackgroundResource(grade.getBackgroundColor(gradeColorTheme)) + backgroundTintList = ColorStateList.valueOf( + ContextCompat.getColor( + requireContext(), + grade.getBackgroundColor(gradeColorTheme) + ) + ) } gradeDialogTeacherValue.text = grade.teacher.ifBlank { getString(R.string.all_no_data) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index 3810902f..abd0b13c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -7,6 +7,7 @@ import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.GradeSummary @@ -118,7 +119,7 @@ class GradeSummaryFragment : } override fun showCalculatedAverageHelpDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.grade_summary_calculated_average_help_dialog_title) .setMessage(R.string.grade_summary_calculated_average_help_dialog_message) .setPositiveButton(R.string.all_close) { _, _ -> } @@ -126,7 +127,7 @@ class GradeSummaryFragment : } override fun showFinalAverageHelpDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.grade_summary_final_average_help_dialog_title) .setMessage(R.string.grade_summary_final_average_help_dialog_message) .setPositiveButton(R.string.all_close) { _, _ -> } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index 9d5130e4..0381acf3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -67,7 +67,7 @@ class HomeworkFragment : BaseFragment(R.layout.fragment openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } - homeworkNavContainer.elevation = requireContext().dpToPx(8f) + homeworkNavContainer.elevation = requireContext().dpToPx(3f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt index c2aff2b1..c51370ea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.homework.add +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.widget.doOnTextChanged +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogHomeworkAddBinding @@ -21,20 +21,15 @@ class HomeworkAddDialog : BaseDialogFragment(), Homewo @Inject lateinit var presenter: HomeworkAddPresenter - // todo: move it to presenter + //todo: move it to presenter private var date: LocalDate? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogHomeworkAddBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index e03707a5..1ad2a0e3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -31,14 +31,8 @@ class HomeworkDetailsAdapter @Inject constructor() : attachments = value?.attachments.orEmpty() } - var isHomeworkFullscreen = false - var onAttachmentClickListener: (url: String) -> Unit = {} - var onFullScreenClickListener = {} - - var onFullScreenExitClickListener = {} - var onDeleteClickListener: (homework: Homework) -> Unit = {} override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 @@ -82,18 +76,6 @@ class HomeworkDetailsAdapter @Inject constructor() : homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE - homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE - homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE - homeworkDialogFullScreen.setOnClickListener { - homeworkDialogFullScreen.visibility = GONE - homeworkDialogFullScreenExit.visibility = VISIBLE - onFullScreenClickListener() - } - homeworkDialogFullScreenExit.setOnClickListener { - homeworkDialogFullScreen.visibility = VISIBLE - homeworkDialogFullScreenExit.visibility = GONE - onFullScreenExitClickListener() - } homeworkDialogDelete.setOnClickListener { onDeleteClickListener(homework!!) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index 5e2cc65d..1f9bc881 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -1,14 +1,12 @@ package io.github.wulkanowy.ui.modules.homework.details import android.annotation.SuppressLint +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework @@ -43,15 +41,14 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) homework = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogHomeworkBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -67,26 +64,11 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew homeworkDialogClose.setOnClickListener { dismiss() } } - if (presenter.isHomeworkFullscreen) { - dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT) - } else { - dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) - } - with(binding.homeworkDialogRecycler) { layoutManager = LinearLayoutManager(context) adapter = detailsAdapter.apply { onAttachmentClickListener = { context.openInternetBrowser(it, ::showMessage) } - onFullScreenClickListener = { - dialog?.window?.setLayout(MATCH_PARENT, MATCH_PARENT) - presenter.isHomeworkFullscreen = true - } - onFullScreenExitClickListener = { - dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) - presenter.isHomeworkFullscreen = false - } onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } - isHomeworkFullscreen = presenter.isHomeworkFullscreen homework = this@HomeworkDetailsDialog.homework } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt index e76df6bd..84933f06 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt @@ -5,7 +5,6 @@ import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.HomeworkRepository -import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter @@ -19,15 +18,8 @@ class HomeworkDetailsPresenter @Inject constructor( studentRepository: StudentRepository, private val homeworkRepository: HomeworkRepository, private val analytics: AnalyticsHelper, - private val preferencesRepository: PreferencesRepository ) : BasePresenter(errorHandler, studentRepository) { - var isHomeworkFullscreen - get() = preferencesRepository.isHomeworkFullscreen - set(value) { - preferencesRepository.isHomeworkFullscreen = value - } - override fun onAttachView(view: HomeworkDetailsView) { super.onAttachView(view) view.initView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index 53f06cac..a78ce5dd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -61,7 +61,7 @@ class LuckyNumberHistoryFragment : luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } - luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) + luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(3f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt index 024beff8..a2d23e54 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureActivity.kt @@ -1,16 +1,12 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget -import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE -import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID -import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS +import android.appwidget.AppWidgetManager.* import android.content.Intent -import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AlertDialog 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.databinding.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity @@ -41,7 +37,6 @@ class LuckyNumberWidgetConfigureActivity : setContentView( ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root ) - intent.extras.let { presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID)) } @@ -56,22 +51,6 @@ class LuckyNumberWidgetConfigureActivity : configureAdapter.onClickListener = presenter::onItemSelect } - override fun showThemeDialog() { - var items = arrayOf( - getString(R.string.widget_timetable_theme_light), - getString(R.string.widget_timetable_theme_dark) - ) - if (appInfo.systemVersion >= Build.VERSION_CODES.Q) items += (getString(R.string.widget_timetable_theme_system)) - - dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) - .setTitle(R.string.widget_timetable_theme_title) - .setOnDismissListener { presenter.onDismissThemeView() } - .setSingleChoiceItems(items, -1) { _, which -> - presenter.onThemeSelect(which) - } - .show() - } - override fun updateData(data: List, selectedStudentId: Long) { with(configureAdapter) { selectedId = selectedStudentId diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt index cac648da..7e53dad0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigurePresenter.kt @@ -8,7 +8,6 @@ import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getStudentWidgetKey -import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getThemeWidgetKey import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -32,20 +31,9 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( fun onItemSelect(student: Student) { selectedStudent = student - view?.showThemeDialog() - } - - fun onThemeSelect(index: Int) { - appWidgetId?.let { - sharedPref.putLong(getThemeWidgetKey(it), index.toLong()) - } registerStudent(selectedStudent) } - fun onDismissThemeView() { - view?.finishView() - } - private fun loadData() { resourceFlow { studentRepository.getSavedStudents(false) }.onEach { when (it) { @@ -56,10 +44,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor( } ?: -1 when { it.data.isEmpty() -> view?.openLoginView() - it.data.size == 1 -> { - selectedStudent = it.data.single().student - view?.showThemeDialog() - } + it.data.size == 1 -> onItemSelect(it.data.single().student) else -> view?.updateData(it.data, selectedStudentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt index b4556f7e..df13b993 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetConfigureView.kt @@ -7,8 +7,6 @@ interface LuckyNumberWidgetConfigureView : BaseView { fun initView() - fun showThemeDialog() - fun updateData(data: List, selectedStudentId: Long) fun updateLuckyNumberWidget(widgetId: Int) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index e03e3e90..bafb2d7e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -2,14 +2,12 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT -import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH import android.appwidget.AppWidgetProvider import android.content.Context import android.content.res.Configuration import android.os.Bundle -import android.view.View.GONE -import android.view.View.VISIBLE +import android.util.TypedValue.COMPLEX_UNIT_SP +import android.view.View import android.widget.RemoteViews import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -17,7 +15,6 @@ import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.LuckyNumber -import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.toFirstResult @@ -41,16 +38,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { lateinit var sharedPref: SharedPrefProvider companion object { + private const val LUCKY_NUMBER_WIDGET_MAX_SIZE = 196 - const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 + private const val LUCKY_NUMBER_PENDING_INTENT_ID = 300 + private const val LUCKY_NUMBER_HISTORY_PENDING_INTENT_ID = 301 fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" - - fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" - - fun getHeightWidgetKey(appWidgetId: Int) = "lucky_number_widget_height_$appWidgetId" - - fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" } override fun onUpdate( @@ -59,107 +52,86 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { appWidgetIds: IntArray? ) { super.onUpdate(context, appWidgetManager, appWidgetIds) - appWidgetIds?.forEach { appWidgetId -> - val luckyNumber = - getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val appIntent = PendingIntent.getActivity( - context, - LUCKY_NUMBER_PENDING_INTENT_ID, - SplashActivity.getStartIntent(context, Destination.LuckyNumber), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - if (luckyNumber is Resource.Error) { - Timber.e("Error loading lucky number for widget", luckyNumber.error) - } + val appIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.LuckyNumber), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - val remoteView = - RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) - .apply { - setTextViewText( - R.id.luckyNumberWidgetNumber, - luckyNumber.dataOrNull?.luckyNumber?.toString() ?: "#" - ) - setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) - } + val historyIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_HISTORY_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.LuckyNumberHistory), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - setStyles(remoteView, appWidgetId) - appWidgetManager.updateAppWidget(appWidgetId, remoteView) + appWidgetIds?.forEach { widgetId -> + val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) + val luckyNumberResource = getLuckyNumber(studentId, widgetId) + val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber?.toString() + val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber) + .apply { + setTextViewText(R.id.luckyNumberWidgetValue, luckyNumber ?: "-") + setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) + setOnClickPendingIntent(R.id.luckyNumberWidgetHistoryButton, historyIntent) + } + + resizeWidget(context, appWidgetManager.getAppWidgetOptions(widgetId), remoteView) + appWidgetManager.updateAppWidget(widgetId, remoteView) + } + } + + override fun onAppWidgetOptionsChanged( + context: Context?, + appWidgetManager: AppWidgetManager?, + appWidgetId: Int, + newOptions: Bundle? + ) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) + + if (context == null || newOptions == null || appWidgetManager == null) { + return + } + + val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber) + resizeWidget(context, newOptions, remoteView) + appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteView) + } + + private fun resizeWidget(context: Context, options: Bundle, remoteViews: RemoteViews) { + val (width, height) = options.getWidgetSize(context) + val size = minOf(width, height, LUCKY_NUMBER_WIDGET_MAX_SIZE).toFloat() + resizeWidgetContents(size, remoteViews) + Timber.v("LuckyNumberWidget resized: ${width}x${height} ($size)") + } + + private fun resizeWidgetContents(size: Float, remoteViews: RemoteViews) { + var historyButtonVisibility = View.VISIBLE + var luckyNumberTextSize = 72f + + if (size < 150) { + luckyNumberTextSize = 44f + historyButtonVisibility = View.GONE + } + if (size < 75) { + luckyNumberTextSize = 26f + } + + remoteViews.apply { + setTextViewTextSize(R.id.luckyNumberWidgetValue, COMPLEX_UNIT_SP, luckyNumberTextSize) + setViewVisibility(R.id.luckyNumberWidgetHistoryButton, historyButtonVisibility) } } override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { super.onDeleted(context, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> - with(sharedPref) { - delete(getHeightWidgetKey(appWidgetId)) - delete(getStudentWidgetKey(appWidgetId)) - delete(getThemeWidgetKey(appWidgetId)) - delete(getWidthWidgetKey(appWidgetId)) - } + sharedPref.delete(getStudentWidgetKey(appWidgetId)) } } - override fun onAppWidgetOptionsChanged( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int, - newOptions: Bundle? - ) { - super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) - - setStyles(remoteView, appWidgetId, newOptions) - appWidgetManager.updateAppWidget(appWidgetId, remoteView) - } - - private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) { - val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong( - getWidthWidgetKey(appWidgetId), 74 - ).toInt() - val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong( - getHeightWidgetKey(appWidgetId), 74 - ).toInt() - - with(sharedPref) { - putLong(getWidthWidgetKey(appWidgetId), width.toLong()) - putLong(getHeightWidgetKey(appWidgetId), height.toLong()) - } - - val rows = getCellsForSize(height) - val cols = getCellsForSize(width) - - Timber.d("New lucky number widget measurement: %dx%d", width, height) - Timber.d("Widget size: $cols x $rows") - - when { - 1 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = false) - 1 == cols && 1 < rows -> views.setVisibility(imageTop = true, imageLeft = false) - 1 < cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true) - 1 == cols && 1 == rows -> views.setVisibility(imageTop = true, imageLeft = false) - 2 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true) - else -> views.setVisibility(imageTop = false, imageLeft = false, title = true) - } - } - - private fun RemoteViews.setVisibility( - imageTop: Boolean, - imageLeft: Boolean, - title: Boolean = false - ) { - setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE) - setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) - setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE) - setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE) - } - - private fun getCellsForSize(size: Int): Int { - var n = 2 - while (74 * n - 30 < size) ++n - return n - 1 - } - private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking { try { val students = studentRepository.getSavedStudents() @@ -181,22 +153,24 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { Resource.Success(null) } } catch (e: Exception) { - if (e.cause !is NoCurrentStudentException) { - Timber.e(e, "An error has occurred in lucky number provider") - } + Timber.e(e, "An error has occurred in lucky number provider") Resource.Error(e) } } - private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { - val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = - context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + private fun Bundle.getWidgetSize(context: Context): Pair { + val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) + val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) + val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) + val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) - return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { - R.layout.widget_luckynumber_dark + val orientation = context.resources.configuration.orientation + val isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT + + return if (isPortrait) { + minWidth to maxHeight } else { - R.layout.widget_luckynumber + maxWidth to minHeight } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 51092376..091080a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -2,14 +2,14 @@ package io.github.wulkanowy.ui.modules.main import android.content.Context import android.content.Intent -import android.os.Build.VERSION_CODES.P +import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.ViewGroup.MarginLayoutParams import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback -import androidx.core.view.ViewCompat -import androidx.core.view.isVisible +import androidx.core.view.* import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -90,8 +90,16 @@ class MainActivity : BaseActivity(), MainVie super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowCompat.setDecorFitsSystemWindows(window, false) + binding.mainAppBar.isLifted = true + } + initializeFragmentContainer() + this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer + messageAnchor = binding.mainMessageContainer updateHelper.messageContainer = binding.mainFragmentContainer onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) { presenter.onBackPressed() @@ -187,6 +195,17 @@ class MainActivity : BaseActivity(), MainVie } } + private fun initializeFragmentContainer() { + ViewCompat.setOnApplyWindowInsetsListener(binding.mainFragmentContainer) { view, insets -> + val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + view.updateLayoutParams { + bottomMargin = if (binding.mainBottomNav.isVisible) 0 else bottomInsets.bottom + } + WindowInsetsCompat.CONSUMED + } + } + override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, pref: Preference @@ -231,20 +250,9 @@ class MainActivity : BaseActivity(), MainVie showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters)) } - override fun showActionBarElevation(show: Boolean) { - ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) - } - override fun showBottomNavigation(show: Boolean) { binding.mainBottomNav.isVisible = show - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = if (show) { - getThemeAttrColor(android.R.attr.navigationBarColor) - } else { - getThemeAttrColor(R.attr.colorSurface) - } - } + binding.mainFragmentContainer.requestApplyInsets() } override fun openMoreDestination(destination: Destination) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index d51cdac6..ae05ecf2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -14,9 +14,6 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.AccountView import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView -import io.github.wulkanowy.ui.modules.grade.GradeView -import io.github.wulkanowy.ui.modules.message.MessageView -import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AnalyticsHelper @@ -100,7 +97,6 @@ class MainPresenter @Inject constructor( fun onViewChange(destinationView: BaseView) { view?.apply { showBottomNavigation(shouldShowBottomNavigation(destinationView)) - showActionBarElevation(shouldShowActionBarElevation(destinationView)) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentStackSize?.let { @@ -110,13 +106,6 @@ class MainPresenter @Inject constructor( } } - private fun shouldShowActionBarElevation(destination: BaseView) = when (destination) { - is GradeView, - is MessageView, - is SchoolAndTeachersView -> false - else -> true - } - private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) { is AccountView, is StudentInfoView, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 03f9641d..62436f3b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -28,8 +28,6 @@ interface MainView : BaseView { fun showAccountPicker(studentWithSemesters: List) - fun showActionBarElevation(show: Boolean) - fun showBottomNavigation(show: Boolean) fun notifyMenuViewReselected() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt index 37f9a19b..8bd84f2b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt @@ -1,11 +1,11 @@ package io.github.wulkanowy.ui.modules.message.mailboxchooser +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.databinding.DialogMailboxChooserBinding @@ -37,19 +37,19 @@ class MailboxChooserDialog : BaseDialogFragment(), } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogMailboxChooserBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogMailboxChooserBinding.inflate(inflater).apply { binding = this }.root @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) presenter.onAttachView( view = this, requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false), diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index 14f3d718..28147fae 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send import android.annotation.SuppressLint -import android.app.AlertDialog import android.content.Context import android.content.Intent import android.graphics.Rect +import android.os.Build import android.os.Bundle import android.text.Spanned import android.view.Menu @@ -12,11 +12,14 @@ import android.view.MenuItem import android.view.TouchDelegate import android.view.View.GONE import android.view.View.VISIBLE +import android.view.ViewGroup import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.core.text.parseAsHtml import androidx.core.text.toHtml +import androidx.core.view.* import androidx.core.widget.doOnTextChanged +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Mailbox @@ -24,8 +27,8 @@ import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.databinding.ActivitySendMessageBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog -import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY +import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.nullableSerializable @@ -99,6 +102,13 @@ class SendMessageActivity : BaseActivity= Build.VERSION_CODES.R) { + WindowCompat.setDecorFitsSystemWindows(window, false) + binding.sendAppBar.isLifted = true + } + initializeMessageContainer() + messageContainer = binding.sendMessageContainer formRecipientsData = binding.sendMessageTo.addedChipItems as List @@ -130,6 +140,17 @@ class SendMessageActivity : BaseActivity + val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + view.updateLayoutParams { + bottomMargin = bottomInsets.bottom + } + WindowInsetsCompat.CONSUMED + } + } + private fun onMessageSubjectChange(text: CharSequence?) { formSubjectValue = text.toString() presenter.onMessageContentChange() @@ -252,7 +273,7 @@ class SendMessageActivity : BaseActivity presenter.restoreMessageParts() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt index 6df6153c..9792c708 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabAdapter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message.tab +import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.Typeface import android.view.LayoutInflater @@ -68,21 +69,23 @@ class MessageTabAdapter @Inject constructor() : } } + @SuppressLint("PrivateResource") private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = items[position] as MessageTabDataItem.FilterHeader + val context = holder.binding.root.context with(holder.binding) { - chipMailbox.text = item.selectedMailbox - ?: root.context.getString(R.string.message_chip_all_mailboxes) + chipMailbox.text = + item.selectedMailbox ?: context.getString(R.string.message_chip_all_mailboxes) chipMailbox.chipBackgroundColor = ColorStateList.valueOf( if (item.selectedMailbox == null) { - root.context.getCompatColor(R.color.mtrl_choice_chip_background_color) - } else root.context.getThemeAttrColor(android.R.attr.colorPrimary, 64) + context.getCompatColor(R.color.m3_elevated_chip_background_color) + } else context.getThemeAttrColor(R.attr.colorPrimary, 64) ) chipMailbox.setTextColor( if (item.selectedMailbox == null) { - root.context.getThemeAttrColor(android.R.attr.textColorPrimary) - } else root.context.getThemeAttrColor(android.R.attr.colorPrimary) + context.getThemeAttrColor(R.attr.colorOnSurfaceVariant) + } else context.getThemeAttrColor(R.attr.colorPrimary) ) chipMailbox.setOnClickListener { onMailboxClickListener() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt index eb420a6a..2cc2a2aa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/token/MobileDeviceTokenDialog.kt @@ -1,17 +1,17 @@ package io.github.wulkanowy.ui.modules.mobiledevice.token +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.graphics.BitmapFactory import android.os.Bundle import android.util.Base64 -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import android.widget.Toast import androidx.core.content.getSystemService +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.pojos.MobileDeviceToken @@ -31,17 +31,14 @@ class MobileDeviceTokenDialog : BaseDialogFragment(), fun newInstance() = MobileDeviceTokenDialog() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogMobileDeviceBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogMobileDeviceBinding.inflate(inflater).apply { binding = this }.root - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt index e46ab42c..0592e924 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NoteDialog.kt @@ -1,25 +1,24 @@ package io.github.wulkanowy.ui.modules.note import android.annotation.SuppressLint +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.os.bundleOf -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.databinding.DialogNoteBinding import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class NoteDialog : DialogFragment() { - - private var binding: DialogNoteBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class NoteDialog : BaseDialogFragment() { private lateinit var note: Note @@ -34,15 +33,14 @@ class NoteDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) note = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogNoteBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogNoteBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt index 163ba8cd..0763d4fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notifications/NotificationsFragment.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.modules.notifications import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts.RequestPermission -import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentNotificationsBinding @@ -41,7 +41,7 @@ class NotificationsFragment : } private fun showSettingsDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.notifications_header_title) .setMessage(R.string.notifications_header_description) .setNegativeButton(R.string.notifications_skip) { dialog, _ -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt index e33a48f0..c1c58441 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt @@ -1,21 +1,20 @@ package io.github.wulkanowy.ui.modules.schoolannouncement +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.parseUonetHtml import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString -class SchoolAnnouncementDialog : DialogFragment() { - - private var binding: DialogSchoolAnnouncementBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class SchoolAnnouncementDialog : BaseDialogFragment() { private lateinit var announcement: SchoolAnnouncement @@ -30,15 +29,17 @@ class SchoolAnnouncementDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) announcement = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogSchoolAnnouncementBinding.inflate(layoutInflater) + .apply { binding = this }.root + ) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 77a3c6cf..98ac1573 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -8,13 +8,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.thelittlefireman.appkillermanager.AppKillerManager import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException import dagger.hilt.android.AndroidEntryPoint @@ -149,7 +149,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun showFixSyncDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.pref_notify_fix_sync_issues) .setMessage(R.string.pref_notify_fix_sync_issues_message) .setNegativeButton(android.R.string.cancel) { _, _ -> } @@ -177,7 +177,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun openNotificationsPermissionDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.notifications_header_title) .setMessage(R.string.notifications_header_description) .setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ -> @@ -191,7 +191,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun openNotificationPiggyBackPermissionDialog() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.pref_notification_piggyback_popup_title)) .setMessage(getString(R.string.pref_notification_piggyback_popup_description)) .setPositiveButton(getString(R.string.pref_notification_go_to_settings)) { _, _ -> @@ -205,7 +205,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun openNotificationExactAlarmSettings() { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.pref_notification_exact_alarm_popup_title)) .setMessage(getString(R.string.pref_notification_exact_alarm_popup_descriptions)) .setPositiveButton(getString(R.string.pref_notification_go_to_settings)) { _, _ -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 8477e322..2a804d9f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.View import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity @@ -75,7 +76,11 @@ class SyncFragment : PreferenceFragmentCompat(), } override fun showMessage(text: String) { - (activity as? BaseActivity<*, *>)?.showMessage(text) + Snackbar.make(requireView(), text, Snackbar.LENGTH_LONG) + .apply { + anchorView = requireActivity().findViewById(R.id.main_bottom_nav) + show() + } } override fun showExpiredDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 2f0d697f..d917e7d5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -160,7 +160,7 @@ class TimetableAdapter @Inject constructor() : timetableSmallItemDescription.setTextColor( root.context.getThemeAttrColor( - if (lesson.canceled) R.attr.colorPrimary + if (lesson.canceled) R.attr.colorTimetableCanceled else R.attr.colorTimetableChange ) ) @@ -185,7 +185,7 @@ class TimetableAdapter @Inject constructor() : timetableItemDescription.setTextColor( root.context.getThemeAttrColor( - if (lesson.canceled) R.attr.colorPrimary + if (lesson.canceled) R.attr.colorTimetableCanceled else R.attr.colorTimetableChange ) ) @@ -228,8 +228,8 @@ class TimetableAdapter @Inject constructor() : } private fun updateNumberAndSubjectCanceledColor(numberView: TextView, subjectView: TextView) { - numberView.setTextColor(numberView.context.getThemeAttrColor(R.attr.colorPrimary)) - subjectView.setTextColor(subjectView.context.getThemeAttrColor(R.attr.colorPrimary)) + numberView.setTextColor(numberView.context.getThemeAttrColor(R.attr.colorTimetableCanceled)) + subjectView.setTextColor(subjectView.context.getThemeAttrColor(R.attr.colorTimetableCanceled)) } private fun updateNumberColor(numberView: TextView, lesson: Timetable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt index 4f5547d2..e8a85347 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt @@ -1,24 +1,24 @@ package io.github.wulkanowy.ui.modules.timetable import android.annotation.SuppressLint +import android.app.Dialog import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG import android.os.Bundle -import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.DialogTimetableBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.* import java.time.Instant -class TimetableDialog : DialogFragment() { - - private var binding: DialogTimetableBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class TimetableDialog : BaseDialogFragment() { private lateinit var lesson: Timetable @@ -33,15 +33,14 @@ class TimetableDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) lesson = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogTimetableBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView(DialogTimetableBinding.inflate(layoutInflater).apply { binding = this }.root) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -82,12 +81,12 @@ class TimetableDialog : DialogFragment() { if (canceled) { timetableDialogChangesTitle.setTextColor( requireContext().getThemeAttrColor( - R.attr.colorPrimary + R.attr.colorTimetableCanceled ) ) timetableDialogChangesValue.setTextColor( requireContext().getThemeAttrColor( - R.attr.colorPrimary + R.attr.colorTimetableCanceled ) ) } else { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index e95d6f82..ebc16239 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -87,7 +87,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme timetableNavDate.setOnClickListener { presenter.onPickDate() } timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.elevation = requireContext().dpToPx(8f) + timetableNavContainer.elevation = requireContext().dpToPx(3f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index 043fa1f7..faa833c2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -2,8 +2,8 @@ package io.github.wulkanowy.ui.modules.timetable.additional import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.TimetableAdditional @@ -13,11 +13,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.timetable.additional.add.AdditionalLessonAddDialog import io.github.wulkanowy.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.dpToPx -import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear -import io.github.wulkanowy.utils.openMaterialDatePicker +import io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -73,7 +69,7 @@ class AdditionalLessonsFragment : openAddAdditionalLessonButton.setOnClickListener { presenter.onAdditionalLessonAddButtonClicked() } - additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) + additionalLessonsNavContainer.elevation = requireContext().dpToPx(3f) } } @@ -154,7 +150,7 @@ class AdditionalLessonsFragment : } override fun showDeleteLessonDialog(timetableAdditional: TimetableAdditional) { - AlertDialog.Builder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.additional_lessons_delete_title)) .setItems( arrayOf( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt index f82d6483..13471997 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.widget.doOnTextChanged +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import dagger.hilt.android.AndroidEntryPoint @@ -29,16 +29,14 @@ class AdditionalLessonAddDialog : BaseDialogFragment fun newInstance() = AdditionalLessonAddDialog() } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) - } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogAdditionalAddBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogAdditionalAddBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt index ddd7488e..d937d4dd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonDialog.kt @@ -1,19 +1,18 @@ package io.github.wulkanowy.ui.modules.timetable.completed +import android.app.Dialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.os.bundleOf -import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.databinding.DialogLessonCompletedBinding -import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.serializable -class CompletedLessonDialog : DialogFragment() { - - private var binding: DialogLessonCompletedBinding by lifecycleAwareVariable() +@AndroidEntryPoint +class CompletedLessonDialog : BaseDialogFragment() { private lateinit var completedLesson: CompletedLesson @@ -28,15 +27,16 @@ class CompletedLessonDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NO_TITLE, 0) completedLesson = requireArguments().serializable(ARGUMENT_KEY) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = DialogLessonCompletedBinding.inflate(inflater).apply { binding = this }.root + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setView( + DialogLessonCompletedBinding.inflate(layoutInflater).apply { binding = this }.root + ) + .create() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt index 34a69e6a..77a7bbd5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -2,9 +2,7 @@ package io.github.wulkanowy.ui.modules.timetable.completed import android.os.Bundle import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE +import android.view.View.* import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -14,12 +12,7 @@ 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.dpToPx -import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear -import io.github.wulkanowy.utils.getCompatDrawable -import io.github.wulkanowy.utils.getThemeAttrColor -import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear -import io.github.wulkanowy.utils.openMaterialDatePicker +import io.github.wulkanowy.utils.* import java.time.LocalDate import javax.inject.Inject @@ -73,7 +66,7 @@ class CompletedLessonsFragment : completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.elevation = requireContext().dpToPx(8f) + completedLessonsNavContainer.elevation = requireContext().dpToPx(3f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt index 6ef6cfc9..672dbe72 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureActivity.kt @@ -2,14 +2,11 @@ package io.github.wulkanowy.ui.modules.timetablewidget import android.appwidget.AppWidgetManager.* import android.content.Intent -import android.os.Build import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG -import androidx.appcompat.app.AlertDialog 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.databinding.ActivityWidgetConfigureBinding import io.github.wulkanowy.ui.base.BaseActivity @@ -34,8 +31,6 @@ class TimetableWidgetConfigureActivity : @Inject lateinit var appInfo: AppInfo - private var dialog: AlertDialog? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setResult(RESULT_CANCELED) @@ -61,23 +56,6 @@ class TimetableWidgetConfigureActivity : configureAdapter.onClickListener = presenter::onItemSelect } - override fun showThemeDialog() { - var items = arrayOf( - getString(R.string.widget_timetable_theme_light), - getString(R.string.widget_timetable_theme_dark) - ) - - if (appInfo.systemVersion >= Build.VERSION_CODES.Q) items += getString(R.string.widget_timetable_theme_system) - - dialog = AlertDialog.Builder(this, R.style.WulkanowyTheme_WidgetAccountSwitcher) - .setTitle(R.string.widget_timetable_theme_title) - .setOnDismissListener { presenter.onDismissThemeView() } - .setSingleChoiceItems(items, -1) { _, which -> - presenter.onThemeSelect(which) - } - .show() - } - override fun updateData(data: List, selectedStudentId: Long) { with(configureAdapter) { selectedId = selectedStudentId @@ -110,9 +88,4 @@ class TimetableWidgetConfigureActivity : override fun openLoginView() { startActivity(LoginActivity.getStartIntent(this)) } - - override fun onDestroy() { - super.onDestroy() - dialog?.dismiss() - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt index dc2a7c6c..87e89336 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigurePresenter.kt @@ -8,7 +8,6 @@ import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey -import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getThemeWidgetKey import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -39,22 +38,9 @@ class TimetableWidgetConfigurePresenter @Inject constructor( fun onItemSelect(student: Student) { selectedStudent = student - - if (isFromProvider) registerStudent(selectedStudent) - else view?.showThemeDialog() - } - - fun onThemeSelect(index: Int) { - appWidgetId?.let { - sharedPref.putLong(getThemeWidgetKey(it), index.toLong()) - } registerStudent(selectedStudent) } - fun onDismissThemeView() { - view?.finishView() - } - private fun loadData() { resourceFlow { studentRepository.getSavedStudents(false) }.onEach { when (it) { @@ -65,10 +51,7 @@ class TimetableWidgetConfigurePresenter @Inject constructor( } ?: -1 when { it.data.isEmpty() -> view?.openLoginView() - it.data.size == 1 && !isFromProvider -> { - selectedStudent = it.data.single().student - view?.showThemeDialog() - } + it.data.size == 1 && !isFromProvider -> onItemSelect(it.data.single().student) else -> view?.updateData(it.data, selectedStudentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt index accdc28d..7740b9bb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetConfigureView.kt @@ -11,8 +11,6 @@ interface TimetableWidgetConfigureView : BaseView { fun updateTimetableWidget(widgetId: Int) - fun showThemeDialog() - fun setSuccessResult(widgetId: Int) fun finishView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 664086bc..9c5abe1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -1,27 +1,25 @@ package io.github.wulkanowy.ui.modules.timetablewidget -import android.annotation.SuppressLint import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.graphics.Paint.ANTI_ALIAS_FLAG import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG import android.view.View.GONE import android.view.View.VISIBLE -import android.widget.AdapterView.INVALID_POSITION import android.widget.RemoteViews import android.widget.RemoteViewsService import io.github.wulkanowy.R import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable -import io.github.wulkanowy.data.enums.TimetableMode -import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.toFirstResult -import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getCurrentThemeWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey @@ -29,13 +27,13 @@ import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking import timber.log.Timber +import java.time.Instant import java.time.LocalDate class TimetableWidgetFactory( private val timetableRepository: TimetableRepository, private val studentRepository: StudentRepository, private val semesterRepository: SemesterRepository, - private val prefRepository: PreferencesRepository, private val sharedPref: SharedPrefProvider, private val context: Context, private val intent: Intent? @@ -43,19 +41,22 @@ class TimetableWidgetFactory( private var lessons = emptyList() - private var savedCurrentTheme: Long? = null - - private var primaryColor: Int? = null + private var timetableCanceledColor: Int? = null private var textColor: Int? = null private var timetableChangeColor: Int? = null + private var lastSyncInstant: Instant? = null + override fun getLoadingView() = null override fun hasStableIds() = true - override fun getCount() = lessons.size + override fun getCount() = when { + lessons.isEmpty() -> 0 + else -> lessons.size + 1 + } override fun getViewTypeCount() = 2 @@ -70,195 +71,170 @@ class TimetableWidgetFactory( val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)) val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) - updateTheme(appWidgetId) - lessons = getLessons(date, studentId) - - val todayLastLessonEndTimestamp = lessons.maxOfOrNull { it.end } - if (date == LocalDate.now() && todayLastLessonEndTimestamp != null) { - sharedPref.putLong( - key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), - value = todayLastLessonEndTimestamp.epochSecond, - sync = true - ) - } - } - } - - private fun updateTheme(appWidgetId: Int) { - savedCurrentTheme = sharedPref.getLong(getCurrentThemeWidgetKey(appWidgetId), 0) - - if (savedCurrentTheme == 0L) { - primaryColor = R.color.colorPrimary - textColor = android.R.color.black - timetableChangeColor = R.color.timetable_change_dark - } else { - primaryColor = R.color.colorPrimaryLight - textColor = android.R.color.white - timetableChangeColor = R.color.timetable_change_light - } - } - - private fun getItemLayout(lesson: Timetable): Int { - return when { - prefRepository.showWholeClassPlan == TimetableMode.SMALL_OTHER_GROUP && !lesson.isStudentPlan -> { - if (savedCurrentTheme == 0L) R.layout.item_widget_timetable_small - else R.layout.item_widget_timetable_small_dark - } - savedCurrentTheme == 1L -> R.layout.item_widget_timetable_dark - else -> R.layout.item_widget_timetable - } - } - - private fun getLessons(date: LocalDate, studentId: Long) = try { - runBlocking { - if (!studentRepository.isStudentSaved()) return@runBlocking emptyList() - - val students = studentRepository.getSavedStudents() - val student = students.singleOrNull { it.student.id == studentId }?.student - ?: return@runBlocking emptyList() - - val semester = semesterRepository.getCurrentSemester(student) - timetableRepository.getTimetable(student, semester, date, date, false) - .toFirstResult().dataOrNull?.lessons.orEmpty() - .sortedWith(compareBy({ it.number }, { !it.isStudentPlan })) - .filter { - if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { - it.isStudentPlan - } else true + runCatching { + runBlocking { + val student = getStudent(studentId) ?: return@runBlocking + val semester = semesterRepository.getCurrentSemester(student) + lessons = getLessons(student, semester, date) + lastSyncInstant = + timetableRepository.getLastRefreshTimestamp(semester, date, date) + if (date == LocalDate.now()) { + updateTodayLastLessonEnd(appWidgetId) + } } + }.onFailure { + Timber.e(it, "An error has occurred in timetable widget factory") + } } - } catch (e: Exception) { - Timber.e(e, "An error has occurred in timetable widget factory") - emptyList() } - @SuppressLint("DefaultLocale") + private suspend fun getStudent(studentId: Long): Student? { + val students = studentRepository.getSavedStudents() + return students.singleOrNull { it.student.id == studentId }?.student + } + + private suspend fun getLessons( + student: Student, semester: Semester, date: LocalDate + ): List { + val timetable = timetableRepository.getTimetable(student, semester, date, date, false) + val lessons = timetable.toFirstResult().dataOrNull?.lessons.orEmpty() + return lessons.sortedBy { it.number } + } + + private fun updateTodayLastLessonEnd(appWidgetId: Int) { + val todayLastLessonEnd = lessons.maxOfOrNull { it.end } ?: return + val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId) + sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) + } + + companion object { + const val TIME_FORMAT_STYLE = "HH:mm" + } + override fun getViewAt(position: Int): RemoteViews? { - if (position == INVALID_POSITION || lessons.getOrNull(position) == null) return null - - val lesson = lessons[position] - return RemoteViews(context.packageName, getItemLayout(lesson)).apply { - setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) - setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) - setTextViewText( - R.id.timetableWidgetItemTimeStart, - lesson.start.toFormattedString("HH:mm") - ) - setTextViewText( - R.id.timetableWidgetItemTimeFinish, - lesson.end.toFormattedString("HH:mm") - ) - - updateDescription(this, lesson) - - if (lesson.canceled) { - updateStylesCanceled(this) - } else { - updateStylesNotCanceled(this, lesson) + if (position == lessons.size) { + val synchronizationInstant = lastSyncInstant ?: Instant.MIN + val synchronizationText = getSynchronizationInfoText(synchronizationInstant) + return RemoteViews(context.packageName, R.layout.item_widget_timetable_footer).apply { + setTextViewText(R.id.timetableWidgetSynchronizationTime, synchronizationText) } + } + val lesson = lessons.getOrNull(position) ?: return null + + val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) + val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) + val roomText = "${context.getString(R.string.timetable_room)} ${lesson.room}" + + val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply { + setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) + setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) + setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) + setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) + setTextViewText(R.id.timetableWidgetItemRoom, roomText) + setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) + setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) } + + updateTheme() + clearLessonStyles(remoteViews) + + when { + lesson.canceled -> applyCancelledLessonStyles(remoteViews) + lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( + remoteViews, lesson + ) + } + + return remoteViews } - private fun updateDescription(remoteViews: RemoteViews, lesson: Timetable) { - with(remoteViews) { - if (lesson.info.isNotBlank() && !lesson.changes) { - setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) - setViewVisibility(R.id.timetableWidgetItemDescription, VISIBLE) - setViewVisibility(R.id.timetableWidgetItemRoom, GONE) - setViewVisibility(R.id.timetableWidgetItemTeacher, GONE) - } else { - setViewVisibility(R.id.timetableWidgetItemDescription, GONE) - setViewVisibility(R.id.timetableWidgetItemRoom, VISIBLE) - setViewVisibility(R.id.timetableWidgetItemTeacher, VISIBLE) + private fun updateTheme() { + when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> { + textColor = android.R.color.white + timetableChangeColor = R.color.timetable_change_dark + timetableCanceledColor = R.color.timetable_canceled_dark + } + + else -> { + textColor = android.R.color.black + timetableChangeColor = R.color.timetable_change_light + timetableCanceledColor = R.color.timetable_canceled_light } } } - private fun updateStylesCanceled(remoteViews: RemoteViews) { - with(remoteViews) { - setInt( - R.id.timetableWidgetItemSubject, "setPaintFlags", - STRIKE_THRU_TEXT_FLAG or ANTI_ALIAS_FLAG - ) - setTextColor(R.id.timetableWidgetItemNumber, context.getCompatColor(primaryColor!!)) - setTextColor(R.id.timetableWidgetItemSubject, context.getCompatColor(primaryColor!!)) - setTextColor( - R.id.timetableWidgetItemDescription, - context.getCompatColor(primaryColor!!) - ) - } - } + private fun clearLessonStyles(remoteViews: RemoteViews) { + val defaultTextColor = context.getCompatColor(textColor ?: 0) - private fun updateStylesNotCanceled(remoteViews: RemoteViews, lesson: Timetable) { - with(remoteViews) { + remoteViews.apply { setInt(R.id.timetableWidgetItemSubject, "setPaintFlags", ANTI_ALIAS_FLAG) - setTextColor(R.id.timetableWidgetItemSubject, context.getCompatColor(textColor!!)) - setTextColor( - R.id.timetableWidgetItemDescription, - context.getCompatColor(timetableChangeColor!!) - ) - - updateNotCanceledLessonNumberColor(this, lesson) - updateNotCanceledSubjectColor(this, lesson) - - val teacherChange = lesson.teacherOld.isNotBlank() - updateNotCanceledRoom(this, lesson, teacherChange) - updateNotCanceledTeacher(this, lesson, teacherChange) + setViewVisibility(R.id.timetableWidgetItemRoom, VISIBLE) + setViewVisibility(R.id.timetableWidgetItemTeacher, VISIBLE) + setViewVisibility(R.id.timetableWidgetItemIcon, GONE) + setViewVisibility(R.id.timetableWidgetItemDescription, GONE) + setTextColor(R.id.timetableWidgetItemNumber, defaultTextColor) + setTextColor(R.id.timetableWidgetItemSubject, defaultTextColor) + setTextColor(R.id.timetableWidgetItemRoom, defaultTextColor) + setTextColor(R.id.timetableWidgetItemTeacher, defaultTextColor) + setTextColor(R.id.timetableWidgetItemDescription, defaultTextColor) } } - private fun updateNotCanceledLessonNumberColor(remoteViews: RemoteViews, lesson: Timetable) { - remoteViews.setTextColor( - R.id.timetableWidgetItemNumber, context.getCompatColor( - if (lesson.changes || (lesson.info.isNotBlank() && !lesson.canceled)) timetableChangeColor!! - else textColor!! - ) - ) - } + private fun applyCancelledLessonStyles(remoteViews: RemoteViews) { + val cancelledThemeColor = context.getCompatColor(timetableCanceledColor ?: 0) + val strikeThroughPaintFlags = STRIKE_THRU_TEXT_FLAG or ANTI_ALIAS_FLAG - private fun updateNotCanceledSubjectColor(remoteViews: RemoteViews, lesson: Timetable) { - remoteViews.setTextColor( - R.id.timetableWidgetItemSubject, context.getCompatColor( - if (lesson.subjectOld.isNotBlank() && lesson.subject != lesson.subjectOld) timetableChangeColor!! - else textColor!! - ) - ) - } - - private fun updateNotCanceledRoom( - remoteViews: RemoteViews, - lesson: Timetable, - teacherChange: Boolean - ) { - with(remoteViews) { - if (lesson.room.isNotBlank()) { - setTextViewText( - R.id.timetableWidgetItemRoom, - if (teacherChange) lesson.room - else "${context.getString(R.string.timetable_room)} ${lesson.room}" - ) - - setTextColor( - R.id.timetableWidgetItemRoom, context.getCompatColor( - if (lesson.roomOld.isNotBlank() && lesson.room != lesson.roomOld) timetableChangeColor!! - else textColor!! - ) - ) - } else setTextViewText(R.id.timetableWidgetItemRoom, "") + remoteViews.apply { + setInt(R.id.timetableWidgetItemSubject, "setPaintFlags", strikeThroughPaintFlags) + setTextColor(R.id.timetableWidgetItemNumber, cancelledThemeColor) + setTextColor(R.id.timetableWidgetItemSubject, cancelledThemeColor) + setTextColor(R.id.timetableWidgetItemDescription, cancelledThemeColor) + setViewVisibility(R.id.timetableWidgetItemDescription, VISIBLE) + setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + setViewVisibility(R.id.timetableWidgetItemTeacher, GONE) } } - private fun updateNotCanceledTeacher( - remoteViews: RemoteViews, - lesson: Timetable, - teacherChange: Boolean - ) { - remoteViews.setTextViewText( - R.id.timetableWidgetItemTeacher, - if (teacherChange) lesson.teacher - else "" - ) + private fun applyChangedLessonStyles(remoteViews: RemoteViews, lesson: Timetable) { + val changesTextColor = context.getCompatColor(timetableChangeColor ?: 0) + + remoteViews.apply { + setTextColor(R.id.timetableWidgetItemNumber, changesTextColor) + setTextColor(R.id.timetableWidgetItemDescription, changesTextColor) + setViewVisibility(R.id.timetableWidgetItemIcon, VISIBLE) + setImageViewResource(R.id.timetableWidgetItemIcon, R.drawable.ic_timetable_widget_swap) + } + + if (lesson.subject != lesson.subjectOld) { + remoteViews.setTextColor(R.id.timetableWidgetItemSubject, changesTextColor) + } + + if (lesson.room != lesson.roomOld) { + remoteViews.setTextColor(R.id.timetableWidgetItemRoom, changesTextColor) + } + + if (lesson.teacher != lesson.teacherOld) { + remoteViews.setTextColor(R.id.timetableWidgetItemTeacher, changesTextColor) + } + + if (lesson.info.isNotBlank() && !lesson.changes) { + remoteViews.setViewVisibility(R.id.timetableWidgetItemDescription, VISIBLE) + remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + remoteViews.setViewVisibility(R.id.timetableWidgetItemTeacher, GONE) + } } + + private fun getSynchronizationInfoText(synchronizationInstant: Instant) = + synchronizationInstant.run { + val synchronizationTime = toFormattedString(TIME_FORMAT_STYLE) + val synchronizationDate = toFormattedString() + context.getString( + R.string.widget_timetable_last_synchronization, + synchronizationDate, + synchronizationTime, + ) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 3ba2ae94..624ca30f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -8,10 +8,10 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.Canvas import android.widget.RemoteViews +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.graphics.drawable.toBitmap import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.SharedPrefProvider @@ -70,11 +70,6 @@ class TimetableWidgetProvider : BroadcastReceiver() { "timetable_widget_today_last_lesson_end_date_time_$appWidgetId" fun getStudentWidgetKey(appWidgetId: Int) = "timetable_widget_student_$appWidgetId" - - fun getThemeWidgetKey(appWidgetId: Int) = "timetable_widget_theme_$appWidgetId" - - fun getCurrentThemeWidgetKey(appWidgetId: Int) = - "timetable_widget_current_theme_$appWidgetId" } @OptIn(DelicateCoroutinesApi::class) @@ -109,8 +104,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0) val student = getStudent( - sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), - toggledWidgetId + sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId ) val savedDate = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0)) @@ -122,8 +116,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { } if (!buttonType.isNullOrBlank()) { analytics.logEvent( - "changed_timetable_widget_day", - "button" to buttonType + "changed_timetable_widget_day", "button" to buttonType ) } updateWidget(context, toggledWidgetId, date, student) @@ -137,49 +130,21 @@ class TimetableWidgetProvider : BroadcastReceiver() { with(sharedPref) { delete(getStudentWidgetKey(appWidgetId)) delete(getDateWidgetKey(appWidgetId)) - delete(getThemeWidgetKey(appWidgetId)) - delete(getCurrentThemeWidgetKey(appWidgetId)) } } } private fun updateWidget( - context: Context, - appWidgetId: Int, - date: LocalDate, - student: Student? + context: Context, appWidgetId: Int, date: LocalDate, student: Student? ) { - val savedConfigureTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = - context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - var currentTheme = 0L - var layoutId = R.layout.widget_timetable - - if (savedConfigureTheme == 1L || (savedConfigureTheme == 2L && isSystemDarkMode)) { - currentTheme = 1L - layoutId = R.layout.widget_timetable_dark - } - val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV) val resetNavIntent = createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET) - val adapterIntent = Intent(context, TimetableWidgetService::class.java) - .apply { - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - //make Intent unique - action = appWidgetId.toString() - } - val accountIntent = PendingIntent.getActivity( - context, - -Int.MAX_VALUE + appWidgetId, - Intent(context, TimetableWidgetConfigureActivity::class.java).apply { - addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - putExtra(EXTRA_FROM_PROVIDER, true) - }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - - ) + val adapterIntent = Intent(context, TimetableWidgetService::class.java).apply { + putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + action = appWidgetId.toString() //make Intent unique + } val appIntent = PendingIntent.getActivity( context, TIMETABLE_PENDING_INTENT_ID, @@ -187,56 +152,41 @@ class TimetableWidgetProvider : BroadcastReceiver() { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - val remoteView = RemoteViews(context.packageName, layoutId).apply { + val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise() + val remoteView = RemoteViews(context.packageName, R.layout.widget_timetable).apply { setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) - setTextViewText( - R.id.timetableWidgetDate, - date.toFormattedString("EEEE, dd.MM").capitalise() - ) - setTextViewText( - R.id.timetableWidgetName, - student?.nickOrName ?: context.getString(R.string.all_no_data) - ) - - student?.let { - setImageViewBitmap(R.id.timetableWidgetAccount, context.createAvatarBitmap(it)) - } - + setTextViewText(R.id.timetableWidgetDate, formattedDate) setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent) setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent) - setOnClickPendingIntent(R.id.timetableWidgetName, resetNavIntent) - setOnClickPendingIntent(R.id.timetableWidgetAccount, accountIntent) setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } + student?.let { + setupAccountView(context, student, remoteView, appWidgetId) + } + with(sharedPref) { - putLong(getCurrentThemeWidgetKey(appWidgetId), currentTheme) putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) } with(appWidgetManager) { - updateAppWidget(appWidgetId, remoteView) + partiallyUpdateAppWidget(appWidgetId, remoteView) notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) - Timber.d("TimetableWidgetProvider updated") } + + Timber.d("TimetableWidgetProvider updated") } private fun createNavIntent( - context: Context, - code: Int, - appWidgetId: Int, - buttonType: String + context: Context, code: Int, appWidgetId: Int, buttonType: String ) = PendingIntent.getBroadcast( - context, - code, - Intent(context, TimetableWidgetProvider::class.java).apply { + context, code, Intent(context, TimetableWidgetProvider::class.java).apply { action = ACTION_APPWIDGET_UPDATE putExtra(EXTRA_BUTTON_TYPE, buttonType) putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) - }, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { @@ -258,31 +208,6 @@ class TimetableWidgetProvider : BroadcastReceiver() { null } - private fun Context.createAvatarBitmap(student: Student): Bitmap { - val avatarColor = if (student.avatarColor == -2937041L) { - getCompatColor(R.color.colorPrimaryLight).toLong() - } else { - student.avatarColor - } - val avatarDrawable = createNameInitialsDrawable(student.nickOrName, avatarColor, 0.5f) - - val avatarBitmap = - if (avatarDrawable.intrinsicWidth <= 0 || avatarDrawable.intrinsicHeight <= 0) { - Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - } else { - Bitmap.createBitmap( - avatarDrawable.intrinsicWidth, - avatarDrawable.intrinsicHeight, - Bitmap.Config.ARGB_8888 - ) - } - - val canvas = Canvas(avatarBitmap) - avatarDrawable.setBounds(0, 0, canvas.width, canvas.height) - avatarDrawable.draw(canvas) - return avatarBitmap - } - private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { val lastLessonEndTimestamp = sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) @@ -299,4 +224,44 @@ class TimetableWidgetProvider : BroadcastReceiver() { todayDate.nextOrSameSchoolDay } } + + private fun setupAccountView( + context: Context, + student: Student, + remoteViews: RemoteViews, + appWidgetId: Int + ) { + val accountInitials = student.nickOrName + .split(" ") + .mapNotNull { it.firstOrNull() }.take(2) + .joinToString(separator = "").uppercase() + + val accountPickerIntent = PendingIntent.getActivity( + context, + -Int.MAX_VALUE + appWidgetId, + Intent(context, TimetableWidgetConfigureActivity::class.java).apply { + addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) + putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_FROM_PROVIDER, true) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + // Create background bitmap + val avatarDrawableResource = R.drawable.background_timetable_widget_avatar + AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> + val screenDensity = context.resources.displayMetrics.density + val avatarSize = (48 * screenDensity).toInt() + val backgroundBitmap = DrawableCompat.wrap(drawable).run { + DrawableCompat.setTint(this, student.avatarColor.toInt()) + toBitmap(avatarSize, avatarSize) + } + remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, backgroundBitmap) + } + + remoteViews.apply { + setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) + setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerIntent) + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt index 032e2d28..76ce66dc 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LifecycleAwareVariable.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.utils import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -29,18 +30,18 @@ class LifecycleAwareVariable : ReadWriteProperty, DefaultL } } -class LifecycleAwareVariableActivity : ReadWriteProperty, +class LifecycleAwareVariableComponent : ReadWriteProperty, DefaultLifecycleObserver { private var _value: T? = null - override fun setValue(thisRef: AppCompatActivity, property: KProperty<*>, value: T) { + override fun setValue(thisRef: LifecycleOwner, property: KProperty<*>, value: T) { thisRef.lifecycle.removeObserver(this) _value = value thisRef.lifecycle.addObserver(this) } - override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>) = _value + override fun getValue(thisRef: LifecycleOwner, property: KProperty<*>) = _value ?: throw IllegalStateException("Trying to call an lifecycle-aware value outside of the view lifecycle, or the value has not been initialized") override fun onDestroy(owner: LifecycleOwner) { @@ -53,4 +54,8 @@ class LifecycleAwareVariableActivity : ReadWriteProperty Fragment.lifecycleAwareVariable() = LifecycleAwareVariable() -fun lifecycleAwareVariable() = LifecycleAwareVariableActivity() +@Suppress("unused") +fun DialogFragment.lifecycleAwareVariable() = LifecycleAwareVariableComponent() + +@Suppress("unused") +fun AppCompatActivity.lifecycleAwareVariable() = LifecycleAwareVariableComponent() diff --git a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt index 93e67be0..72129751 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt @@ -49,4 +49,9 @@ class AutoRefreshHelper @Inject constructor( fun updateLastRefreshTimestamp(key: String) { sharedPref.putLong(key, Instant.now().toEpochMilli()) } + + fun getLastRefreshTimestamp(key: String): Instant { + val refreshTimestampMilli = sharedPref.getLong(key, 0) + return Instant.ofEpochMilli(refreshTimestampMilli) + } } diff --git a/app/src/main/res/drawable-night/background_header_note.xml b/app/src/main/res/drawable-night/background_header_note.xml deleted file mode 100644 index 6b594e7c..00000000 --- a/app/src/main/res/drawable-night/background_header_note.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/background_grade_details_rounded.xml b/app/src/main/res/drawable/background_grade_details_rounded.xml new file mode 100644 index 00000000..e24088a0 --- /dev/null +++ b/app/src/main/res/drawable/background_grade_details_rounded.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/background_grade_details_weight_rounded.xml b/app/src/main/res/drawable/background_grade_details_weight_rounded.xml new file mode 100644 index 00000000..4b210912 --- /dev/null +++ b/app/src/main/res/drawable/background_grade_details_weight_rounded.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/background_widget_timetable_dark.xml b/app/src/main/res/drawable/background_grade_rounded.xml similarity index 74% rename from app/src/main/res/drawable/background_widget_timetable_dark.xml rename to app/src/main/res/drawable/background_grade_rounded.xml index 6fe7d0ab..52c10c2f 100644 --- a/app/src/main/res/drawable/background_widget_timetable_dark.xml +++ b/app/src/main/res/drawable/background_grade_rounded.xml @@ -1,5 +1,5 @@ - + - \ No newline at end of file + diff --git a/app/src/main/res/drawable/background_widget_item_timetable_dark.xml b/app/src/main/res/drawable/background_grade_small_rounded.xml similarity index 51% rename from app/src/main/res/drawable/background_widget_item_timetable_dark.xml rename to app/src/main/res/drawable/background_grade_small_rounded.xml index e432a648..dd50417f 100644 --- a/app/src/main/res/drawable/background_widget_item_timetable_dark.xml +++ b/app/src/main/res/drawable/background_grade_small_rounded.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/background_header_note.xml b/app/src/main/res/drawable/background_header_note.xml index c21e55c6..8cf84a1c 100644 --- a/app/src/main/res/drawable/background_header_note.xml +++ b/app/src/main/res/drawable/background_header_note.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/background_luckynumber_widget.xml b/app/src/main/res/drawable/background_luckynumber_widget_button.xml similarity index 65% rename from app/src/main/res/drawable/background_luckynumber_widget.xml rename to app/src/main/res/drawable/background_luckynumber_widget_button.xml index 367c5527..66b1685f 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget_button.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml deleted file mode 100644 index cb094b57..00000000 --- a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/background_material_alert_dialog.xml b/app/src/main/res/drawable/background_material_alert_dialog.xml new file mode 100644 index 00000000..5ab8a350 --- /dev/null +++ b/app/src/main/res/drawable/background_material_alert_dialog.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/background_timetable_widget_avatar.xml b/app/src/main/res/drawable/background_timetable_widget_avatar.xml new file mode 100644 index 00000000..7f64c4eb --- /dev/null +++ b/app/src/main/res/drawable/background_timetable_widget_avatar.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/background_widget_header_timetable.xml b/app/src/main/res/drawable/background_widget_header_timetable.xml deleted file mode 100644 index 98eec700..00000000 --- a/app/src/main/res/drawable/background_widget_header_timetable.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_header_timetable_dark.xml b/app/src/main/res/drawable/background_widget_header_timetable_dark.xml deleted file mode 100644 index 616a9127..00000000 --- a/app/src/main/res/drawable/background_widget_header_timetable_dark.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_item_timetable.xml b/app/src/main/res/drawable/background_widget_item_timetable.xml index 08854fba..09635758 100644 --- a/app/src/main/res/drawable/background_widget_item_timetable.xml +++ b/app/src/main/res/drawable/background_widget_item_timetable.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/background_widget_timetable.xml b/app/src/main/res/drawable/background_widget_timetable.xml index 2267587d..b589ad29 100644 --- a/app/src/main/res/drawable/background_widget_timetable.xml +++ b/app/src/main/res/drawable/background_widget_timetable.xml @@ -1,5 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml index ee3ff4be..4250fae4 100644 --- a/app/src/main/res/drawable/ic_chevron_left.xml +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml index a6d73497..de5037cf 100644 --- a/app/src/main/res/drawable/ic_chevron_right.xml +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 00000000..f20e2094 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_scale_balance.xml b/app/src/main/res/drawable/ic_scale_balance.xml new file mode 100644 index 00000000..c65467a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_scale_balance.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_timetable_widget_swap.xml b/app/src/main/res/drawable/ic_timetable_widget_swap.xml new file mode 100644 index 00000000..2f91489a --- /dev/null +++ b/app/src/main/res/drawable/ic_timetable_widget_swap.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_widget_chevron.png b/app/src/main/res/drawable/ic_widget_chevron.png deleted file mode 100644 index 34345521a5b5f97b7a8bb543c3f86104ac914c4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA@dWsUxVjhk{D%Ry`4>PMSW1HY zf*JnRhX*Fk)C2MrJzX3_G|nd{Nc?H& + + diff --git a/app/src/main/res/drawable/img_luckynumber_widget_preview.png b/app/src/main/res/drawable/img_luckynumber_widget_preview.png index 539b0a598422fe833ab445130c55c51373473568..267018550f11c15ad0a1c0ecefc14285a5e23d8b 100644 GIT binary patch literal 3702 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 C8_;zC literal 19550 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 z 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 O0000KI`ZUu_FPdaza!>l`V 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 diff --git a/app/src/main/res/drawable/shape_badge.xml b/app/src/main/res/drawable/shape_badge.xml new file mode 100644 index 00000000..3153f592 --- /dev/null +++ b/app/src/main/res/drawable/shape_badge.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-v31/widget_timetable_preview.xml b/app/src/main/res/layout-v31/widget_timetable_preview.xml new file mode 100644 index 00000000..bc556f50 --- /dev/null +++ b/app/src/main/res/layout-v31/widget_timetable_preview.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 11844e24..d14de50a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,24 +1,31 @@ - + android:fitsSystemWindows="true" + app:layout_constraintBaseline_toTopOf="parent"> + + + + app:layout_constraintTop_toBottomOf="@id/main_app_bar" + tools:layout="@layout/fragment_dashboard" /> - + android:fitsSystemWindows="true" + app:layout_constraintBaseline_toTopOf="parent"> + + + + app:layout_constraintTop_toBottomOf="@id/send_app_bar"> + app:layout_constraintTop_toBottomOf="@id/send_app_bar" /> diff --git a/app/src/main/res/layout/dialog_account_edit.xml b/app/src/main/res/layout/dialog_account_edit.xml index 9f617e44..2ab4ccc6 100644 --- a/app/src/main/res/layout/dialog_account_edit.xml +++ b/app/src/main/res/layout/dialog_account_edit.xml @@ -1,19 +1,14 @@ - - + android:layout_height="match_parent"> - + android:paddingStart="24dp" + android:paddingEnd="24dp"> + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + android:text="@string/additional_lessons_repeat" + app:layout_constraintTop_toBottomOf="@id/additionalLessonDialogDate" /> + android:hint="@string/all_subject" + app:layout_constraintTop_toBottomOf="@id/additionalLessonDialogEnd"> - + - - - - - + + diff --git a/app/src/main/res/layout/dialog_ads_consent.xml b/app/src/main/res/layout/dialog_ads_consent.xml index 81607478..118fb9c1 100644 --- a/app/src/main/res/layout/dialog_ads_consent.xml +++ b/app/src/main/res/layout/dialog_ads_consent.xml @@ -63,7 +63,7 @@ - - - + android:layout_height="match_parent"> - - + app:layout_constraintEnd_toStartOf="@+id/examDialogClose" /> diff --git a/app/src/main/res/layout/dialog_grade.xml b/app/src/main/res/layout/dialog_grade.xml index 94facb23..f47f6108 100644 --- a/app/src/main/res/layout/dialog_grade.xml +++ b/app/src/main/res/layout/dialog_grade.xml @@ -32,43 +32,57 @@ android:id="@+id/gradeDialogValue" android:layout_width="match_parent" android:layout_height="86dp" - android:layout_marginStart="0dp" android:layout_marginEnd="16dp" - android:background="@color/grade_material_default" + android:background="@drawable/background_grade_details_rounded" + android:backgroundTint="@color/grade_material_default" android:gravity="center" android:textColor="@android:color/white" android:textSize="30sp" tools:text="6" /> - + android:background="@drawable/background_grade_details_weight_rounded" + android:backgroundTint="@color/grade_black" + android:gravity="center_horizontal"> + + + + + + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:text="@string/all_no_description" + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + android:textIsSelectable="true" /> - - diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 341cec54..8c6cf0a7 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -1,71 +1,56 @@ - + android:layout_height="wrap_content"> + android:background="@drawable/ic_all_divider" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogRecycler" /> - + - - - - - + + diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml index 524f0db0..e0ff5b74 100644 --- a/app/src/main/res/layout/dialog_homework_add.xml +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -2,37 +2,35 @@ 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" - android:fillViewport="true" - android:minWidth="300dp" - android:paddingStart="8dp" - android:paddingEnd="8dp"> + android:layout_height="match_parent"> - + android:paddingStart="24dp" + android:paddingEnd="24dp"> + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + android:hint="@string/all_subject" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogDate"> + android:hint="@string/all_teacher" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogSubject"> + android:hint="@string/all_content" + app:layout_constraintTop_toBottomOf="@id/homeworkDialogTeacher"> - + - - - - - + + diff --git a/app/src/main/res/layout/dialog_lesson_completed.xml b/app/src/main/res/layout/dialog_lesson_completed.xml index 500cdb6f..3a1d3fd0 100644 --- a/app/src/main/res/layout/dialog_lesson_completed.xml +++ b/app/src/main/res/layout/dialog_lesson_completed.xml @@ -1,4 +1,5 @@ + - - - - + - - + - - - - + app:layout_constraintTop_toBottomOf="@id/timetableDialogLessonValue" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/timetableDialogTeacherValue" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/timetableDialogRoomValue" + tools:visibility="visible" /> @@ -106,7 +105,7 @@ diff --git a/app/src/main/res/layout/fragment_login_advanced.xml b/app/src/main/res/layout/fragment_login_advanced.xml index c7acaa70..43016db4 100644 --- a/app/src/main/res/layout/fragment_login_advanced.xml +++ b/app/src/main/res/layout/fragment_login_advanced.xml @@ -97,7 +97,7 @@ + android:layout_height="0dp" + android:layout_weight="1"> @@ -173,4 +173,4 @@ app:srcCompat="@drawable/ic_chevron_right" app:tint="?colorPrimary" /> - + diff --git a/app/src/main/res/layout/fragment_timetable_completed.xml b/app/src/main/res/layout/fragment_timetable_completed.xml index e089275d..8d647ff6 100644 --- a/app/src/main/res/layout/fragment_timetable_completed.xml +++ b/app/src/main/res/layout/fragment_timetable_completed.xml @@ -93,7 +93,7 @@ + android:layout_marginVertical="6dp"> + android:layout_marginVertical="6dp"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_announcements.xml b/app/src/main/res/layout/item_dashboard_announcements.xml index 19f72088..b9ddb757 100644 --- a/app/src/main/res/layout/item_dashboard_announcements.xml +++ b/app/src/main/res/layout/item_dashboard_announcements.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_conferences.xml b/app/src/main/res/layout/item_dashboard_conferences.xml index 02d3edfc..b02b8e18 100644 --- a/app/src/main/res/layout/item_dashboard_conferences.xml +++ b/app/src/main/res/layout/item_dashboard_conferences.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_exams.xml b/app/src/main/res/layout/item_dashboard_exams.xml index 9cc98d79..84302403 100644 --- a/app/src/main/res/layout/item_dashboard_exams.xml +++ b/app/src/main/res/layout/item_dashboard_exams.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_grades.xml b/app/src/main/res/layout/item_dashboard_grades.xml index 5cc9ce30..345d8a5e 100644 --- a/app/src/main/res/layout/item_dashboard_grades.xml +++ b/app/src/main/res/layout/item_dashboard_grades.xml @@ -6,8 +6,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:layout_marginVertical="6dp" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_homework.xml b/app/src/main/res/layout/item_dashboard_homework.xml index 975d66ef..b36afc57 100644 --- a/app/src/main/res/layout/item_dashboard_homework.xml +++ b/app/src/main/res/layout/item_dashboard_homework.xml @@ -8,8 +8,7 @@ android:layout_marginVertical="6dp" android:clickable="true" android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:foreground="?selectableItemBackground"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml index 0c59d1eb..1c9246a1 100644 --- a/app/src/main/res/layout/item_dashboard_horizontal_group.xml +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -13,7 +13,6 @@ android:layout_width="0dp" android:layout_height="44dp" android:layout_marginVertical="4dp" - app:cardElevation="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/dashboard_horizontal_group_item_message_container" app:layout_constraintHorizontal_chainStyle="spread_inside" @@ -81,7 +80,6 @@ android:layout_height="44dp" android:layout_marginVertical="4dp" android:layout_marginEnd="8dp" - app:cardElevation="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/dashboard_horizontal_group_item_attendance_container" app:layout_constraintStart_toEndOf="@id/dashboard_horizontal_group_item_lucky_container" @@ -154,7 +152,6 @@ android:layout_width="0dp" android:layout_height="44dp" android:layout_marginVertical="4dp" - app:cardElevation="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/dashboard_horizontal_group_item_message_container" @@ -221,7 +218,6 @@ android:layout_height="44dp" android:layout_marginVertical="4dp" android:visibility="gone" - app:cardElevation="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/dashboard_horizontal_group_item_message_container" diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index 9156c1a2..a40f17f2 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -5,11 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" - android:layout_marginVertical="6dp" - android:clickable="true" - android:focusable="true" - android:foreground="?selectableItemBackground" - app:cardElevation="4dp"> + android:layout_marginVertical="6dp"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_grade_details.xml b/app/src/main/res/layout/item_grade_details.xml index 2f3bd2de..6849e929 100644 --- a/app/src/main/res/layout/item_grade_details.xml +++ b/app/src/main/res/layout/item_grade_details.xml @@ -20,11 +20,12 @@ android:id="@+id/gradeItemValue" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@color/grade_material_default" + android:background="@drawable/background_grade_rounded" + android:backgroundTint="@color/grade_material_default" android:gravity="center" android:maxLength="5" android:minWidth="45dp" - android:minHeight="40dp" + android:minHeight="45dp" android:textColor="@android:color/white" android:textSize="16sp" app:layout_constraintBottom_toBottomOf="@+id/gradeDetailsContainer" diff --git a/app/src/main/res/layout/item_homework_dialog_details.xml b/app/src/main/res/layout/item_homework_dialog_details.xml index 9d560ba5..1b1c1d39 100644 --- a/app/src/main/res/layout/item_homework_dialog_details.xml +++ b/app/src/main/res/layout/item_homework_dialog_details.xml @@ -13,16 +13,17 @@ android:orientation="horizontal"> + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:textColor="?attr/colorOnSurface" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - - - - diff --git a/app/src/main/res/layout/item_message_chips.xml b/app/src/main/res/layout/item_message_chips.xml index da2e2031..c1f36c4d 100644 --- a/app/src/main/res/layout/item_message_chips.xml +++ b/app/src/main/res/layout/item_message_chips.xml @@ -21,29 +21,23 @@ + android:text="@string/message_chip_only_unread" /> + android:text="@string/message_chip_only_with_attachments" /> diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml index 16a7ae0c..a2a67748 100644 --- a/app/src/main/res/layout/item_notifications_center.xml +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="8dp" - android:layout_marginTop="12dp" - app:cardElevation="4dp"> + android:layout_marginTop="12dp"> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_widget_timetable.xml b/app/src/main/res/layout/item_widget_timetable.xml index 899d7503..27c9db66 100644 --- a/app/src/main/res/layout/item_widget_timetable.xml +++ b/app/src/main/res/layout/item_widget_timetable.xml @@ -1,129 +1,112 @@ - + android:textAppearance="?attr/textAppearanceHeadline6" + android:textSize="24sp" + tools:text="1" + tools:textColor="?attr/colorTimetableChange" /> - - - + + android:textAppearance="?attr/textAppearanceBodySmall" + tools:text="08:00" /> + android:layout_marginTop="4dp" + android:textAppearance="?attr/textAppearanceBodySmall" + tools:text="09:45" /> + + + + - - + android:lines="1" + android:textAppearance="?attr/textAppearanceTitleMedium" + tools:text="Programowanie aplikacji mobilnych i desktopowych" /> + + + + + + + + - + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable_dark.xml b/app/src/main/res/layout/item_widget_timetable_dark.xml deleted file mode 100644 index 06233244..00000000 --- a/app/src/main/res/layout/item_widget_timetable_dark.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_widget_timetable_footer.xml b/app/src/main/res/layout/item_widget_timetable_footer.xml new file mode 100644 index 00000000..ef14da5d --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_footer.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/layout/item_widget_timetable_small.xml b/app/src/main/res/layout/item_widget_timetable_small.xml deleted file mode 100644 index 1bf4072d..00000000 --- a/app/src/main/res/layout/item_widget_timetable_small.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_widget_timetable_small_dark.xml b/app/src/main/res/layout/item_widget_timetable_small_dark.xml deleted file mode 100644 index 50bbbd03..00000000 --- a/app/src/main/res/layout/item_widget_timetable_small_dark.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_preference_switch.xml b/app/src/main/res/layout/layout_preference_switch.xml new file mode 100644 index 00000000..c4f8a6c2 --- /dev/null +++ b/app/src/main/res/layout/layout_preference_switch.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/subitem_dashboard_grades.xml b/app/src/main/res/layout/subitem_dashboard_grades.xml index 9354be3d..c8165b95 100644 --- a/app/src/main/res/layout/subitem_dashboard_grades.xml +++ b/app/src/main/res/layout/subitem_dashboard_grades.xml @@ -23,7 +23,7 @@ android:id="@+id/dashboard_grades_subitem_grade_container" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" + android:layout_marginStart="2dp" android:layout_marginBottom="6dp" android:orientation="horizontal" app:layout_constraintBottom_toBottomOf="parent" @@ -36,4 +36,4 @@ android:visibility="gone" tools:visibility="visible" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/subitem_dashboard_small_grade.xml b/app/src/main/res/layout/subitem_dashboard_small_grade.xml index 986d9602..6800b72e 100644 --- a/app/src/main/res/layout/subitem_dashboard_small_grade.xml +++ b/app/src/main/res/layout/subitem_dashboard_small_grade.xml @@ -5,7 +5,8 @@ android:layout_width="wrap_content" android:layout_height="20dp" android:layout_marginStart="4dp" - android:background="@color/grade_material_default" + android:background="@drawable/background_grade_small_rounded" + android:backgroundTint="@color/grade_material_default" android:gravity="center" android:maxLength="5" android:minWidth="20dp" diff --git a/app/src/main/res/layout/widget_luckynumber.xml b/app/src/main/res/layout/widget_luckynumber.xml index 360a1970..fc8acc60 100644 --- a/app/src/main/res/layout/widget_luckynumber.xml +++ b/app/src/main/res/layout/widget_luckynumber.xml @@ -1,63 +1,46 @@ - + android:adjustViewBounds="true" + android:importantForAccessibility="no" + android:scaleType="fitCenter" + android:src="@drawable/shape_badge" + android:tint="?attr/colorSurface" + app:tint="?attr/colorSurface" + tools:ignore="UseAppTint" /> - - + android:layout_gravity="center" + android:text="17" + android:textColor="?attr/colorPrimary" + android:textSize="72sp" + android:textStyle="bold" + tools:ignore="HardcodedText" /> - - + + diff --git a/app/src/main/res/layout/widget_luckynumber_dark.xml b/app/src/main/res/layout/widget_luckynumber_dark.xml deleted file mode 100644 index def110de..00000000 --- a/app/src/main/res/layout/widget_luckynumber_dark.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/widget_timetable.xml b/app/src/main/res/layout/widget_timetable.xml index 059bb741..3abc488e 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -1,99 +1,115 @@ - - + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingVertical="16dp"> - - - - - - - - + android:layout_marginStart="8dp" + android:layout_weight="1" + android:lines="1" + android:textAppearance="?attr/textAppearanceHeadline5" + tools:text="Pon, 12.05" /> + android:tint="?attr/colorPrimary" + app:tint="?attr/colorPrimary" + tools:ignore="UseAppTint" /> - + android:tint="?attr/colorPrimary" + app:tint="?attr/colorPrimary" + tools:ignore="UseAppTint" /> - + + + + + + + + + android:layout_height="match_parent"> - - + + + + + diff --git a/app/src/main/res/layout/widget_timetable_dark.xml b/app/src/main/res/layout/widget_timetable_dark.xml deleted file mode 100644 index 9c8b8c56..00000000 --- a/app/src/main/res/layout/widget_timetable_dark.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/action_menu_dashboard.xml b/app/src/main/res/menu/action_menu_dashboard.xml index 13565a19..71203d32 100644 --- a/app/src/main/res/menu/action_menu_dashboard.xml +++ b/app/src/main/res/menu/action_menu_dashboard.xml @@ -6,13 +6,13 @@ android:icon="@drawable/ic_settings_notifications" android:orderInCategory="1" android:title="@string/notifications_center_title" - app:iconTint="@color/material_on_surface_emphasis_medium" + app:iconTint="?colorControlNormal" app:showAsAction="ifRoom" /> - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index da1bca12..a8699eec 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/res/values-night-v31/styles.xml b/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 00000000..067a4353 --- /dev/null +++ b/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,65 @@ + + + + + + + + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 881d5bd4..7d2f0cfe 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,20 +1,41 @@ - - - - - - + + diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml index 840f5357..95450ae1 100644 --- a/app/src/main/res/values-v23/styles.xml +++ b/app/src/main/res/values-v23/styles.xml @@ -3,6 +3,11 @@ - \ No newline at end of file + + + diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml deleted file mode 100644 index 3fb0a5dd..00000000 --- a/app/src/main/res/values-v26/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 00000000..1cbe9791 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml deleted file mode 100644 index a936566f..00000000 --- a/app/src/main/res/values-v28/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml deleted file mode 100644 index a936566f..00000000 --- a/app/src/main/res/values-v29/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..cffb284e --- /dev/null +++ b/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,60 @@ + + + + + + + - - - - + + diff --git a/app/src/main/res/xml/provider_widget_lucky_number.xml b/app/src/main/res/xml/provider_widget_lucky_number.xml index 064f2057..330bd53f 100644 --- a/app/src/main/res/xml/provider_widget_lucky_number.xml +++ b/app/src/main/res/xml/provider_widget_lucky_number.xml @@ -4,11 +4,13 @@ android:configure="io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity" android:initialLayout="@layout/widget_luckynumber" android:minWidth="110dp" - android:minHeight="40dp" - android:minResizeWidth="40dp" + android:minHeight="110dp" android:minResizeHeight="40dp" android:previewImage="@drawable/img_luckynumber_widget_preview" + android:previewLayout="@layout/widget_luckynumber" android:resizeMode="horizontal|vertical" + android:targetCellWidth="2" + android:targetCellHeight="2" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" - tools:targetApi="jelly_bean_mr1" /> + tools:targetApi="s" /> diff --git a/app/src/main/res/xml/provider_widget_timetable.xml b/app/src/main/res/xml/provider_widget_timetable.xml index 5392dd50..3cdad0c8 100644 --- a/app/src/main/res/xml/provider_widget_timetable.xml +++ b/app/src/main/res/xml/provider_widget_timetable.xml @@ -8,7 +8,10 @@ android:minResizeWidth="250dp" android:minResizeHeight="110dp" android:previewImage="@drawable/img_timetable_widget_preview" + android:previewLayout="@layout/widget_timetable_preview" android:resizeMode="horizontal|vertical" + android:targetCellWidth="3" + android:targetCellHeight="2" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" - tools:targetApi="jelly_bean_mr1" /> + tools:targetApi="s" /> From 8d2d7922f94bf7a15607aaad1bdb49234cb3f610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 29 Mar 2023 22:23:42 +0200 Subject: [PATCH 53/77] Fix collapse garde subject when grade is unread (#2158) --- .../github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 38bae376..2d63aae4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -15,6 +15,7 @@ import io.github.wulkanowy.utils.changeModifier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject @@ -77,7 +78,7 @@ class GradeAverageProvider @Inject constructor( ) } } - } + }.distinctUntilChanged() private fun calculateCombinedAverage( student: Student, From 7aa65e98ce3393a6e5a1e615d5ddc4ef0743a17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:30:27 +0000 Subject: [PATCH 54/77] Bump com.android.tools:desugar_jdk_libs from 2.0.2 to 2.0.3 (#2162) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 3fd11ccc..e96d9b80 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -188,7 +188,7 @@ ext { dependencies { implementation "io.github.wulkanowy:sdk:1.9.2" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" From cb914fe32b862fe5ab2f6aba597410e0bc2af916 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:30:56 +0000 Subject: [PATCH 55/77] Bump com.google.android.gms:play-services-ads from 21.5.0 to 22.0.0 (#2161) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e96d9b80..a0bc17aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -249,7 +249,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:21.5.0' + playImplementation 'com.google.android.gms:play-services-ads:22.0.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.1.300' From a7cf54897ac6d05c042a2b7e39a138d01adccac7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:32:06 +0000 Subject: [PATCH 56/77] Bump kotlin_version from 1.8.10 to 1.8.20 (#2160) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c14e0dbd..2aa3f758 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.10' + kotlin_version = '1.8.20' about_libraries = '10.6.1' hilt_version = "2.45" } From 253e55f70e63e7fe24e75669c01a84a4a3448fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 5 Apr 2023 22:33:01 +0200 Subject: [PATCH 57/77] New Crowdin updates (#2159) --- app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-da-rDK/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es-rES/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + 8 files changed, 8 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1897a48b..bedb491b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -682,6 +682,7 @@ Zrušit Žádné lekce + Synchronizováno %1$s v %2$s Vybrat motiv Světlý Tmavý diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 3875b3d9..ebec11b2 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -594,6 +594,7 @@ Cancel No lessons + Synchronized on %1$s at %2$s Choose theme Light Dark diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c03181e4..1e1785bf 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -594,6 +594,7 @@ Cancel Keine Lektionen + Synchronized on %1$s at %2$s Thema wählen Licht Dunkel diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 3875b3d9..ebec11b2 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -594,6 +594,7 @@ Cancel No lessons + Synchronized on %1$s at %2$s Choose theme Light Dark diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7f0c3291..f797a4dc 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -682,6 +682,7 @@ Anuluj Brak lekcji + Zsynchronizowano %1$s o %2$s Wybierz motyw Jasny Ciemny diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 42e1e0bb..7a42e388 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -682,6 +682,7 @@ Отменить Нет уроков + Synchronized on %1$s at %2$s Выбрать тему Светлая Тёмная diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 8979a95f..cde3178b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -682,6 +682,7 @@ Zrušiť Žiadne lekcie + Synchronizované %1$s v %2$s Vybrať motív Svetlý Tmavý diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0c736904..9602aabb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -682,6 +682,7 @@ Скасувати Немаэ уроків + Synchronized on %1$s at %2$s Увібрати тему Яскрава Темна From c67d2d767d1b037c571efea34787838b92c2fec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 6 Apr 2023 01:28:47 +0200 Subject: [PATCH 58/77] Set error tint to password toggle icon when error occured (#2163) --- .../ui/modules/login/form/LoginFormFragment.kt | 11 +++++------ .../io/github/wulkanowy/utils/ContextExtension.kt | 6 ++---- app/src/main/res/layout/fragment_login_form.xml | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index a0e7608d..bbc38219 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -15,12 +15,7 @@ import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData -import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.hideSoftInput -import io.github.wulkanowy.utils.openEmailClient -import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.setOnEditorDoneSignIn -import io.github.wulkanowy.utils.showSoftInput +import io.github.wulkanowy.utils.* import javax.inject.Inject @AndroidEntryPoint @@ -149,12 +144,14 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { error = getString(R.string.error_field_required) + setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) } } override fun setErrorPassInvalid(focus: Boolean) { with(binding.loginFormPassLayout) { error = getString(R.string.login_invalid_password) + setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) } } @@ -162,6 +159,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme with(binding) { loginFormUsernameLayout.error = " " loginFormPassLayout.error = " " + loginFormPassLayout.setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) loginFormHostLayout.error = " " loginFormErrorBox.text = message ?: getString(R.string.login_incorrect_password_default) loginFormErrorBox.isVisible = true @@ -181,6 +179,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun clearPassError() { binding.loginFormPassLayout.error = null + binding.loginFormPassLayout.setEndIconTintList(null) binding.loginFormErrorBox.isVisible = false } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index cc4c5aaa..77f3eb64 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -6,7 +6,6 @@ import android.content.res.ColorStateList import android.graphics.* import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT -import android.widget.ImageView import androidx.annotation.* import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils @@ -14,7 +13,6 @@ import androidx.core.graphics.applyCanvas import androidx.core.graphics.drawable.RoundedBitmapDrawable import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.toBitmap -import androidx.core.widget.ImageViewCompat @ColorInt @@ -89,6 +87,6 @@ fun Context.createNameInitialsDrawable( .apply { isCircular = true } } -fun ImageView.setTint(@ColorInt color: Int) { - ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color)) +fun Context.getAttrColorStateList(@AttrRes color: Int): ColorStateList { + return ColorStateList.valueOf(getThemeAttrColor(color)) } diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 1aea7066..fac3960e 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -180,12 +180,12 @@ android:layout_marginEnd="24dp" android:layout_marginRight="24dp" android:hint="@string/login_password_hint" + app:endIconMode="password_toggle" app:errorEnabled="true" app:errorIconDrawable="@null" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout" - app:passwordToggleEnabled="true"> + app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout"> Date: Thu, 6 Apr 2023 01:29:46 +0200 Subject: [PATCH 59/77] Use segmented toggle buttons instead of option group in grades statistics (#2164) --- .../statistics/GradeStatisticsAdapter.kt | 4 ++- .../layout/item_grade_statistics_header.xml | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt index fd0ac547..3fce8d57 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt @@ -116,7 +116,9 @@ class GradeStatisticsAdapter @Inject constructor() : } ) - binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId -> + binding.gradeStatisticsTypeSwitch.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + currentDataType = when (checkedId) { R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER diff --git a/app/src/main/res/layout/item_grade_statistics_header.xml b/app/src/main/res/layout/item_grade_statistics_header.xml index 92f522ba..cc35f606 100644 --- a/app/src/main/res/layout/item_grade_statistics_header.xml +++ b/app/src/main/res/layout/item_grade_statistics_header.xml @@ -1,8 +1,10 @@ + android:layout_height="wrap_content" + tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter"> - + android:paddingEnd="16dp" + app:selectionRequired="true" + app:singleSelection="true"> - - - - + - \ No newline at end of file + From bce2c39ccc0801e7831ed9936aa76b287fb435eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 6 Apr 2023 10:13:34 +0200 Subject: [PATCH 60/77] Disable error dialog for admin messages (#2165) --- .../wulkanowy/ui/modules/dashboard/DashboardPresenter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 22b0d267..ac2c896d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -606,7 +606,7 @@ class DashboardPresenter @Inject constructor( } is Resource.Error -> { Timber.i("Loading dashboard admin message result: An exception occurred") - errorHandler.dispatch(it.error) + Timber.e(it.error) updateData( dashboardItem = DashboardItem.AdminMessages( adminMessage = null, @@ -748,7 +748,7 @@ class DashboardPresenter @Inject constructor( itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError - val firstError = itemsLoadedList.mapNotNull { it.error }.firstOrNull() + val firstError = itemsLoadedList.firstNotNullOfOrNull { it.error } val filteredOriginalLoadedList = dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } From 2c9434766861adbc5f7d5d497a4caac12a8c852b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:29:14 +0000 Subject: [PATCH 61/77] Bump androidx.core:core-ktx from 1.9.0 to 1.10.0 (#2167) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a0bc17aa..edb0ca36 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,7 +193,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation "androidx.core:core-ktx:1.9.0" + implementation "androidx.core:core-ktx:1.10.0" implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" From 327e61bbdd0c3dbc37da1029c2aad4aae804b444 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:29:50 +0000 Subject: [PATCH 62/77] Bump about_libraries from 10.6.1 to 10.6.2 (#2166) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2aa3f758..d53632fe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.20' - about_libraries = '10.6.1' + about_libraries = '10.6.2' hilt_version = "2.45" } repositories { From 6978ad11ebab55ffb221b060ba82443e9612929d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:11:17 +0000 Subject: [PATCH 63/77] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.4 to 2.9.5 (#2174) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d53632fe..ad23becd 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.8.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929" From e7054bb5b94c716f76e997fc4542d56ca981ffef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:11:45 +0000 Subject: [PATCH 64/77] Bump com.google.firebase:firebase-bom from 31.4.0 to 31.5.0 (#2173) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index edb0ca36..0cc3592f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.4.0') + playImplementation platform('com.google.firebase:firebase-bom:31.5.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From de8b38dd9ca1a5ab5b2b4618b8d406031c2e8b19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:12:06 +0000 Subject: [PATCH 65/77] Bump org.robolectric:robolectric from 4.9.2 to 4.10 (#2169) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0cc3592f..c5c8992c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -265,7 +265,7 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.9.2' + testImplementation 'org.robolectric:robolectric:4.10' testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test:core:1.5.0" From 1f30cc1f902b25cf2b56cc17a8b896d73ec946ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:12:24 +0000 Subject: [PATCH 66/77] Bump mockk from 1.13.4 to 1.13.5 (#2170) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c5c8992c..d0199a0e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ ext { android_hilt = "1.0.0" room = "2.5.1" chucker = "3.5.2" - mockk = "1.13.4" + mockk = "1.13.5" coroutines = "1.6.4" } From 623f0339e692a362a03e202d3767cba8f7b6c9ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:21:17 +0000 Subject: [PATCH 67/77] Bump com.fredporciuncula:flow-preferences from 1.9.0 to 1.9.1 (#2172) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d0199a0e..b8d4e1ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { implementation "io.coil-kt:coil:2.3.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' - implementation 'com.fredporciuncula:flow-preferences:1.9.0' + implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' playImplementation platform('com.google.firebase:firebase-bom:31.5.0') From b1d22843b59cbcabca741bdb8cdb9d92c2cc3d9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:29:42 +0000 Subject: [PATCH 68/77] Bump androidx.core:core-splashscreen from 1.0.0 to 1.0.1 (#2180) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b8d4e1ca..77b3d8c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,7 +194,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.10.0" - implementation 'androidx.core:core-splashscreen:1.0.0' + implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.6" From b2af5ed57de63a39cd392c3db3c93b91fd7690a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:30:39 +0000 Subject: [PATCH 69/77] Bump com.squareup.okhttp3:logging-interceptor from 4.10.0 to 4.11.0 (#2177) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 77b3d8c4..2a1d5888 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,7 +230,7 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" - implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.11.0" implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" From 56d7e94946dca8536764050fdcf0080df10e9cd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:31:46 +0000 Subject: [PATCH 70/77] Bump com.huawei.agconnect:agcp from 1.8.1.300 to 1.9.0.300 (#2179) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ad23becd..b8813f89 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.4.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.huawei.agconnect:agcp:1.8.1.300' + classpath 'com.huawei.agconnect:agcp:1.9.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" From 4fedb74005e3a3cb02d6792f7e156ec62149c189 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:32:02 +0000 Subject: [PATCH 71/77] Bump kotlin_version from 1.8.20 to 1.8.21 (#2182) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b8813f89..3c8552d2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.20' + kotlin_version = '1.8.21' about_libraries = '10.6.2' hilt_version = "2.45" } From f7fa89638a4e0e55ce56b2f7e254816c1b9b82b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:32:49 +0000 Subject: [PATCH 72/77] Bump com.huawei.agconnect:agconnect-crash from 1.8.1.300 to 1.9.0.300 (#2178) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2a1d5888..77a17a29 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:22.0.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.1.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 4be66637526c7ef1a77194c87532a52c4dbf4b1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:50:34 +0000 Subject: [PATCH 73/77] Bump com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter (#2175) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 77a17a29..732de13c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -229,7 +229,7 @@ dependencies { implementation "com.github.YarikSOffice:lingver:1.3.0" implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.squareup.okhttp3:logging-interceptor:4.11.0" implementation "com.jakewharton.timber:timber:5.0.1" From b1a5a77559df78866d7d194771485ec8746557e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:50:48 +0000 Subject: [PATCH 74/77] Bump androidx.fragment:fragment-ktx from 1.5.6 to 1.5.7 (#2176) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 732de13c..f54d80cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,7 +197,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.5.6" + implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From b195fda026d7d262a3a1c408bcc2fdd11e01527c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 22:02:43 +0000 Subject: [PATCH 75/77] Bump androidx.activity:activity-ktx from 1.7.0 to 1.7.1 (#2181) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f54d80cb..79cb37ff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ dependencies { implementation "androidx.core:core-ktx:1.10.0" implementation 'androidx.core:core-splashscreen:1.0.1' - implementation "androidx.activity:activity-ktx:1.7.0" + implementation "androidx.activity:activity-ktx:1.7.1" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.annotation:annotation:1.6.0" From f8431d7ad6d10b67594599015bd5f6c9d5a45ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 7 May 2023 23:21:59 +0200 Subject: [PATCH 76/77] SDK update (#2168) --- .gitignore | 1 + app/build.gradle | 2 +- .../io/github/wulkanowy/data/DataModule.kt | 2 - .../github/wulkanowy/data/db/entities/Exam.kt | 1 + .../data/mappers/ConferenceMapper.kt | 6 +- .../wulkanowy/data/mappers/ExamMapper.kt | 2 +- .../wulkanowy/data/mappers/MessageMapper.kt | 2 +- .../data/mappers/MobileDeviceMapper.kt | 2 +- .../data/mappers/RegisterUserMapper.kt | 31 +++---- .../wulkanowy/data/mappers/StudentMapper.kt | 37 -------- .../wulkanowy/data/mappers/TimetableMapper.kt | 16 ++-- .../wulkanowy/data/pojos/RegisterUser.kt | 11 ++- .../data/repositories/AppCreatorRepository.kt | 1 - .../data/repositories/AttendanceRepository.kt | 2 +- .../data/repositories/ExamRepository.kt | 2 +- .../data/repositories/NoteRepository.kt | 2 +- .../data/repositories/SemesterRepository.kt | 2 +- .../data/repositories/StudentRepository.kt | 36 ++++---- .../data/repositories/TeacherRepository.kt | 2 +- .../data/repositories/TimetableRepository.kt | 2 +- .../ui/modules/login/LoginErrorHandler.kt | 14 ++-- .../login/advanced/LoginAdvancedFragment.kt | 8 +- .../login/advanced/LoginAdvancedPresenter.kt | 84 ++++--------------- .../login/advanced/LoginAdvancedView.kt | 1 - .../modules/login/form/LoginFormFragment.kt | 4 + .../modules/login/form/LoginFormPresenter.kt | 3 + .../ui/modules/login/form/LoginFormView.kt | 2 + .../LoginStudentSelectFragment.kt | 1 - .../io/github/wulkanowy/utils/SdkExtension.kt | 11 ++- .../main/res/layout/fragment_login_form.xml | 1 - .../io/github/wulkanowy/TestEnityCreator.kt | 2 +- .../data/mappers/AttendanceMapperKtTest.kt | 4 +- .../repositories/AttendanceRepositoryTest.kt | 12 +-- .../data/repositories/ExamRemoteTest.kt | 13 ++- .../data/repositories/GradeRepositoryTest.kt | 33 ++++++-- .../repositories/MessageRepositoryTest.kt | 2 +- .../MobileDeviceRepositoryTest.kt | 17 ++-- .../repositories/SemesterRepositoryTest.kt | 3 +- .../data/repositories/StudentTest.kt | 81 ------------------ .../repositories/TimetableRepositoryTest.kt | 80 ++++++++++++++---- .../domain/GetMailboxByStudentUseCaseTest.kt | 2 +- .../modules/grade/GradeAverageProviderTest.kt | 2 +- .../login/form/LoginFormPresenterTest.kt | 23 ++++- .../LoginStudentSelectPresenterTest.kt | 16 +++- 44 files changed, 260 insertions(+), 321 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt delete mode 100644 app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt diff --git a/.gitignore b/.gitignore index cd5ff714..921bd0a9 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ Thumbs.db app/src/release/agconnect-services.json app/src/release/agconnect-credentials.json .idea/deploymentTargetDropDown.xml +.idea/kotlinc.xml diff --git a/app/build.gradle b/app/build.gradle index 79cb37ff..22de4b8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.9.2" + implementation "io.github.wulkanowy:sdk:14267a9a" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index e538b2b2..c9e4990f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -20,7 +20,6 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.RemoteConfigHelper -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -81,7 +80,6 @@ internal class DataModule { .readTimeout(30, TimeUnit.SECONDS) .build() - @OptIn(ExperimentalSerializationApi::class) @Singleton @Provides fun provideRetrofit( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt index 50299e60..2292c3e6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt @@ -22,6 +22,7 @@ data class Exam( val subject: String, + @Deprecated("not available anymore") val group: String, val type: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt index 17a9e5cd..add6439d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt @@ -10,9 +10,9 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, agenda = it.agenda, conferenceId = it.id, - date = it.dateZoned.toInstant(), + date = it.date.toInstant(), presentOnConference = it.presentOnConference, - subject = it.subject, - title = it.title + subject = it.topic, + title = it.place, ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt index bdb5efbb..173dfebf 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt @@ -11,7 +11,7 @@ fun List.mapToEntities(semester: Semester) = map { date = it.date, entryDate = it.entryDate, subject = it.subject, - group = it.group, + group = "", type = it.type, description = it.description, teacher = it.teacher, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt index 6fc5dc95..a26d7665 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt @@ -26,7 +26,7 @@ fun List.mapToEntities( messageId = it.id, correspondents = it.correspondents, subject = it.subject.trim(), - date = it.dateZoned.toInstant(), + date = it.date.toInstant(), folderId = it.folderId, unread = it.unread, unreadBy = it.unreadBy, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt index 1a1c501f..1f4178fa 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken fun List.mapToEntities(student: Student) = map { MobileDevice( userLoginId = student.userLoginId, - date = it.createDateZoned.toInstant(), + date = it.createDate.toInstant(), deviceId = it.id, name = it.name ) diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt index 2dfd7e06..bcf26a5e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt @@ -3,22 +3,24 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.* -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.mapper.mapSemesters import java.time.Instant -import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent -import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser +import io.github.wulkanowy.sdk.pojo.RegisterStudent as SdkRegisterStudent +import io.github.wulkanowy.sdk.pojo.RegisterUser as SdkRegisterUser -fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser( +fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser( email = email, login = login, password = password, - baseUrl = baseUrl, + scrapperBaseUrl = scrapperBaseUrl, + loginMode = loginMode, loginType = loginType, symbols = symbols.map { registerSymbol -> RegisterSymbol( symbol = registerSymbol.symbol, error = registerSymbol.error, + hebeBaseUrl = registerSymbol.hebeBaseUrl, + keyId = registerSymbol.keyId, + privatePem = registerSymbol.privatePem, userName = registerSymbol.userName, schools = registerSymbol.schools.map { RegisterUnit( @@ -42,14 +44,13 @@ fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser( classId = registerSubject.classId, isParent = registerSubject.isParent, semesters = registerSubject.semesters - .mapSemesters() .mapToEntities(registerSubject.studentId), ) }, ) } ) - } + }, ) fun RegisterStudent.mapToStudentWithSemesters( @@ -68,17 +69,17 @@ fun RegisterStudent.mapToStudentWithSemesters( classId = classId, studentId = studentId, symbol = symbol.symbol, - loginType = user.loginType.name, + loginType = user.loginType?.name.orEmpty(), schoolName = unit.schoolName, schoolShortName = unit.schoolShortName, schoolSymbol = unit.schoolId, studentName = "$studentName $studentSurname", - loginMode = Sdk.Mode.SCRAPPER.name, - scrapperBaseUrl = user.baseUrl, - mobileBaseUrl = "", - certificateKey = "", - privateKey = "", - password = user.password, + loginMode = user.loginMode.name, + scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(), + mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(), + certificateKey = symbol.keyId.orEmpty(), + privateKey = symbol.privatePem.orEmpty(), + password = user.password.orEmpty(), isCurrent = false, registrationDate = Instant.now(), ).apply { diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt deleted file mode 100644 index a2110d7f..00000000 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.wulkanowy.data.mappers - -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentWithSemesters -import java.time.Instant -import io.github.wulkanowy.sdk.pojo.Student as SdkStudent - -fun List.mapToEntities(password: String = "", colors: List) = map { - StudentWithSemesters( - student = Student( - email = it.email, - password = password, - isParent = it.isParent, - symbol = it.symbol, - studentId = it.studentId, - userLoginId = it.userLoginId, - userName = it.userName, - studentName = it.studentName + " " + it.studentSurname, - schoolSymbol = it.schoolSymbol, - schoolShortName = it.schoolShortName, - schoolName = it.schoolName, - className = it.className, - classId = it.classId, - scrapperBaseUrl = it.scrapperBaseUrl, - loginType = it.loginType.name, - isCurrent = false, - registrationDate = Instant.now(), - mobileBaseUrl = it.mobileBaseUrl, - privateKey = it.privateKey, - certificateKey = it.certificateKey, - loginMode = it.loginMode.name, - ).apply { - avatarColor = colors.random() - }, - semesters = it.semesters.mapToEntities(it.studentId) - ) -} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt index e55aa3cf..ee525e10 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt @@ -5,10 +5,10 @@ import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.pojos.TimetableFull -import io.github.wulkanowy.sdk.pojo.TimetableFull as SdkTimetableFull +import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetableFull import io.github.wulkanowy.sdk.pojo.TimetableDayHeader as SdkTimetableHeader -import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable -import io.github.wulkanowy.sdk.pojo.TimetableAdditional as SdkTimetableAdditional +import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson +import io.github.wulkanowy.sdk.pojo.LessonAdditional as SdkTimetableAdditional fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( lessons = lessons.mapToEntities(semester), @@ -16,13 +16,13 @@ fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( headers = headers.mapToEntities(semester) ) -fun List.mapToEntities(semester: Semester) = map { +fun List.mapToEntities(semester: Semester) = map { Timetable( studentId = semester.studentId, diaryId = semester.diaryId, number = it.number, - start = it.startZoned.toInstant(), - end = it.endZoned.toInstant(), + start = it.start.toInstant(), + end = it.end.toInstant(), date = it.date, subject = it.subject, subjectOld = it.subjectOld, @@ -45,8 +45,8 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, subject = it.subject, date = it.date, - start = it.startZoned.toInstant(), - end = it.endZoned.toInstant(), + start = it.start.toInstant(), + end = it.end.toInstant(), ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt index 4aea3377..98bf1402 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt @@ -1,20 +1,25 @@ package io.github.wulkanowy.data.pojos import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper data class RegisterUser( val email: String, - val password: String, + val password: String?, val login: String, // may be the same as email - val baseUrl: String, - val loginType: Scrapper.LoginType, + val scrapperBaseUrl: String?, + val loginType: Scrapper.LoginType?, + val loginMode: Sdk.Mode, val symbols: List, ) : java.io.Serializable data class RegisterSymbol( val symbol: String, val error: Throwable?, + val hebeBaseUrl: String?, + val keyId: String?, + val privatePem: String?, val userName: String, val schools: List, ) : java.io.Serializable diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index cbaa12bd..bec2797d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -19,7 +19,6 @@ class AppCreatorRepository @Inject constructor( ) { @OptIn(ExperimentalSerializationApi::class) - @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.io) { val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index fd5d8bd1..3afb9907 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -59,7 +59,7 @@ class AttendanceRepository @Inject constructor( } sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getAttendance(start.monday, end.sunday, semester.semesterId) + .getAttendance(start.monday, end.sunday) .mapToEntities(semester, lessons) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index faa80b93..013c0951 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -52,7 +52,7 @@ class ExamRepository @Inject constructor( fetch = { sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getExams(start.startExamsDay, start.endExamsDay, semester.semesterId) + .getExams(start.startExamsDay, start.endExamsDay) .mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index e5d7bc5c..4101803f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -42,7 +42,7 @@ class NoteRepository @Inject constructor( fetch = { sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getNotes(semester.semesterId) + .getNotes() .mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 96f01922..92bb3708 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -40,7 +40,7 @@ class SemesterRepository @Inject constructor( val isNoSemesters = semesters.isEmpty() val isRefreshOnModeChangeRequired = when { - Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API -> { + Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { semesters.firstOrNull { it.isCurrent }?.let { 0 == it.diaryId && 0 == it.kindergartenDiaryId } == true diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index b1d1ba83..4c7069ef 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -10,11 +10,9 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.exceptions.NoCurrentStudentException -import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToPojo import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.encrypt @@ -29,37 +27,35 @@ class StudentRepository @Inject constructor( private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appInfo: AppInfo, private val appDatabase: AppDatabase ) { - suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() - suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun getStudentsApi( pin: String, symbol: String, token: String - ): List = - sdk.getStudentsFromMobileApi(token, pin, symbol, "") - .mapToEntities(colors = appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getStudentsFromHebe(token, pin, symbol, "") + .mapToPojo(null) suspend fun getStudentsScrapper( email: String, password: String, scrapperBaseUrl: String, symbol: String - ): List = - sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) - .mapToEntities(password, appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) + .mapToPojo(password) suspend fun getUserSubjectsFromScrapper( email: String, password: String, scrapperBaseUrl: String, symbol: String - ): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) + ): RegisterUser = sdk + .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) .mapToPojo(password) suspend fun getStudentsHybrid( @@ -67,15 +63,15 @@ class StudentRepository @Inject constructor( password: String, scrapperBaseUrl: String, symbol: String - ): List = - sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) - .mapToEntities(password, appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) + .mapToPojo(password) suspend fun getSavedStudents(decryptPass: Boolean = true) = studentDb.loadStudentsWithSemesters() .map { it.apply { - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -85,7 +81,7 @@ class StudentRepository @Inject constructor( suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) = studentDb.loadStudentWithSemestersById(id)?.apply { - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -95,7 +91,7 @@ class StudentRepository @Inject constructor( suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -106,7 +102,7 @@ class StudentRepository @Inject constructor( suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -119,7 +115,7 @@ class StudentRepository @Inject constructor( val students = studentsWithSemesters.map { it.student } .map { it.apply { - if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { + if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { password = withContext(dispatchers.io) { encrypt(password, context) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index acd71e1f..4e3b40f9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -40,7 +40,7 @@ class TeacherRepository @Inject constructor( fetch = { sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getTeachers(semester.semesterId) + .getTeachers() .mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 26e1f3ff..136fb8d5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -66,7 +66,7 @@ class TimetableRepository @Inject constructor( fetch = { val timetableFull = sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getTimetableFull(start.monday, end.sunday) + .getTimetable(start.monday, end.sunday) timetableFull.mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index 37ab71dc..4f709438 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -4,13 +4,15 @@ import android.content.Context import android.database.sqlite.SQLiteConstraintException import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException -import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException -import io.github.wulkanowy.sdk.mobile.exception.InvalidTokenException -import io.github.wulkanowy.sdk.mobile.exception.TokenDeadException +import io.github.wulkanowy.sdk.hebe.exception.InvalidPinException +import io.github.wulkanowy.sdk.hebe.exception.InvalidTokenException +import io.github.wulkanowy.sdk.hebe.exception.TokenDeadException +import io.github.wulkanowy.sdk.hebe.exception.UnknownTokenException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject +import io.github.wulkanowy.sdk.hebe.exception.InvalidSymbolException as InvalidHebeSymbolException +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException as InvalidScrapperSymbolException class LoginErrorHandler @Inject constructor( @ApplicationContext context: Context, @@ -32,9 +34,11 @@ class LoginErrorHandler @Inject constructor( is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) + is UnknownTokenException, is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) is InvalidPinException -> onInvalidPin(resources.getString(R.string.login_invalid_pin)) - is InvalidSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) + is InvalidScrapperSymbolException, + is InvalidHebeSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) else -> super.proceed(error) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 8c90623e..ead2d71a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -34,9 +34,9 @@ class LoginAdvancedFragment : override val formLoginType: String get() = when (binding.loginTypeSwitch.checkedRadioButtonId) { - R.id.loginTypeApi -> "API" - R.id.loginTypeScrapper -> "SCRAPPER" - else -> "HYBRID" + R.id.loginTypeApi -> Sdk.Mode.HEBE.name + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER.name + else -> Sdk.Mode.HYBRID.name } override val formUsernameValue: String @@ -99,7 +99,7 @@ class LoginAdvancedFragment : loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> presenter.onLoginModeSelected( when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeApi -> Sdk.Mode.HEBE R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER else -> Sdk.Mode.HYBRID } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index 33a76e5f..ab56bd78 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -1,17 +1,12 @@ package io.github.wulkanowy.ui.modules.login.advanced import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.onResourceNotLoading -import io.github.wulkanowy.data.pojos.RegisterStudent -import io.github.wulkanowy.data.pojos.RegisterSymbol -import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData @@ -97,14 +92,16 @@ class LoginAdvancedPresenter @Inject constructor( fun onLoginModeSelected(type: Sdk.Mode) { view?.run { when (type) { - Sdk.Mode.API -> { + Sdk.Mode.HEBE -> { showOnlyMobileApiModeInputs() showMobileApiWarningMessage() } + Sdk.Mode.SCRAPPER -> { showOnlyScrapperModeInputs() showScraperWarningMessage() } + Sdk.Mode.HYBRID -> { showOnlyHybridModeInputs() showHybridWarningMessage() @@ -145,11 +142,12 @@ class LoginAdvancedPresenter @Inject constructor( showProgress(true) showContent(false) } + is Resource.Success -> { analytics.logEvent( "registration_form", "success" to true, - "students" to it.data.size, + "scrapperBaseUrl" to view?.formHostValue.orEmpty(), "error" to "No error" ) val loginData = LoginData( @@ -158,14 +156,15 @@ class LoginAdvancedPresenter @Inject constructor( baseUrl = view?.formHostValue.orEmpty().trim(), symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), ) - when (it.data.size) { + when (it.data.symbols.size) { 0 -> view?.navigateToSymbol(loginData) else -> view?.navigateToStudentSelect( loginData = loginData, - registerUser = it.data.toRegisterUser(loginData), + registerUser = it.data, ) } } + is Resource.Error -> { analytics.logEvent( "registration_form", @@ -183,59 +182,7 @@ class LoginAdvancedPresenter @Inject constructor( }.launch("login") } - private fun List.toRegisterUser(loginData: LoginData) = RegisterUser( - email = loginData.login, - password = loginData.password, - login = loginData.login, - baseUrl = loginData.baseUrl, - loginType = firstOrNull()?.student?.loginType?.let( - Scrapper.LoginType::valueOf - ) ?: Scrapper.LoginType.AUTO, - symbols = this - .groupBy { students -> students.student.symbol } - .map { (symbol, students) -> - RegisterSymbol( - symbol = symbol, - error = null, - userName = "", - schools = students - .groupBy { student -> - Triple( - first = student.student.schoolSymbol, - second = student.student.userLoginId, - third = student.student.schoolShortName - ) - } - .map { (groupKey, students) -> - val (schoolId, loginId, schoolName) = groupKey - RegisterUnit( - students = students.map { - RegisterStudent( - studentId = it.student.studentId, - studentName = it.student.studentName, - studentSecondName = it.student.studentName, - studentSurname = it.student.studentName, - className = it.student.className, - classId = it.student.classId, - isParent = it.student.isParent, - semesters = it.semesters, - ) - }, - userLoginId = loginId, - schoolId = schoolId, - schoolName = schoolName, - schoolShortName = schoolName, - parentIds = listOf(), - studentIds = listOf(), - employeeIds = listOf(), - error = null - ) - } - ) - }, - ) - - private suspend fun getStudentsAppropriatesToLoginType(): List { + private suspend fun getStudentsAppropriatesToLoginType(): RegisterUser { val email = view?.formUsernameValue.orEmpty() val password = view?.formPassValue.orEmpty() val endpoint = view?.formHostValue.orEmpty() @@ -245,10 +192,11 @@ class LoginAdvancedPresenter @Inject constructor( val token = view?.formTokenValue.orEmpty() return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { - Sdk.Mode.API -> studentRepository.getStudentsApi(pin, symbol, token) + Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token) Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper( email, password, endpoint, symbol ) + Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( email, password, endpoint, symbol ) @@ -267,8 +215,8 @@ class LoginAdvancedPresenter @Inject constructor( var isCorrect = true - when (Sdk.Mode.valueOf(view?.formLoginType ?: "")) { - Sdk.Mode.API -> { + when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { + Sdk.Mode.HEBE -> { if (pin.isEmpty()) { view?.setErrorPinRequired() isCorrect = false @@ -284,17 +232,17 @@ class LoginAdvancedPresenter @Inject constructor( isCorrect = false } } + Sdk.Mode.HYBRID, Sdk.Mode.SCRAPPER -> { if (login.isEmpty()) { view?.setErrorUsernameRequired() isCorrect = false } else { - if ("@" in login && "standard" !in host) { + if ("@" in login && "login" in host) { view?.setErrorLoginRequired() isCorrect = false } - - if ("@" !in login && "standard" in host) { + if ("@" !in login && "email" in host) { view?.setErrorEmailRequired() isCorrect = false } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt index 824fa028..34062d93 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.login.advanced -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index bbc38219..43ba3fe1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -204,6 +204,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } + override fun showOtherOptionsButton(show: Boolean) { + binding.loginFormAdvancedButton.isVisible = show + } + @SuppressLint("SetTextI18n") override fun showVersion() { binding.loginFormVersion.text = "v${appInfo.versionName}" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 8035ea0a..ed70eb12 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.ifNullOrBlank import timber.log.Timber import java.net.URL @@ -15,6 +16,7 @@ import javax.inject.Inject class LoginFormPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, + private val appInfo: AppInfo, private val analytics: AnalyticsHelper ) : BasePresenter(loginErrorHandler, studentRepository) { @@ -25,6 +27,7 @@ class LoginFormPresenter @Inject constructor( view.run { initView() showContact(false) + showOtherOptionsButton(appInfo.isDebug) showVersion() loginErrorHandler.onBadCredentials = { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 5a816fb3..e5c680d6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -56,6 +56,8 @@ interface LoginFormView : BaseView { fun showContent(show: Boolean) + fun showOtherOptionsButton(show: Boolean) + fun showVersion() fun navigateToSymbol(loginData: LoginData) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 16970215..c33d12fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -55,7 +55,6 @@ class LoginStudentSelectFragment : } } - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLoginStudentSelectBinding.bind(view) diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 63a30db8..481cad11 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -12,18 +12,17 @@ fun Sdk.init(student: Student): Sdk { studentId = student.studentId classId = student.classId - if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { + mobileBaseUrl = student.mobileBaseUrl + } else { scrapperBaseUrl = student.scrapperBaseUrl loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) } - loginId = student.userLoginId mode = Sdk.Mode.valueOf(student.loginMode) mobileBaseUrl = student.mobileBaseUrl - certKey = student.certificateKey - privateKey = student.privateKey - - emptyCookieJarInterceptor = true + keyId = student.certificateKey + privatePem = student.privateKey Timber.d("Sdk in ${student.loginMode} mode reinitialized") diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index fac3960e..3bfe0c34 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -247,7 +247,6 @@ android:layout_marginEnd="16dp" android:text="@string/login_advanced" android:textAppearance="?android:textAppearance" - android:visibility="gone" app:backgroundTint="?android:windowBackground" app:fontFamily="sans-serif-medium" app:layout_constraintBottom_toBottomOf="@id/loginFormSignIn" diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index 84a0cb40..c8d95829 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -48,7 +48,7 @@ fun getSemesterPojo(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalD end = end, ) -fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.API) = Student( +fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.HEBE) = Student( scrapperBaseUrl = "http://fakelog.cf", email = "jan@fakelog.cf", certificateKey = "", diff --git a/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt index a35e5d30..ac73becd 100644 --- a/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.sdk.pojo.Attendance -import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuse +import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuseStatus import org.junit.Test import java.time.Instant import java.time.LocalDate @@ -98,7 +98,7 @@ class AttendanceMapperTest { timeId = 1, categoryId = 1, deleted = false, - excuseStatus = SentExcuse.Status.WAITING, + excuseStatus = SentExcuseStatus.WAITING, excusable = false, absence = false, excused = false, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index 896491ef..d0e500f1 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -63,7 +63,7 @@ class AttendanceRepositoryTest { @Test fun `force refresh without difference`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())) @@ -77,7 +77,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } @@ -86,7 +86,7 @@ class AttendanceRepositoryTest { @Test fun `force refresh with more items in remote`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), // after fetch end before save result @@ -101,7 +101,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { @@ -114,7 +114,7 @@ class AttendanceRepositoryTest { @Test fun `force refresh with more items in local`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList.dropLast(1) + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList.dropLast(1) coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())), // after fetch end before save result @@ -129,7 +129,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index e3790662..fb037a87 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -59,7 +59,7 @@ class ExamRemoteTest { @Test fun `force refresh without difference`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) @@ -73,7 +73,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { examDb.deleteAll(match { it.isEmpty() }) } @@ -82,7 +82,7 @@ class ExamRemoteTest { @Test fun `force refresh with more items in remote`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result @@ -97,7 +97,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { @@ -110,7 +110,7 @@ class ExamRemoteTest { @Test fun `force refresh with more items in local`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList.dropLast(1) + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList.dropLast(1) coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result @@ -125,7 +125,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { @@ -137,7 +137,6 @@ class ExamRemoteTest { private fun getExam(date: LocalDate) = SdkExam( subject = "", - group = "", type = "", description = "", teacher = "", diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index e8d0b6c8..1d6dfaff 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -9,13 +9,21 @@ import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.pojo.Grades import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.time.LocalDate @@ -72,7 +80,7 @@ class GradeRepositoryTest { createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"), createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), // empty because it is new user @@ -122,7 +130,7 @@ class GradeRepositoryTest { ), createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Jedna ocena"), @@ -169,7 +177,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -200,7 +208,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), // will be added... createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -230,7 +238,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), @@ -250,7 +258,7 @@ class GradeRepositoryTest { fun `force refresh when remote is empty`() { // prepare val remoteList = emptyList() - coEvery { sdk.getGrades(semester.semesterId) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(semester.semesterId) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -284,4 +292,13 @@ class GradeRepositoryTest { weight = weight.toString(), weightValue = weight ) + + private fun createGrades(grades: List): Grades = Grades( + details = grades, + summary = listOf(), + isAverage = false, + isPoints = false, + isForAdults = false, + type = 0, + ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 9a2c22fd..3a18ee97 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -224,7 +224,7 @@ class MessageRepositoryTest { recipients = listOf(), subject = "", content = "Test", - dateZoned = Instant.EPOCH.atZone(ZoneOffset.UTC), + date = Instant.EPOCH.atZone(ZoneOffset.UTC), folderId = 1, unread = true, readBy = 1, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index 6865aa7d..1a3f9679 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -10,16 +10,21 @@ import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Device import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test -import java.time.LocalDateTime.of -import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime.of class MobileDeviceRepositoryTest { @@ -134,9 +139,7 @@ class MobileDeviceRepositoryTest { id = 0, name = "", deviceId = "", - createDate = of(2019, 5, day, 0, 0, 0), - modificationDate = of(2019, 5, day, 0, 0, 0), - createDateZoned = of(2019, 5, day, 0, 0, 0).atZone(ZoneId.systemDefault()), - modificationDateZoned = of(2019, 5, day, 0, 0, 0).atZone(ZoneId.systemDefault()) + createDate = of(2019, 5, day, 0, 0, 0, 0, ZoneOffset.UTC), + modificationDate = of(2019, 5, day, 0, 0, 0, 0, ZoneOffset.UTC), ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index 0ed00885..d8256869 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt @@ -75,7 +75,7 @@ class SemesterRepositoryTest { coEvery { semesterDb.deleteAll(any()) } just Runs coEvery { semesterDb.insertSemesters(any()) } returns listOf() - val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.API.name)) } + val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.HEBE.name)) } assertEquals(2, items.size) assertEquals(0, items[0].diaryId) } @@ -215,6 +215,7 @@ class SemesterRepositoryTest { @Test(expected = RuntimeException::class) fun getCurrentSemester_emptyList() { coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns emptyList() + coEvery { sdk.getSemesters() } returns emptyList() runBlocking { semesterRepository.getCurrentSemester(student) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt deleted file mode 100644 index 9d3d7a2e..00000000 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.wulkanowy.data.repositories - -import io.github.wulkanowy.TestDispatchersProvider -import io.github.wulkanowy.data.db.dao.SemesterDao -import io.github.wulkanowy.data.db.dao.StudentDao -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.pojo.Student -import io.github.wulkanowy.utils.AppInfo -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -class StudentTest { - - @MockK - private lateinit var mockSdk: Sdk - - @MockK - private lateinit var studentDb: StudentDao - - @MockK - private lateinit var semesterDb: SemesterDao - - private lateinit var studentRepository: StudentRepository - - @Before - fun initApi() { - MockKAnnotations.init(this) - studentRepository = StudentRepository( - context = mockk(), - dispatchers = TestDispatchersProvider(), - studentDb = studentDb, - semesterDb = semesterDb, - sdk = mockSdk, - appInfo = AppInfo(), - appDatabase = mockk() - ) - } - - @Test - fun testRemoteAll() { - coEvery { mockSdk.getStudentsFromScrapper(any(), any(), any(), any()) } returns listOf( - getStudent("test") - ) - - val students = runBlocking { studentRepository.getStudentsScrapper("", "", "http://fakelog.cf", "") } - assertEquals(1, students.size) - assertEquals("test Kowalski", students.first().student.studentName) - } - - private fun getStudent(name: String): Student { - return Student( - email = "", - symbol = "", - studentId = 0, - userLoginId = 0, - userLogin = "", - userName = "", - studentName = name, - studentSurname = "Kowalski", - schoolSymbol = "", - schoolShortName = "", - schoolName = "", - className = "", - classId = 0, - certificateKey = "", - privateKey = "", - loginMode = Sdk.Mode.SCRAPPER, - mobileBaseUrl = "", - loginType = Sdk.ScrapperLoginType.STANDARD, - scrapperBaseUrl = "", - isParent = false, - semesters = emptyList() - ) - } -} diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index e56aaa5d..92ad01b1 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -10,12 +10,17 @@ import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.pojo.TimetableFull import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -25,7 +30,7 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalDateTime.of import java.time.ZoneId -import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable +import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson class TimetableRepositoryTest { @@ -62,18 +67,43 @@ class TimetableRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) + timetableRepository = TimetableRepository( + timetableDb, + timetableAdditionalDao, + timetableHeaderDao, + sdk, + timetableNotificationSchedulerHelper, + refreshHelper + ) } @Test fun `force refresh without difference`() { val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "Jan Kowalski", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język niemiecki", "Joanna Czarniecka", true) + createTimetableRemote( + start = of(2021, 1, 4, 8, 0), + number = 1, + room = "123", + subject = "Język polski", + teacher = "Jan Kowalski", + changes = false + ), + createTimetableRemote( + start = of(2021, 1, 4, 8, 50), + number = 2, + room = "124", + subject = "Język niemiecki", + teacher = "Joanna Czarniecka", + changes = true + ) ) // prepare - coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList()) + coEvery { sdk.getTimetable(startDate, endDate) } returns mockk { + every { headers } returns emptyList() + every { lessons } returns remoteList + every { additional } returns emptyList() + } coEvery { timetableDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) @@ -81,7 +111,14 @@ class TimetableRepositoryTest { coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { timetableDb.deleteAll(any()) } just Runs - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) + coEvery { + timetableAdditionalDao.loadAll( + diaryId = 1, + studentId = 1, + from = startDate, + end = endDate + ) + } returns flowOf(listOf()) coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) @@ -90,23 +127,36 @@ class TimetableRepositoryTest { coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs // execute - val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } + val res = runBlocking { + timetableRepository.getTimetable( + student = student, + semester = semester, + start = startDate, + end = endDate, + forceRefresh = true + ).toFirstResult() + } // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull!!.lessons.size) - coVerify { sdk.getTimetableFull(startDate, endDate) } + coVerify { sdk.getTimetable(startDate, endDate) } coVerify { timetableDb.loadAll(1, 1, startDate, endDate) } coVerify { timetableDb.insertAll(match { it.isEmpty() }) } coVerify { timetableDb.deleteAll(match { it.isEmpty() }) } } - private fun createTimetableRemote(start: LocalDateTime, number: Int = 1, room: String = "", subject: String = "", teacher: String = "", changes: Boolean = false) = SdkTimetable( + private fun createTimetableRemote( + start: LocalDateTime, + number: Int = 1, + room: String = "", + subject: String = "", + teacher: String = "", + changes: Boolean = false + ) = SdkLesson( number = number, - start = start, - end = start.plusMinutes(45), - startZoned = start.atZone(ZoneId.systemDefault()), - endZoned = start.plusMinutes(45).atZone(ZoneId.systemDefault()), + start = start.atZone(ZoneId.systemDefault()), + end = start.plusMinutes(45).atZone(ZoneId.systemDefault()), date = start.toLocalDate(), subject = subject, group = "", diff --git a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt index 6db16d2f..f58a5381 100644 --- a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt +++ b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt @@ -207,7 +207,7 @@ class GetMailboxByStudentUseCaseTest { className = "", isCurrent = false, isParent = false, - loginMode = Sdk.Mode.API.name, + loginMode = Sdk.Mode.HEBE.name, loginType = Sdk.ScrapperLoginType.STANDARD.name, mobileBaseUrl = "", password = "", diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 10c84efc..b94002c0 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -456,7 +456,7 @@ class GradeAverageProviderTest { @Test fun `force calc current semester average with custom modifiers in api mode`() { - val student = student.copy(loginMode = Sdk.Mode.API.name) + val student = student.copy(loginMode = Sdk.Mode.HEBE.name) every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt index bf2d9f2c..eb1f5300 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt @@ -3,11 +3,18 @@ package io.github.wulkanowy.ui.modules.login.form import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.* +import io.github.wulkanowy.utils.AppInfo +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test @@ -30,13 +37,17 @@ class LoginFormPresenterTest { @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper + @MockK + lateinit var appInfo: AppInfo + private lateinit var presenter: LoginFormPresenter private val registerUser = RegisterUser( email = "", password = "", login = "", - baseUrl = "", + scrapperBaseUrl = "", + loginMode = Sdk.Mode.HEBE, loginType = Scrapper.LoginType.AUTO, symbols = listOf(), ) @@ -54,8 +65,14 @@ class LoginFormPresenterTest { every { loginFormView.setErrorPassInvalid(any()) } just Runs every { loginFormView.setErrorPassRequired(any()) } just Runs every { loginFormView.setErrorUsernameRequired() } just Runs + every { appInfo.isDebug } returns false - presenter = LoginFormPresenter(repository, errorHandler, analytics) + presenter = LoginFormPresenter( + studentRepository = repository, + loginErrorHandler = errorHandler, + appInfo = appInfo, + analytics = analytics, + ) presenter.onAttachView(loginFormView) } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index cf426a50..da292c51 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -6,14 +6,22 @@ import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.slot +import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test @@ -76,6 +84,9 @@ class LoginStudentSelectPresenterTest { symbol = "", error = null, userName = "", + keyId = null, + privatePem = null, + hebeBaseUrl = null, schools = listOf(school), ) @@ -83,7 +94,8 @@ class LoginStudentSelectPresenterTest { email = "", password = "", login = "", - baseUrl = "", + scrapperBaseUrl = "", + loginMode = Sdk.Mode.SCRAPPER, loginType = Scrapper.LoginType.AUTO, symbols = listOf(symbol), ) From d99c93ec052f48c3ceb95207397212e086b2e2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 7 May 2023 23:40:18 +0200 Subject: [PATCH 77/77] Version 2.0.0 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 22de4b8b..bcca5587 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 121 - versionName "1.9.2" + versionCode 122 + versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -186,7 +186,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:14267a9a" + implementation "io.github.wulkanowy:sdk:2.0.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index b3fec438..c5ec9de3 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,7 +1,8 @@ -Wersja 1.9.2 +Wersja 2.0.0 -- naprawiliśmy oznaczanie wiadomości jako odczytanych (problem dotyczył głównie kont rodziców z wieloma dziećmi w tej samej szkole) -- naprawiliśmy zapisywanie załączników do wiadomości w sytuacji, gdy ten sam załącznik był dodany do więcej niż jednej wiadomości -- usprawniliśmy ekran z wyborem uczniów i wpisywaniem symbolu przy pierwszym logowaniu +— zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 +— dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym +— poprawiliśmy sposób wyświetlania błędu o nieprawidłowym haśle na ekranie logowania +— od teraz zmiana ustawień liczenia średniej automatycznie odświeży listę ocen Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases