Compare commits

...

131 Commits

Author SHA1 Message Date
a69361708e Merge branch 'release/2.6.4' 2024-05-10 12:34:11 +02:00
567d868f76 Version 2.6.4 2024-05-10 12:30:30 +02:00
12030efee2 Version 2.6.3 2024-05-09 22:59:20 +02:00
3cbe98d7b8 Merge branch 'release/2.6.3' 2024-05-09 22:58:03 +02:00
503a97bc4c Version 2.6.3 2024-05-09 22:57:58 +02:00
04c382643d Fix a typo in settings - apperance & behaviour - menu configuration (#2547) 2024-05-09 18:17:10 +02:00
fc140ad9c1 Merge branch 'release/2.6.2' into develop 2024-05-08 01:55:42 +02:00
ae6a35121b Merge branch 'release/2.6.2' 2024-05-08 01:55:38 +02:00
78a2cc89e9 Version 2.6.2 2024-05-08 01:55:09 +02:00
dbfe5c8918 Bump com.android.tools.build:gradle from 8.3.2 to 8.4.0 (#2539) 2024-05-05 08:27:41 +00:00
c697ca7ad1 Bump androidx.fragment:fragment-ktx from 1.6.2 to 1.7.0 (#2538) 2024-05-05 08:27:28 +00:00
1b00f4e518 Bump about_libraries from 11.1.3 to 11.1.4 (#2540) 2024-05-05 08:27:10 +00:00
fb77bf882f Bump androidx.viewpager2:viewpager2 from 1.1.0-beta02 to 1.1.0-rc01 (#2542) 2024-05-05 08:26:55 +00:00
466ebbef3a Bump com.google.firebase:firebase-bom from 32.8.1 to 33.0.0 (#2544) 2024-05-05 08:26:29 +00:00
458a4c8164 Bump androidx.core:core-ktx from 1.13.0 to 1.13.1 (#2545) 2024-05-05 08:26:14 +00:00
0363c0854f Merge branch 'release/2.6.1' into develop 2024-05-02 15:20:56 +02:00
558addd097 Merge branch 'release/2.6.1' 2024-05-02 15:20:51 +02:00
233ddc955b Version 2.6.1 2024-05-02 15:20:45 +02:00
065c711f91 Add missing release client_info definition to conform gms plugin in hms release (#2537) 2024-05-02 14:10:33 +02:00
49655c11c9 Revert "Bump com.google.android.gms:play-services-ads from 22.6.0 to 23.0.0 (…" (#2536)
This reverts commit a7c2009e49.
2024-05-02 12:43:03 +02:00
38fd4eda22 Merge branch 'release/2.6.0' into develop 2024-05-01 22:30:53 +02:00
e8f9c57c34 Merge branch 'release/2.6.0' 2024-05-01 22:30:48 +02:00
b71630246a Version 2.6.0 2024-05-01 22:30:41 +02:00
fd2eac1f08 New Crowdin updates (#2535) 2024-05-01 19:28:49 +02:00
1545ff65d3 CS and SK listing update (#2534) 2024-05-01 19:07:41 +02:00
ff32c82851 New Crowdin updates (#2532)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-01 18:59:57 +02:00
d531a94594 Update screenshots (#2533) 2024-05-01 18:50:57 +02:00
71ab9586ac Add admin message to LoginStudentSelect and LoginSymbol (#2531)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-01 16:57:31 +02:00
e1a19be06c New Crowdin updates (#2530) 2024-05-01 12:29:41 +02:00
6f2168d641 Additional lessons in timetable view (#2491)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-04-26 23:33:45 +02:00
cde2121b60 New Crowdin updates (#2527) 2024-04-26 22:05:46 +02:00
a0bc37e826 Merge branch 'bugfix/2.5.8' into develop 2024-04-25 12:45:26 +02:00
4d67de8e5f Merge branch 'bugfix/2.5.8' 2024-04-25 12:45:19 +02:00
f983a23b1a Version 2.5.8 2024-04-25 12:45:09 +02:00
6a1851da13 Bump sdk to 2.5.8-SNAPSHOT 2024-04-25 09:26:47 +02:00
ad5381ce34 Fix race condition of showing empty view in timetable (#2486)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-04-24 22:46:55 +02:00
dbc7587741 Add settings button to attendance calculator (#2492) 2024-04-24 22:44:59 +02:00
bc3aa7b8dc New Crowdin updates (#2526) 2024-04-24 22:28:16 +02:00
6bf6a9da11 New Crowdin updates (#2480) 2024-04-24 19:27:11 +02:00
ab175bdd9a Merge branch 'bugfix/2.5.7' into develop 2024-04-22 22:42:22 +02:00
8dbbea2138 Merge branch 'bugfix/2.5.7' 2024-04-22 22:36:13 +02:00
f6226e6b53 Version 2.5.7 2024-04-22 22:36:05 +02:00
43d13db07c Update translations 2024-04-22 22:17:03 +02:00
82210c37e3 Display separate annual average and semester average if available (#2524)
* Add displaying year average on grade details screen

* Add displaying year average on grade summary screen

* Add displaying year average on grade summary header

* Fix tests

* Hide semester average if it is not available in grade summary item

* Add full names of summary averages labels
2024-04-22 22:03:42 +02:00
2816d7217a Version 2.5.6 2024-04-22 00:39:44 +02:00
2fa868173b Show graduated students on top of student select items list 2024-04-22 00:39:44 +02:00
622c75bb42 Don't display brackets in login student select items when schoolShortName is blank 2024-04-22 00:39:44 +02:00
2121125283 Migrate away from userLoginId to studentId due to vulcan last changes 2024-04-22 00:39:44 +02:00
c72a117e34 Bump sdk to 2.5.6-SNAPSHOT 2024-04-22 00:39:44 +02:00
b5cc32d59f Merge branch 'bugfix/2.5.6' 2024-04-22 00:39:19 +02:00
d943d03266 Version 2.5.6 2024-04-22 00:39:04 +02:00
6eca8c42f5 Show graduated students on top of student select items list 2024-04-22 00:11:29 +02:00
af989ba9f6 Don't display brackets in login student select items when schoolShortName is blank 2024-04-21 23:59:29 +02:00
4a65a5b192 Migrate away from userLoginId to studentId due to vulcan last changes 2024-04-21 23:51:34 +02:00
bbbafdfe70 Bump sdk to 2.5.6-SNAPSHOT 2024-04-21 20:37:31 +02:00
860095e862 Bump androidx.activity:activity-ktx from 1.8.2 to 1.9.0 (#2522) 2024-04-21 02:31:28 +00:00
ff9be43291 Bump org.apache.commons:commons-text from 1.11.0 to 1.12.0 (#2523) 2024-04-21 02:25:16 +00:00
a487378daf Bump androidx.core:core-ktx from 1.12.0 to 1.13.0 (#2521) 2024-04-21 02:24:04 +00:00
895f5cbb76 Bump com.android.tools.build:gradle from 8.2.2 to 8.3.2 (#2517) 2024-04-14 14:24:38 +00:00
8b9b1460ab Update AuthDialog text (#2506) 2024-04-13 22:03:23 +02:00
7edd3df074 Bump about_libraries from 11.1.1 to 11.1.3 (#2518) 2024-04-13 19:56:16 +00:00
16c51f7b07 Bump com.google.firebase:firebase-bom from 32.8.0 to 32.8.1 (#2519) 2024-04-13 19:55:50 +00:00
7a3a97447f Add isAdded check in AdditionalLessonAddDialog (#2515) 2024-04-08 20:51:05 +02:00
b500d8e204 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2511) 2024-04-08 18:09:38 +00:00
c34a369286 Bump org.robolectric:robolectric from 4.11.1 to 4.12.1 (#2514) 2024-04-08 18:08:20 +00:00
b9f3ab2e56 Bump com.squareup.retrofit2:retrofit from 2.10.0 to 2.11.0 (#2513) 2024-04-08 18:08:01 +00:00
6b59973624 Bump hilt_version from 2.51 to 2.51.1 (#2510) 2024-04-08 17:41:28 +00:00
d18485293d Merge branch 'bugfix/2.5.5' into develop 2024-03-26 20:38:07 +01:00
a82e11d694 Merge branch 'bugfix/2.5.5' 2024-03-26 20:38:01 +01:00
4dc5fc65ac Version 2.5.5 2024-03-26 20:37:51 +01:00
7463cf6253 Replace function in DAO to 'OR' in SQL query 2024-03-26 20:29:35 +01:00
d799ec7ac9 Add skipping migration when previous attempt failed (#2508) 2024-03-26 19:19:44 +01:00
254719f22f Remove classId from semester query when eduOne (#2509) 2024-03-26 19:02:35 +01:00
596e8df4fc Merge branch 'bugfix/2.5.4' into develop 2024-03-26 13:29:54 +01:00
e1e276e1ea Merge branch 'bugfix/2.5.4' 2024-03-26 13:29:26 +01:00
8cdd4311a9 Version 2.5.4 2024-03-26 13:29:22 +01:00
b7f7b16aef Add logging error from units and symbols during registration (#2507) 2024-03-26 12:54:07 +01:00
f13ce6e2b4 Bump com.squareup.retrofit2:retrofit from 2.9.0 to 2.10.0 (#2502) 2024-03-25 12:32:28 +00:00
8c10606b61 Bump about_libraries from 11.1.0 to 11.1.1 (#2503) 2024-03-25 12:32:07 +00:00
7fda4276d6 Bump com.google.firebase:firebase-bom from 32.7.4 to 32.8.0 (#2504) 2024-03-25 12:31:46 +00:00
7993366bfc Merge branch 'bugfix/2.5.3' into develop 2024-03-25 00:05:11 +01:00
2e71c50894 Merge branch 'bugfix/2.5.3' 2024-03-24 23:43:51 +01:00
b3faac01a5 Version 2.5.3 2024-03-24 23:43:45 +01:00
3881678208 Fix issues related to not authenticated eduOne students (#2501) 2024-03-24 23:22:07 +01:00
76d038eefa Hide grade statistics when is eduOne, add isEduOne flag migration (#2496)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-03-24 19:42:36 +01:00
3a55c3c760 Add ignoring FeatureUnavailableException in SyncWorker (#2500) 2024-03-24 18:55:23 +01:00
a0818de7d1 Add isAdded condition to HomeworkAddDialog (#2499) 2024-03-24 14:55:05 +01:00
b280316b07 Bump sdk to 2.5.3-SNAPSHOT 2024-03-21 23:06:18 +01:00
0554aa91fd Add WulkanowySdkFactory (#2479) 2024-03-21 22:11:03 +01:00
5a77d1e940 Hide lesson number when is eduOne (#2498) 2024-03-21 21:19:49 +01:00
c9a42a6cf6 Add try catch to initialize MobileAds SDK (#2497) 2024-03-21 11:03:08 +01:00
27eb0588d7 Merge branch 'bugfix/2.5.2' into develop 2024-03-20 02:24:26 +01:00
e17129efea Merge branch 'bugfix/2.5.2' 2024-03-20 02:18:07 +01:00
6047af9ff0 Version 2.5.2 2024-03-20 02:18:00 +01:00
d789aa718e Change AuthDialog condition to isAuth flag (#2495)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-03-20 01:49:55 +01:00
8623b53357 Fix lateness color in attendance (#2481) 2024-03-19 22:13:32 +01:00
78e28ad791 Remove savedInstance in MessagePreviewFragment (#2477) 2024-03-19 22:13:09 +01:00
377c288e9e Add missing onDetachView in AutDialog (#2476) 2024-03-19 22:12:54 +01:00
b31c7e1720 Fix task description color crash (#2475) 2024-03-19 22:12:39 +01:00
d01fe9c370 Bump sdk to 2.5.2-SNAPSHOT 2024-03-19 22:11:02 +01:00
34d34a050a Add widget updating on data sync (#2487)
---------

Co-authored-by: Faierbel <RafalBO99@outlook.com>
2024-03-17 21:06:40 +01:00
5ed19cb21a Merge branch 'release/2.5.1' 2024-03-03 11:19:42 +01:00
7b2c839775 Merge branch 'release/2.5.0' 2024-03-02 21:18:10 +01:00
3eae3a7667 Merge branch 'release/2.4.2' 2024-02-22 16:15:30 +01:00
90a5b9e20f Merge branch 'release/2.4.1' 2024-02-17 13:10:34 +01:00
b99ba48d2c Merge branch 'release/2.4.0' 2024-02-09 19:45:01 +01:00
496695162d Merge branch 'release/2.3.5' 2024-01-21 21:04:29 +01:00
1fe464a289 Merge branch 'release/2.3.4' 2024-01-14 17:33:14 +01:00
17c139b559 Merge branch 'release/2.3.3' 2024-01-09 21:46:04 +01:00
f893170dec Merge branch 'release/2.3.2' 2024-01-09 19:31:22 +01:00
770749e158 Merge branch 'release/2.3.1' 2024-01-03 16:02:32 +01:00
eb31f9578f Merge branch 'release/2.3.0' 2024-01-02 01:51:52 +01:00
a2a7d2ebb2 Merge branch 'release/2.2.7' 2023-12-27 22:21:13 +01:00
c64be2fab0 Merge branch 'release/2.2.6' 2023-11-06 11:22:19 +01:00
d9bab2af78 Merge branch 'release/2.2.5' 2023-11-03 23:05:42 +01:00
aba08e6aa9 Merge branch 'release/2.2.4' 2023-10-27 14:49:46 +02:00
387ff1cba7 Merge branch 'release/2.2.3' 2023-10-26 18:31:56 +02:00
6071b7571b Merge branch 'release/2.2.2' 2023-10-23 20:02:25 +02:00
bcd305bef3 Merge branch 'release/2.2.1' 2023-10-03 01:14:17 +02:00
1d8378e136 Merge branch 'release/2.2.0' 2023-09-26 23:13:40 +02:00
af346842a3 Merge branch 'release/2.1.0' 2023-08-25 00:01:42 +02:00
391f38485d Merge branch 'release/2.0.8' 2023-06-01 23:43:06 +02:00
fd482777e8 Merge branch 'release/2.0.7' 2023-06-01 10:59:55 +02:00
cc46b3b124 Merge branch 'release/2.0.6' 2023-05-23 16:26:36 +02:00
c40cdf88ad Merge branch 'release/2.0.5' 2023-05-23 02:37:43 +02:00
5c440010e2 Merge branch 'release/2.0.4' 2023-05-22 17:10:02 +02:00
d08f195968 Merge branch 'release/2.0.3' 2023-05-12 22:59:45 +02:00
88c38c4a8d Merge branch 'release/2.0.2' 2023-05-12 00:45:55 +02:00
9697a39464 Merge branch 'release/2.0.1' 2023-05-12 00:21:28 +02:00
18dbbba328 Merge branch 'release/2.0.0' 2023-05-07 23:40:25 +02:00
c4672b8de9 Merge branch 'hotfix/1.9.2' 2023-03-08 21:28:57 +01:00
132 changed files with 9540 additions and 378 deletions

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 150
versionName "2.5.1"
versionCode 164
versionName "2.6.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -160,8 +160,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.50d
updatePriority = 1
userFraction = 0.99d
updatePriority = 4
enabled.set(false)
}
@ -191,23 +191,23 @@ ext {
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.5.1'
implementation 'io.github.wulkanowy:sdk:2.6.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.8.2"
implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.6.2"
implementation "androidx.fragment:fragment-ktx:1.7.0"
implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
implementation "androidx.viewpager2:viewpager2:1.1.0-rc01"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
@ -233,7 +233,7 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
@ -246,15 +246,15 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0'
implementation 'org.apache.commons:commons-text:1.12.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config'
playImplementation 'com.google.android.gms:play-services-ads:23.0.0'
playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
playImplementation "com.google.android.play:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
@ -274,7 +274,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation 'org.robolectric:robolectric:4.12.1'
testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0"

View File

@ -36,6 +36,37 @@
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1",
"android_client_info": {
"package_name": "io.github.wulkanowy"
}
},
"oauth_client": [
{
"client_id": "",
"client_type": 3
}
],
"api_key": [
{
"current_key": ""
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity">
tools:ignore="DiscouragedApi,LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@ -1,11 +1,15 @@
package io.github.wulkanowy.data
import com.chuckerteam.chucker.api.ChuckerInterceptor
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -14,9 +18,13 @@ import javax.inject.Singleton
class WulkanowySdkFactory @Inject constructor(
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
) {
private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
@ -30,7 +38,12 @@ class WulkanowySdkFactory @Inject constructor(
fun create() = sdk
fun create(student: Student, semester: Semester? = null): Sdk {
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
return create().apply {
email = student.email
password = student.password
@ -39,6 +52,7 @@ class WulkanowySdkFactory @Inject constructor(
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
isEduOne = isStudentEduOne
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
@ -61,4 +75,51 @@ class WulkanowySdkFactory @Inject constructor(
}
}
}
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
if (student.isEduOne != null) return student.isEduOne
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
eduOneMutex.withLock {
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.i("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.i("Migration eduOne: flag missing. Running migration...")
val initializedSdk = buildSdk(
student = student,
semester = null,
isStudentEduOne = false, // doesn't matter
)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.i("Migration eduOne: failed, so skipping")
migrationFailedStudentIds.add(student.id)
return false
}
Timber.i("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
val studentIsEduOne = StudentIsEduOne(
id = student.id,
isEduOne = newCurrentStudent.isEduOne
)
studentDb.update(studentIsEduOne)
return newCurrentStudent.isEduOne
}
}
}

View File

@ -120,6 +120,7 @@ import io.github.wulkanowy.data.db.migrations.Migration55
import io.github.wulkanowy.data.db.migrations.Migration57
import io.github.wulkanowy.data.db.migrations.Migration58
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration63
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
@ -174,6 +175,9 @@ import javax.inject.Singleton
AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
AutoMigration(from = 62, to = 63, spec = Migration63::class),
AutoMigration(from = 63, to = 64),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -182,7 +186,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 61
const val VERSION_SCHEMA = 64
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -309,6 +313,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao
}

View File

@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface MobileDeviceDao : BaseDao<MobileDevice> {
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC")
fun loadAll(userLoginId: Int): Flow<List<MobileDevice>>
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :studentId ORDER BY date DESC")
fun loadAll(studentId: Int): Flow<List<MobileDevice>>
}

View File

@ -10,6 +10,6 @@ import javax.inject.Singleton
@Singleton
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
fun loadAll(userLoginId: Int): Flow<List<SchoolAnnouncement>>
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :studentId ORDER BY date DESC")
fun loadAll(studentId: Int): Flow<List<SchoolAnnouncement>>
}

View File

@ -14,6 +14,6 @@ interface SemesterDao : BaseDao<Semester> {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertSemesters(items: List<Semester>): List<Long>
@Query("SELECT * FROM Semesters WHERE student_id = :studentId AND class_id = :classId")
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId) OR (student_id = :studentId AND class_id = 0)")
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
}

View File

@ -9,6 +9,8 @@ import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -23,6 +25,12 @@ abstract class StudentDao {
@Delete
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@ -39,11 +47,11 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0)")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0) WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")

View File

@ -4,6 +4,8 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@ -34,6 +36,8 @@ data class AdminMessage(
val priority: String,
@SerialName("messageTypes")
@Serializable(with = SafeMessageTypeEnumListSerializer::class)
@ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),

View File

@ -33,7 +33,13 @@ data class GradeSummary(
@ColumnInfo(name = "points_sum")
val pointsSum: String,
val average: Double
@ColumnInfo(name = "points_sum_all_year")
val pointsSumAllYear: String?,
val average: Double,
@ColumnInfo(name = "average_all_year")
val averageAllYear: Double? = null,
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0

View File

@ -9,8 +9,8 @@ import java.time.Instant
@Entity(tableName = "MobileDevices")
data class MobileDevice(
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ColumnInfo(name = "user_login_id") // todo: change column name
val studentId: Int,
@ColumnInfo(name = "device_id")
val deviceId: Int,

View File

@ -9,8 +9,8 @@ import java.time.LocalDate
@Entity(tableName = "SchoolAnnouncements")
data class SchoolAnnouncement(
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ColumnInfo(name = "user_login_id") // todo: change column name
val studentId: Int,
val date: LocalDate,

View File

@ -49,6 +49,7 @@ data class Student(
@ColumnInfo(name = "student_id")
val studentId: Int,
@Deprecated("not available in VULCAN anymore")
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ -78,6 +79,13 @@ data class Student(
@ColumnInfo(name = "registration_date")
val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable {
@PrimaryKey(autoGenerate = true)
@ -88,3 +96,22 @@ data class Student(
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}
@Entity
data class StudentIsAuthorized(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
val isAuthorized: Boolean?,
) : Serializable
@Entity
data class StudentIsEduOne(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration63 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE Students SET is_edu_one = NULL WHERE is_edu_one = 0")
}
}

View File

@ -4,6 +4,8 @@ enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
LOGIN_STUDENT_SELECT_MESSAGE,
LOGIN_SYMBOL_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.enums
enum class ShowAdditionalLessonsMode(val value: String) {
NONE("none"),
INLINE("inline"),
BELOW("below");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: INLINE
}
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
@JvmName("mapDirectorInformationToEntities")
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
userLoginId = student.userLoginId,
studentId = student.studentId,
date = it.date,
subject = it.subject,
content = it.content,
@ -19,7 +19,7 @@ fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
@JvmName("mapLastAnnouncementsToEntities")
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
userLoginId = student.userLoginId,
studentId = student.studentId,
date = it.date,
subject = it.subject,
content = it.content,

View File

@ -37,9 +37,11 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
pointsSumAllYear = it.pointsSumAllYear,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average
average = it.average,
averageAllYear = it.averageAllYear,
)
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
fun List<SdkDevice>.mapToEntities(student: Student) = map {
MobileDevice(
userLoginId = student.userLoginId,
studentId = student.studentId,
date = it.createDate.toInstant(),
deviceId = it.id,
name = it.name

View File

@ -34,17 +34,19 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
.map { registerStudent ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapToEntities(registerSubject.studentId),
studentId = registerStudent.studentId,
studentName = registerStudent.studentName,
studentSecondName = registerStudent.studentSecondName,
studentSurname = registerStudent.studentSurname,
className = registerStudent.className,
classId = registerStudent.classId,
isParent = registerStudent.isParent,
isAuthorized = registerStudent.isAuthorized,
isEduOne = registerStudent.isEduOne,
semesters = registerStudent.semesters
.mapToEntities(registerStudent.studentId),
)
},
)
@ -84,6 +86,8 @@ fun RegisterStudent.mapToStudentWithSemesters(
password = user.password.orEmpty(),
isCurrent = false,
registrationDate = Instant.now(),
isAuthorized = this.isAuthorized,
isEduOne = this.isEduOne,
).apply {
avatarColor = colors.random()
},

View File

@ -45,4 +45,6 @@ data class RegisterStudent(
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
val isAuthorized: Boolean,
val isEduOne: Boolean
) : java.io.Serializable

View File

@ -6,6 +6,8 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
import io.github.wulkanowy.utils.AppWidgetUpdater
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
@ -18,6 +20,7 @@ import javax.inject.Singleton
class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@ -26,6 +29,7 @@ class LuckyNumberRepository @Inject constructor(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
isFromAppWidget: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it == null },
@ -44,6 +48,9 @@ class LuckyNumberRepository @Inject constructor(
oldItems = listOfNotNull(oldLuckyNumber),
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
)
if (!isFromAppWidget) {
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
}
}
}
)

View File

@ -38,7 +38,7 @@ class MobileDeviceRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
query = { mobileDb.loadAll(student.userLoginId) },
query = { mobileDb.loadAll(student.studentId) },
fetch = {
wulkanowySdkFactory.create(student, semester)
.getRegisteredDevices()

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
import io.github.wulkanowy.data.enums.TimetableGapsMode
import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
@ -213,6 +214,12 @@ class PreferencesRepository @Inject constructor(
)
)
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
get() = getString(
R.string.pref_key_timetable_show_additional_lessons,
R.string.pref_default_timetable_show_additional_lessons
).let { ShowAdditionalLessonsMode.getByValue(it) }
val gradeSortingMode: GradeSortingMode
get() = GradeSortingMode.getByValue(
getString(

View File

@ -37,7 +37,7 @@ class SchoolAnnouncementRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired
},
query = {
schoolAnnouncementDb.loadAll(student.userLoginId)
schoolAnnouncementDb.loadAll(student.studentId)
},
fetch = {
val sdk = wulkanowySdkFactory.create(student)
@ -57,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.userLoginId)
return schoolAnnouncementDb.loadAll(student.studentId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =

View File

@ -64,7 +64,10 @@ class SemesterRepository @Inject constructor(
.getSemesters()
.mapToEntities(student.studentId)
if (new.isEmpty()) return Timber.i("Empty semester list!")
if (new.isEmpty()) {
Timber.i("Empty semester list from SDK!")
return
}
val old = semesterDb.loadAll(student.studentId, student.classId)
semesterDb.removeOldAndSaveNew(

View File

@ -7,16 +7,19 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
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.DispatchersProvider
import io.github.wulkanowy.utils.security.Scrambler
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -39,6 +42,7 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
.also { it.logErrors() }
suspend fun getUserSubjectsFromScrapper(
email: String,
@ -49,6 +53,7 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getStudentsHybrid(
email: String,
@ -58,6 +63,7 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
@ -99,6 +105,46 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun updateCurrentStudentAuthStatus() {
Timber.i("Check isAuthorized: started")
val student = getCurrentStudent()
if (student.isAuthorized) {
Timber.i("Check isAuthorized: already authorized")
return
}
val initializedSdk = wulkanowySdkFactory.create(student)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Check isAuthorized: current user is null")
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
)
}
if (!newCurrentStudent.isAuthorized) {
Timber.i("Check isAuthorized: authorization required")
throw NoAuthorizationException()
}
val studentIsAuthorized = StudentIsAuthorized(
id = student.id,
isAuthorized = true
)
Timber.i("Check isAuthorized: already authorized, update local status")
studentDb.update(studentIsAuthorized)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
@ -151,15 +197,21 @@ class StudentRepository @Inject constructor(
wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
.getCurrentStudent() ?: return
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
@ -172,4 +224,18 @@ class StudentRepository @Inject constructor(
appDatabase.clearAllTables()
}
}
private fun RegisterUser.logErrors() {
val symbolsErrors = symbols.filter { it.error != null }
.map { it.error }
val unitsErrors = symbols.flatMap { it.schools }
.filter { it.error != null }
.map { it.error }
(symbolsErrors + unitsErrors).forEach { error ->
Timber.e(error, "Error occurred while fetching students")
}
}
}
class NoAuthorizationException : Exception()

View File

@ -13,6 +13,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
import io.github.wulkanowy.utils.AppWidgetUpdater
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
@ -26,6 +28,7 @@ import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao,
@ -34,6 +37,7 @@ class TimetableRepository @Inject constructor(
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper,
private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@ -52,7 +56,8 @@ class TimetableRepository @Inject constructor(
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
notify: Boolean = false,
timetableType: TimetableType = TimetableType.NORMAL
timetableType: TimetableType = TimetableType.NORMAL,
isFromAppWidget: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = {
@ -83,6 +88,9 @@ class TimetableRepository @Inject constructor(
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
if (!isFromAppWidget) {
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
}
},
filterResult = { (timetable, additional, headers) ->
TimetableFull(

View File

@ -0,0 +1,27 @@
package io.github.wulkanowy.data.serializers
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@OptIn(ExperimentalSerializationApi::class)
object SafeMessageTypeEnumListSerializer : KSerializer<List<MessageType>> {
private val serializer = ListSerializer(String.serializer())
override val descriptor = serializer.descriptor
override fun serialize(encoder: Encoder, value: List<MessageType>) {
encoder.encodeNotNullMark()
serializer.serialize(encoder, value.map { it.name })
}
override fun deserialize(decoder: Decoder): List<MessageType> =
serializer.deserialize(decoder).mapNotNull { enumName ->
MessageType.entries.find { it.name == enumName }
}
}

View File

@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
}
}
}

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
@ -48,6 +49,7 @@ class SyncWorker @AssistedInject constructor(
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
Timber.e(e)
return@withContext getResultFromErrors(listOf(e))
}
@ -59,7 +61,7 @@ class SyncWorker @AssistedInject constructor(
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
null
} else {
Timber.e(e)

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.data.repositories.NoAuthorizationException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -40,7 +40,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
is NoAuthorizationException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
@ -9,7 +12,9 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@ -33,6 +38,12 @@ class AttendanceCalculatorFragment :
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceCalculatorBinding.bind(view)
@ -40,6 +51,19 @@ class AttendanceCalculatorFragment :
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_attendance_calculator, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected()
else false
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target)))
}
override fun initView() {
with(binding.attendanceCalculatorRecycler) {
layoutManager = LinearLayoutManager(context)
@ -50,7 +74,11 @@ class AttendanceCalculatorFragment :
with(binding) {
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -1,6 +1,11 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
@ -81,4 +86,9 @@ class AttendanceCalculatorPresenter @Inject constructor(
} else showError(message, error)
}
}
fun onSettingsSelected(): Boolean {
view?.openSettingsView()
return true
}
}

View File

@ -26,4 +26,6 @@ interface AttendanceCalculatorView : BaseView {
fun updateData(data: List<AttendanceData>)
fun clearView()
fun openSettingsView()
}

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AuthPresenter @Inject constructor(
@ -26,8 +27,12 @@ class AuthPresenter @Inject constructor(
private fun loadName() {
presenterScope.launch {
runCatching { studentRepository.getCurrentStudent(false) }
.onSuccess { view?.showDescriptionWithName(it.studentName) }
runCatching {
studentRepository.getCurrentStudent(false)
.studentName
.replace(" ", "\u00A0")
}
.onSuccess { view?.showDescriptionWithName(it) }
.onFailure { errorHandler.dispatch(it) }
}
}
@ -57,8 +62,9 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) {
studentRepository.refreshStudentName(student, semester)
studentRepository.refreshStudentAfterAuthorize(student, semester)
}
isSuccess
}
@ -68,6 +74,7 @@ class AuthPresenter @Inject constructor(
view?.showContent(true)
}
.onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) {
view?.showSuccess(true)
view?.showContent(false)

View File

@ -26,5 +26,7 @@ private fun generateSummary(subject: String, predicted: String, final: String) =
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = .0
average = .0,
pointsSumAllYear = null,
averageAllYear = null,
)

View File

@ -19,6 +19,6 @@ val debugSchoolAnnouncementItems = listOf(
private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement(
subject = subject,
content = content,
userLoginId = 0,
studentId = 0,
date = LocalDate.now()
)

View File

@ -266,7 +266,9 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = .0
pointsSumAllYear = null,
average = .0,
averageAllYear = null,
)
}
@ -294,13 +296,15 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
pointsSumAllYear = null,
average = when {
calcAverage -> details
.updateModifiers(student, params)
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
else -> .0
}
},
averageAllYear = null,
)
}
}

View File

@ -7,7 +7,6 @@ import android.view.MenuItem
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
@ -31,14 +30,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
@Inject
lateinit var presenter: GradePresenter
private val pagerAdapter by lazy {
BaseFragmentPagerAdapter(
fragmentManager = childFragmentManager,
pagesCount = 3,
lifecycle = lifecycle,
)
}
private var semesterSwitchMenu: MenuItem? = null
companion object {
@ -52,6 +43,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
private var pagerAdapter: BaseFragmentPagerAdapter? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -71,13 +64,26 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun initView() {
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun initTabs(pageCount: Int) {
pagerAdapter = BaseFragmentPagerAdapter(
lifecycle = lifecycle,
pagesCount = pageCount,
fragmentManager = childFragmentManager
)
with(binding.gradeViewPager) {
adapter = pagerAdapter
offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected)
}
with(pagerAdapter) {
with(pagerAdapter!!) {
containerId = binding.gradeViewPager.id
titleFactory = {
when (it) {
@ -99,11 +105,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -169,19 +170,20 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
}
override fun notifyChildParentReselected(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
}
override fun notifyChildSemesterChange(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onDestroyView() {
pagerAdapter = null
presenter.onDetachView()
super.onDestroyView()
}

View File

@ -22,11 +22,8 @@ class GradePresenter @Inject constructor(
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
private var selectedIndex = 0
private var schoolYear = 0
private var semesters = emptyList<Semester>()
private var availableSemesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>()
private lateinit var lastError: Throwable
@ -40,7 +37,7 @@ class GradePresenter @Inject constructor(
}
fun onCreateMenu() {
if (semesters.isEmpty()) view?.showSemesterSwitch(false)
if (availableSemesters.isEmpty()) view?.showSemesterSwitch(false)
}
fun onViewReselected() {
@ -49,8 +46,8 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
if (availableSemesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, availableSemesters.take(2))
}
return true
}
@ -83,7 +80,7 @@ class GradePresenter @Inject constructor(
}
fun onPageSelected(index: Int) {
if (semesters.isNotEmpty()) loadChild(index)
if (availableSemesters.isNotEmpty()) loadChild(index)
}
fun onRetry() {
@ -101,16 +98,24 @@ class GradePresenter @Inject constructor(
private fun loadData() {
resourceFlow {
val student = studentRepository.getCurrentStudent()
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
val semesters = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
student to semesters
}
.logResourceStatus("load grade data")
.onResourceData {
val current = it.getCurrentOrLast()
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
schoolYear = current.schoolYear
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
view?.setCurrentSemesterName(current.semesterName, schoolYear)
.onResourceData { (student, semesters) ->
val currentSemester = semesters.getCurrentOrLast()
selectedIndex =
if (selectedIndex == 0) currentSemester.semesterName else selectedIndex
schoolYear = currentSemester.schoolYear
availableSemesters = semesters.filter { semester ->
semester.diaryId == currentSemester.diaryId
}
view?.run {
initTabs(if (student.isEduOne == true) 2 else 3)
setCurrentSemesterName(currentSemester.semesterName, schoolYear)
Timber.i("Loading grade data: Attempt load index $currentPageIndex")
loadChild(currentPageIndex)
showErrorView(false)
@ -131,10 +136,10 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${availableSemesters.joinToString { it.semesterName.toString() }}")
val newSelectedSemesterId = try {
semesters.first { it.semesterName == selectedIndex }.semesterId
availableSemesters.first { it.semesterName == selectedIndex }.semesterId
} catch (e: NoSuchElementException) {
Timber.e(e, "Selected semester no exists")
return

View File

@ -9,6 +9,8 @@ interface GradeView : BaseView {
fun initView()
fun initTabs(pageCount: Int)
fun showContent(show: Boolean)
fun showProgress(show: Boolean)

View File

@ -96,9 +96,11 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
ViewType.HEADER.id -> HeaderViewHolder(
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
)
ViewType.ITEM.id -> ItemViewHolder(
ItemGradeDetailsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
@ -110,6 +112,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
header = items[position].value as GradeDetailsHeader,
position = position
)
is ItemViewHolder -> bindItemViewHolder(
holder = holder,
grade = items[position].value as Grade
@ -133,6 +136,10 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
maxLines = if (expandedPositions[headerPosition]) 2 else 1
}
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
with(gradeHeaderAverageAllYear) {
isVisible = header.averageAllYear != null && header.averageAllYear != .0
text = formatAverageAllYear(header.averageAllYear, root.context.resources)
}
gradeHeaderPointsSum.text =
context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
@ -233,6 +240,13 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
resources.getString(R.string.grade_average, average)
}
private fun formatAverageAllYear(average: Double?, resources: Resources) =
if (average == null || average == .0) {
resources.getString(R.string.grade_no_average)
} else {
resources.getString(R.string.grade_average_year, average)
}
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)

View File

@ -13,6 +13,7 @@ data class GradeDetailsItem(
data class GradeDetailsHeader(
val subject: String,
val average: Double?,
val averageAllYear: Double?,
val pointsSum: String?,
val grades: List<GradeDetailsItem>
) {

View File

@ -226,8 +226,9 @@ class GradeDetailsPresenter @Inject constructor(
GradeDetailsHeader(
subject = gradeSubject.subject,
average = gradeSubject.average,
averageAllYear = gradeSubject.summary.averageAllYear,
pointsSum = gradeSubject.points,
grades = subItems
grades = subItems,
).apply {
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
}, ViewType.HEADER

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.grade.summary
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
@ -65,37 +66,55 @@ class GradeSummaryAdapter @Inject constructor(
val gradeSummaries = items
.filter { it.gradeDescriptive == null }
.map { it.gradeSummary }
val isSecondSemester = items.any { item ->
item.gradeSummary.let { it.averageAllYear != null && it.averageAllYear != .0 }
}
val context = binding.root.context
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val calculatedSemesterItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val calculatedAnnualItemsCount =
gradeSummaries.count { value -> value.averageAllYear != 0.0 }
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
val finalAverage = gradeSummaries.calcFinalAverage(
preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier
plusModifier = preferencesRepository.gradePlusModifier,
minusModifier = preferencesRepository.gradeMinusModifier,
)
val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
val calculatedSemesterAverage = gradeSummaries.filter { value -> value.average != 0.0 }
.map { values -> values.average }
.reversed() // fix average precision
.average()
.let { if (it.isNaN()) 0.0 else it }
val calculatedAnnualAverage = gradeSummaries.filter { value -> value.averageAllYear != 0.0 }
.mapNotNull { values -> values.averageAllYear }
.reversed() // fix average precision
.average()
.let { if (it.isNaN()) 0.0 else it }
with(binding) {
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedSemesterAverage)
gradeSummaryScrollableHeaderCalculatedAnnual.text =
formatAverage(calculatedAnnualAverage)
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage)
gradeSummaryScrollableHeaderFinalSubjectCount.text =
context.getString(
R.string.grade_summary_from_subjects,
finalItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
gradeSummaryScrollableHeaderFinalSubjectCount.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedItemsCount,
finalItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedSemesterItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCountAnnual.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedAnnualItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedAnnualContainer.isVisible = isSecondSemester
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryCalculatedAverageHelpAnnual.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
}
}
@ -107,7 +126,12 @@ class GradeSummaryAdapter @Inject constructor(
with(binding) {
gradeSummaryItemTitle.text = gradeSummary.subject
gradeSummaryItemPoints.text = gradeSummary.pointsSum
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
gradeSummaryItemAverageAllYear.text = gradeSummary.averageAllYear?.let {
formatAverage(it, "")
}
gradeSummaryItemPredicted.text =
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
gradeSummaryItemFinal.text =
@ -116,6 +140,12 @@ class GradeSummaryAdapter @Inject constructor(
root.context.getString(R.string.all_no_data)
}
gradeSummaryItemAverageContainer.isVisible = gradeSummary.average != .0
gradeSummaryItemAverageDivider.isVisible = gradeSummaryItemAverageContainer.isVisible
gradeSummaryItemAverageAllYearContainer.isGone =
gradeSummary.averageAllYear == null || gradeSummary.averageAllYear == .0
gradeSummaryItemAverageAllYearDivider.isGone =
gradeSummaryItemAverageAllYearContainer.isGone
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
@ -123,6 +153,7 @@ class GradeSummaryAdapter @Inject constructor(
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
gradeSummaryItemPointsDivider.isVisible = gradeSummaryItemPointsContainer.isVisible
}
}

View File

@ -98,7 +98,9 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear,
onDateSelected = {
date = it
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
if (isAdded) {
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
}
}
)
}

View File

@ -19,19 +19,23 @@ class LoginStudentSelectAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (LoginStudentSelectItemType.values()[viewType]) {
return when (LoginStudentSelectItemType.entries[viewType]) {
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
)
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.HELP -> HelpViewHolder(
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
)
@ -98,9 +102,11 @@ class LoginStudentSelectAdapter @Inject constructor() :
with(binding) {
loginStudentSelectHeaderSchoolName.text = buildString {
append(item.unit.schoolName.trim())
append(" (")
append(item.unit.schoolShortName)
append(")")
if (item.unit.schoolShortName.isNotBlank()) {
append(" (")
append(item.unit.schoolShortName)
append(")")
}
}
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
@ -170,9 +176,11 @@ class LoginStudentSelectAdapter @Inject constructor() :
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
oldItem.symbol == newItem.symbol
}
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
oldItem.student == newItem.student
}
else -> oldItem == newItem
}

View File

@ -6,10 +6,12 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
@ -111,6 +113,19 @@ class LoginStudentSelectFragment :
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
override fun showAdminMessage(adminMessage: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginStudentSelectAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(adminMessage)
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -2,16 +2,24 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
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.PreferencesRepository
import io.github.wulkanowy.data.repositories.SchoolsRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@ -32,6 +40,8 @@ class LoginStudentSelectPresenter @Inject constructor(
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val appInfo: AppInfo,
private val preferencesRepository: PreferencesRepository,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
@ -64,6 +74,7 @@ class LoginStudentSelectPresenter @Inject constructor(
this.loginData = loginData
this.registerUser = registerUser
loadData()
loadAdminMessage()
}
private fun loadData() {
@ -87,7 +98,20 @@ class LoginStudentSelectPresenter @Inject constructor(
refreshItems()
}
}
}.launch()
}.launch("load_data")
}
private fun loadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(),
type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch("load_admin_message")
}
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
@ -108,8 +132,8 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
val notEmptySymbols = registerUser.symbols.filter { it.shouldShowOnTop() }
val emptySymbols = registerUser.symbols.filter { !it.shouldShowOnTop() }
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) {
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol }))
@ -127,6 +151,10 @@ class LoginStudentSelectPresenter @Inject constructor(
add(helpItem)
}
private fun RegisterSymbol.shouldShowOnTop(): Boolean {
return schools.isNotEmpty() || error is StudentGraduateException
}
private fun createNotEmptySymbolItems(
notEmptySymbols: List<RegisterSymbol>,
students: List<StudentWithSemesters>,
@ -336,4 +364,14 @@ class LoginStudentSelectPresenter @Inject constructor(
)
}
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
}
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
@ -25,4 +26,8 @@ interface LoginStudentSelectView : BaseView {
fun openDiscordInvite()
fun openEmail(supportInfo: LoginSupportInfo)
fun showAdminMessage(adminMessage: AdminMessage?)
fun openInternetBrowser(url: String)
}

View File

@ -9,13 +9,16 @@ import android.view.inputmethod.EditorInfo.IME_NULL
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
@ -179,4 +182,17 @@ class LoginSymbolFragment :
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
override fun showAdminMessage(adminMessage: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginSymbolAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(adminMessage)
binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
}

View File

@ -2,10 +2,18 @@ package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter
@ -21,7 +29,9 @@ import javax.inject.Inject
class LoginSymbolPresenter @Inject constructor(
studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler,
private val analytics: AnalyticsHelper
private val analytics: AnalyticsHelper,
private val preferencesRepository: PreferencesRepository,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
@ -43,6 +53,21 @@ class LoginSymbolPresenter @Inject constructor(
clearAndFocusSymbol()
showSoftKeyboard()
}
loadAdminMessage()
}
private fun loadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = loginData.baseUrl,
type = MessageType.LOGIN_SYMBOL_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch("load_admin_message")
}
fun onSymbolTextChanged() {
@ -166,4 +191,14 @@ class LoginSymbolPresenter @Inject constructor(
)
)
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
}
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
@ -44,4 +45,8 @@ interface LoginSymbolView : BaseView {
fun openFaqPage()
fun openSupportDialog(supportInfo: LoginSupportInfo)
fun showAdminMessage(adminMessage: AdminMessage?)
fun openInternetBrowser(url: String)
}

View File

@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() :
}
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
}
}

View File

@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
}
if (currentStudent != null) {
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
luckyNumberRepository.getLuckyNumber(
student = currentStudent,
forceRefresh = false,
isFromAppWidget = true
)
.toFirstResult()
.dataOrThrow
} else null

View File

@ -73,6 +73,7 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker()
checkAppSupport()
updateCurrentStudentAuthStatus()
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
@ -191,4 +192,11 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent)
}
private fun updateCurrentStudentAuthStatus() {
presenterScope.launch {
runCatching { studentRepository.updateCurrentStudentAuthStatus() }
.onFailure { errorHandler.dispatch(it) }
}
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings.appearance
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import com.yariksoffice.lingver.Lingver
@ -30,9 +31,18 @@ class AppearanceFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.pref_settings_appearance_title
companion object {
fun withFocusedPreference(key: String) = AppearanceFragment().apply {
arguments = bundleOf(FOCUSED_KEY to key)
}
private const val FOCUSED_KEY = "focusedKey"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) }
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View File

@ -7,27 +7,28 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.ItemTimetableBinding
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
import io.github.wulkanowy.databinding.ItemTimetableMainAdditionalBinding
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
import io.github.wulkanowy.utils.SyncListAdapter
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class TimetableAdapter @Inject constructor() :
ListAdapter<TimetableItem, RecyclerView.ViewHolder>(differ) {
SyncListAdapter<TimetableItem, RecyclerView.ViewHolder>(Differ) {
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (TimetableItemType.values()[viewType]) {
return when (TimetableItemType.entries[viewType]) {
TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false)
)
@ -39,6 +40,10 @@ class TimetableAdapter @Inject constructor() :
TimetableItemType.EMPTY -> EmptyViewHolder(
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
)
TimetableItemType.ADDITIONAL -> AdditionalViewHolder(
ItemTimetableMainAdditionalBinding.inflate(inflater, parent, false)
)
}
}
@ -61,16 +66,30 @@ class TimetableAdapter @Inject constructor() :
binding = holder.binding,
item = getItem(position) as TimetableItem.Small,
)
is NormalViewHolder -> bindNormalView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Normal,
)
is EmptyViewHolder -> bindEmptyView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Empty,
)
is AdditionalViewHolder -> bindAdditionalView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Additional,
)
}
}
private fun bindAdditionalView(
binding: ItemTimetableMainAdditionalBinding,
item: TimetableItem.Additional
) {
with(binding) {
timetableItemSubject.text = item.additional.subject
timetableItemTimeStart.text = item.additional.start.toFormattedString("HH:mm")
timetableItemTimeFinish.text = item.additional.end.toFormattedString("HH:mm")
}
}
@ -79,6 +98,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableSmallItemNumber.text = lesson.number.toString()
timetableSmallItemNumber.isVisible = item.isLessonNumberVisible
timetableSmallItemSubject.text = lesson.subject
timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm")
timetableSmallItemRoom.text = lesson.room
@ -97,6 +117,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableItemNumber.text = lesson.number.toString()
timetableItemNumber.isVisible = item.isLessonNumberVisible
timetableItemSubject.text = lesson.subject
timetableItemGroup.text = lesson.group
timetableItemRoom.text = lesson.room
@ -305,31 +326,32 @@ class TimetableAdapter @Inject constructor() :
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
RecyclerView.ViewHolder(binding.root)
companion object {
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
when {
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start
}
private class AdditionalViewHolder(val binding: ItemTimetableMainAdditionalBinding) :
RecyclerView.ViewHolder(binding.root)
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start
}
else -> oldItem == newItem
private object Differ : DiffUtil.ItemCallback<TimetableItem>() {
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
when {
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start
}
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
oldItem == newItem
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start
}
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
"time_left"
} else super.getChangePayload(oldItem, newItem)
} else super.getChangePayload(oldItem, newItem)
else -> oldItem == newItem
}
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
oldItem == newItem
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
"time_left"
} else super.getChangePayload(oldItem, newItem)
} else super.getChangePayload(oldItem, newItem)
}
}
}

View File

@ -21,7 +21,11 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.*
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 java.time.LocalDate
import javax.inject.Inject
@ -104,8 +108,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
}
}
override fun updateData(data: List<TimetableItem>) {
timetableAdapter.submitList(data)
override fun updateData(data: List<TimetableItem>, isDayChanged: Boolean) {
when {
isDayChanged -> timetableAdapter.recreate(data)
else -> timetableAdapter.submitList(data)
}
}
override fun clearData() {

View File

@ -1,18 +1,21 @@
package io.github.wulkanowy.ui.modules.timetable
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import java.time.Duration
sealed class TimetableItem(val type: TimetableItemType) {
data class Small(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.SMALL)
data class Normal(
val lesson: Timetable,
val showGroupsInPlan: Boolean,
val isLessonNumberVisible: Boolean,
val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL)
@ -21,6 +24,10 @@ sealed class TimetableItem(val type: TimetableItemType) {
val numFrom: Int,
val numTo: Int
) : TimetableItem(TimetableItemType.EMPTY)
data class Additional(
val additional: TimetableAdditional,
) : TimetableItem(TimetableItemType.ADDITIONAL)
}
data class TimeLeft(
@ -32,5 +39,6 @@ data class TimeLeft(
enum class TimetableItemType {
SMALL,
NORMAL,
EMPTY
EMPTY,
ADDITIONAL,
}

View File

@ -4,6 +4,9 @@ import android.os.Handler
import android.os.Looper
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.BELOW
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.NONE
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
import io.github.wulkanowy.data.enums.TimetableMode
@ -14,6 +17,7 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -57,6 +61,7 @@ class TimetablePresenter @Inject constructor(
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
private var isEduOne: Boolean = false
var currentDate: LocalDate? = null
private set
@ -80,7 +85,7 @@ class TimetablePresenter @Inject constructor(
} else currentDate?.previousSchoolDay
reloadView(date ?: return)
loadData()
loadData(isDayChanged = true)
}
fun onNextDay() {
@ -89,7 +94,7 @@ class TimetablePresenter @Inject constructor(
} else currentDate?.nextSchoolDay
reloadView(date ?: return)
loadData()
loadData(isDayChanged = true)
}
fun onPickDate() {
@ -103,7 +108,7 @@ class TimetablePresenter @Inject constructor(
fun onSwipeRefresh() {
Timber.i("Force refreshing the timetable")
loadData(true)
loadData(forceRefresh = true)
}
fun onRetry() {
@ -111,7 +116,7 @@ class TimetablePresenter @Inject constructor(
showErrorView(false)
showProgress(true)
}
loadData(true)
loadData(forceRefresh = true)
}
fun onDetailsClick() {
@ -144,11 +149,12 @@ class TimetablePresenter @Inject constructor(
return true
}
private fun loadData(forceRefresh: Boolean = false) {
private fun loadData(forceRefresh: Boolean = false, isDayChanged: Boolean = false) {
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
isEduOne = student.isEduOne == true
checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable(
student = student,
@ -167,9 +173,9 @@ class TimetablePresenter @Inject constructor(
enableSwipe(true)
showProgress(false)
showErrorView(false)
showContent(it.lessons.isNotEmpty())
showEmpty(it.lessons.isEmpty())
updateData(it.lessons)
updateData(it, isDayChanged)
showContent(it.lessons.isNotEmpty() || it.additional.isNotEmpty())
showEmpty(it.lessons.isEmpty() && it.additional.isEmpty())
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
reloadNavigation()
}
@ -214,66 +220,97 @@ class TimetablePresenter @Inject constructor(
}
}
private fun updateData(lessons: List<Timetable>) {
private fun updateData(lessons: TimetableFull, isDayChanged: Boolean) {
tickTimer?.cancel()
if (currentDate != now()) {
view?.updateData(createItems(lessons))
} else {
tickTimer = timer(period = 2_000) {
view?.updateData(createItems(lessons), isDayChanged)
if (currentDate == now()) {
tickTimer = timer(period = 2_000, initialDelay = 2_000) {
Handler(Looper.getMainLooper()).post {
view?.updateData(createItems(lessons))
view?.updateData(createItems(lessons), isDayChanged)
}
}
}
}
private fun createItems(items: List<Timetable>): List<TimetableItem> {
val filteredItems = items
.filter {
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
)
private sealed class Item(
val isStudentPlan: Boolean,
val start: Instant,
val number: Int?,
) {
class Lesson(val lesson: Timetable) :
Item(lesson.isStudentPlan, lesson.start, lesson.number)
class Additional(val additional: TimetableAdditional) : Item(true, additional.start, null)
}
private fun createItems(fullTimetable: TimetableFull): List<TimetableItem> {
val showAdditionalLessonsInPlan = prefRepository.showAdditionalLessonsInPlan
val allItems =
fullTimetable.lessons.map(Item::Lesson) + fullTimetable.additional.map(Item::Additional)
.takeIf { showAdditionalLessonsInPlan != NONE }.orEmpty()
val filteredItems = allItems.filter {
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
(compareBy<Item> { it is Item.Additional }
.takeIf { showAdditionalLessonsInPlan == BELOW } ?: EmptyComparator())
.thenBy { it.start }
.thenBy { !it.isStudentPlan }
)
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
else -> null
}
var prevIsAdditional = false
return buildList {
filteredItems.forEachIndexed { i, it ->
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
val emptyLesson = TimetableItem.Empty(
numFrom = prevNum!! + 1,
numTo = it.number - 1
)
add(emptyLesson)
if (prefRepository.showTimetableGaps != NO_GAPS) {
if (prevNum != null && it.number != null && it.number > prevNum!! + 1) {
if (!prevIsAdditional) {
// Additional lessons do count as a lesson so don't add empty lessons
// when there is an additional lesson present
val emptyLesson = TimetableItem.Empty(
numFrom = prevNum!! + 1, numTo = it.number - 1
)
add(emptyLesson)
}
}
prevNum = it.number
prevIsAdditional = it is Item.Additional
}
if (it.isStudentPlan) {
val normalLesson = TimetableItem.Normal(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
)
add(smallLesson)
if (it is Item.Lesson) {
if (it.isStudentPlan) {
val normalLesson = TimetableItem.Normal(
lesson = it.lesson,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it.lesson, i),
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it.lesson,
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(smallLesson)
}
} else if (it is Item.Additional) {
// If the user disabled showing additional lessons, they would've been filtered
// out already, so there's no need to check it again.
add(TimetableItem.Additional(it.additional))
}
prevNum = it.number
}
}
}
private fun List<Timetable>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
private fun List<Item>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index))
return TimeLeft(
until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil },
@ -282,11 +319,20 @@ class TimetablePresenter @Inject constructor(
)
}
private fun List<Timetable>.getPreviousLesson(position: Int): Instant? {
return filter { it.isStudentPlan }
.getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size)
private fun List<Item>.getPreviousLesson(position: Int): Instant? {
val lessonAdditionalOffset = filterIndexed { i, item ->
i < position && item is Item.Additional
}.size
val lessonStudentPlanOffset = filterIndexed { i, item ->
i < position && !item.isStudentPlan
}.size
val lessonIndex = position - 1 - lessonAdditionalOffset - lessonStudentPlanOffset
return filterIsInstance<Item.Lesson>()
.filter { it.isStudentPlan }
.getOrNull(lessonIndex)
?.let {
if (!it.canceled && it.isStudentPlan) it.end
if (!it.lesson.canceled && it.isStudentPlan) it.lesson.end
else null
}
}
@ -339,3 +385,7 @@ class TimetablePresenter @Inject constructor(
super.onDetachView()
}
}
private class EmptyComparator<T> : Comparator<T> {
override fun compare(o1: T, o2: T) = 0
}

View File

@ -12,7 +12,7 @@ interface TimetableView : BaseView {
fun initView()
fun updateData(data: List<TimetableItem>)
fun updateData(data: List<TimetableItem>, isDayChanged: Boolean)
fun updateNavigationDay(date: String)

View File

@ -13,7 +13,11 @@ 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.*
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 java.time.LocalDate
import javax.inject.Inject
@ -132,8 +136,12 @@ class AdditionalLessonsFragment :
binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
}
override fun showAddAdditionalLessonDialog() {
(activity as? MainActivity)?.showDialogFragment(AdditionalLessonAddDialog.newInstance())
override fun showAddAdditionalLessonDialog(currentDate: LocalDate) {
(activity as? MainActivity)?.showDialogFragment(
AdditionalLessonAddDialog.newInstance(
currentDate
)
)
}
override fun showDatePickerDialog(selectedDate: LocalDate) {

View File

@ -1,14 +1,27 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import android.annotation.SuppressLint
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
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.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
@ -22,11 +35,14 @@ class AdditionalLessonsPresenter @Inject constructor(
errorHandler: ErrorHandler,
private val semesterRepository: SemesterRepository,
private val timetableRepository: TimetableRepository,
private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase,
private val analytics: AnalyticsHelper
) : BasePresenter<AdditionalLessonsView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate
private set
@ -43,12 +59,18 @@ class AdditionalLessonsPresenter @Inject constructor(
}
fun onPreviousDay() {
loadData(currentDate.previousSchoolDay)
val date = if (isWeekendHasLessons) {
currentDate.minusDays(1)
} else currentDate.previousSchoolDay
loadData(date)
reloadView()
}
fun onNextDay() {
loadData(currentDate.nextSchoolDay)
val date = if (isWeekendHasLessons) {
currentDate.plusDays(1)
} else currentDate.nextSchoolDay
loadData(date)
reloadView()
}
@ -57,7 +79,7 @@ class AdditionalLessonsPresenter @Inject constructor(
}
fun onAdditionalLessonAddButtonClicked() {
view?.showAddAdditionalLessonDialog()
view?.showAddAdditionalLessonDialog(currentDate)
}
fun onDateSet(year: Int, month: Int, day: Int) {
@ -131,6 +153,8 @@ class AdditionalLessonsPresenter @Inject constructor(
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester, currentDate)
timetableRepository.getTimetable(
student = student,
semester = semester,

View File

@ -36,7 +36,7 @@ interface AdditionalLessonsView : BaseView {
fun showDatePickerDialog(selectedDate: LocalDate)
fun showAddAdditionalLessonDialog()
fun showAddAdditionalLessonDialog(currentDate: LocalDate)
fun showSuccessMessage()

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.timepicker.MaterialTimePicker
@ -26,10 +27,12 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
lateinit var presenter: AdditionalLessonAddPresenter
companion object {
fun newInstance() = AdditionalLessonAddDialog()
const val ARGUMENT_KEY = "additional_lesson_default_date"
fun newInstance(defaultDate: LocalDate) = AdditionalLessonAddDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to defaultDate.toEpochDay())
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(
@ -40,10 +43,13 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getLong(ARGUMENT_KEY)?.let(LocalDate::ofEpochDay)?.let {
presenter.onDateSelected(it)
}
presenter.onAttachView(this)
}
override fun initView() {
override fun initView(selectedDate: LocalDate) {
with(binding) {
additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ ->
additionalLessonDialogStart.isErrorEnabled = false
@ -53,6 +59,7 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
additionalLessonDialogEnd.isErrorEnabled = false
additionalLessonDialogEnd.error = null
}
additionalLessonDialogDateEdit.setText(selectedDate.toFormattedString())
additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ ->
additionalLessonDialogDate.isErrorEnabled = false
additionalLessonDialogDate.error = null
@ -61,7 +68,6 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
additionalLessonDialogContent.isErrorEnabled = false
additionalLessonDialogContent.error = null
}
additionalLessonDialogAdd.setOnClickListener {
presenter.onAddAdditionalClicked(
start = additionalLessonDialogStartEdit.text?.toString(),
@ -155,7 +161,9 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
.build()
timePicker.addOnPositiveButtonClickListener {
onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
if (isAdded) {
onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
}
}
if (!parentFragmentManager.isStateSaved) {

View File

@ -10,9 +10,12 @@ import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
import io.github.wulkanowy.utils.toLocalDate
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.*
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.UUID
import javax.inject.Inject
class AdditionalLessonAddPresenter @Inject constructor(
@ -30,7 +33,7 @@ class AdditionalLessonAddPresenter @Inject constructor(
override fun onAttachView(view: AdditionalLessonAddView) {
super.onAttachView(view)
view.initView()
view.initView(selectedDate)
Timber.i("AdditionalLesson details view was initialized")
}

View File

@ -6,7 +6,7 @@ import java.time.LocalTime
interface AdditionalLessonAddView : BaseView {
fun initView()
fun initView(selectedDate: LocalDate)
fun closeDialog()

View File

@ -46,11 +46,8 @@ class TimetableWidgetFactory(
) : RemoteViewsService.RemoteViewsFactory {
private var items = emptyList<TimetableWidgetItem>()
private var timetableCanceledColor: Int? = null
private var textColor: Int? = null
private var timetableChangeColor: Int? = null
override fun getLoadingView() = null
@ -81,7 +78,7 @@ class TimetableWidgetFactory(
val lessons = getLessons(student, semester, date)
val lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
createItems(lessons, lastSync)
createItems(lessons, lastSync, !(student.isEduOne ?: false))
}
.onFailure {
items = listOf(TimetableWidgetItem.Error(it))
@ -104,14 +101,22 @@ class TimetableWidgetFactory(
private suspend fun getLessons(
student: Student, semester: Semester, date: LocalDate
): List<Timetable> {
val timetable = timetableRepository.getTimetable(student, semester, date, date, false)
val timetable = timetableRepository.getTimetable(
student = student,
semester = semester,
start = date,
end = date,
forceRefresh = false,
isFromAppWidget = true
)
val lessons = timetable.toFirstResult().dataOrThrow.lessons
return lessons.sortedBy { it.number }
return lessons.sortedBy { it.start }
}
private fun createItems(
lessons: List<Timetable>,
lastSync: Instant?,
isEduOne: Boolean
): List<TimetableWidgetItem> {
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
@ -127,7 +132,7 @@ class TimetableWidgetFactory(
)
add(emptyItem)
}
add(TimetableWidgetItem.Normal(it))
add(TimetableWidgetItem.Normal(it, isEduOne))
prevNum = it.number
}
add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN))
@ -155,9 +160,11 @@ class TimetableWidgetFactory(
val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE)
val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE)
val lessonNumberVisibility = if (item.isLessonNumberVisible) VISIBLE else GONE
val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString())
setViewVisibility(R.id.timetableWidgetItemNumber, lessonNumberVisibility)
setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime)
setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime)
setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject)

View File

@ -7,6 +7,7 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) {
data class Normal(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL)
data class Empty(

View File

@ -0,0 +1,34 @@
package io.github.wulkanowy.utils
import android.appwidget.AppWidgetManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
import kotlin.reflect.KClass
class AppWidgetUpdater @Inject constructor(
@ApplicationContext private val context: Context,
private val appWidgetManager: AppWidgetManager
) {
fun updateAllAppWidgetsByProvider(providerClass: KClass<out BroadcastReceiver>) {
try {
val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, providerClass.java))
if (ids.isEmpty()) return
val intent = Intent(context, providerClass.java)
.apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
}
context.sendBroadcast(intent)
} catch (e: Exception) {
Timber.e(e, "Failed to update all widgets for provider $providerClass")
}
}
}

View File

@ -23,7 +23,7 @@ fun getRefreshKey(name: String, semester: Semester): String {
}
fun getRefreshKey(name: String, student: Student): String {
return "${name}_${student.userLoginId}"
return "${name}_${student.studentId}"
}
fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String {

View File

@ -18,7 +18,7 @@ fun Semester.isCurrent(now: LocalDate = now()): Boolean {
}
fun List<Semester>.getCurrentOrLast(): Semester {
if (isEmpty()) throw RuntimeException("Empty semester list")
if (isEmpty()) throw IllegalStateException("Empty semester list")
// when there is only one current semester
singleOrNull { it.isCurrent() }?.let { return it }

View File

@ -0,0 +1,66 @@
package io.github.wulkanowy.utils
import android.annotation.SuppressLint
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/**
* Custom alternative to androidx.recyclerview.widget.ListAdapter. ListAdapter is asynchronous which
* caused data race problems in views when a Resource.Error arrived shortly after
* Resource.Intermediate/Success - occasionally in that case the user could see both the Resource's
* data and an error message one on top of the other. This is synchronized by design to avoid that
* problem, however it retains the quality of life improvements of the original.
*/
abstract class SyncListAdapter<T : Any, VH : RecyclerView.ViewHolder> private constructor(
private val updateStrategy: SyncListAdapter<T, VH>.(List<T>) -> Unit
) : RecyclerView.Adapter<VH>() {
constructor(differ: DiffUtil.ItemCallback<T>) : this({ newItems ->
val diffResult = DiffUtil.calculateDiff(toCallback(differ, items, newItems))
items = newItems
diffResult.dispatchUpdatesTo(this)
})
var items = emptyList<T>()
private set
final override fun getItemCount() = items.size
fun getItem(position: Int): T {
return items[position]
}
/**
* Updates all items, same as submitList, however also disables animations temporarily.
* This prevents a flashing effect on some views. Should be used in favor of submitList when
* all data is changed (e.g. the selected day changes in timetable causing all lessons to change).
*/
@SuppressLint("NotifyDataSetChanged")
fun recreate(data: List<T>) {
items = data
notifyDataSetChanged()
}
fun submitList(data: List<T>) {
updateStrategy(data.toList())
}
private fun <T : Any> toCallback(
itemCallback: DiffUtil.ItemCallback<T>,
old: List<T>,
new: List<T>,
) = object : DiffUtil.Callback() {
override fun getOldListSize() = old.size
override fun getNewListSize() = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
itemCallback.areItemsTheSame(old[oldItemPosition], new[newItemPosition])
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
itemCallback.areContentsTheSame(old[oldItemPosition], new[newItemPosition])
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) =
itemCallback.getChangePayload(old[oldItemPosition], new[newItemPosition])
}
}

View File

@ -6,7 +6,7 @@ Zvýrazněné vlastnosti a funkce:
- šťastné číslo,
- náhled na další a dokončené lekce,
- tmavý motiv,
- žádné reklamy,
- volitelné reklamy,
- offline režim,
- upozornění.

View File

@ -6,7 +6,7 @@ Wyróżnione cechy i funkcje:
- szczęśliwy numerek,
- podgląd lekcji dodatkowych i zrealizowanych,
- ciemny motyw.
- brak reklam,
- opcjonalne reklam,
- tryb offline,
- powiadomienia.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

@ -6,7 +6,7 @@ Zvýraznené vlastnosti a funkcie:
- šťastné číslo,
- náhľad na ďalšie a dokončené lekcie,
- tmavý motív,
- žiadne reklamy,
- voliteľné reklamy,
- offline režim,
- upozornenia.

View File

@ -1,11 +1,5 @@
Wersja 2.5.1
Wersja 2.6.4
dodaliśmy wyświetlanie ogłoszeń
— dodaliśmy opcję przywracania wiadomości z kosza
— dodaliśmy opcję wyciszania nadawców wiadomości
— naprawiliśmy opcjonalne liczenie średniej arytmetycznej, kiedy brak ocen z wagą w drugim semestrze
— usprawniliśmy ładowanie frekwencji i planu lekcji
— naprawiliśmy usprawiedliwianie nieobecności i autoryzację u użytkowników eduOne
— zmieniliśmy komunikat o zmienionym haśle
naprawiliśmy dostęp do modułu ucznia i modułu wiadomości po kolejnej próbie blokady nas (niestety jeszcze bez nowego modułu ucznia)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -32,12 +32,11 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/auth_title"
app:lineHeight="24sp"
app:lineHeight="18sp"
tools:text="@string/auth_description" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/auth_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"

View File

@ -11,6 +11,18 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/login_student_select_admin_message"
layout="@layout/item_dashboard_admin_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView
android:id="@+id/loginStudentSelectHeader"
android:layout_width="match_parent"
@ -28,7 +40,7 @@
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_student_select_admin_message"
app:layout_constraintVertical_chainStyle="packed" />
<androidx.recyclerview.widget.RecyclerView

View File

@ -95,6 +95,18 @@
android:background="?android:attr/listDivider" />
</LinearLayout>
<include
android:id="@+id/login_symbol_admin_message"
layout="@layout/item_dashboard_admin_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/loginSymbolContact"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView
android:id="@+id/loginSymbolHeader"
android:layout_width="match_parent"
@ -111,7 +123,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSymbolContact"
app:layout_constraintTop_toBottomOf="@+id/login_symbol_admin_message"
app:layout_constraintVertical_chainStyle="packed" />
<TextView

View File

@ -45,13 +45,30 @@
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/gradeHeaderPointsSum"
app:layout_constraintEnd_toStartOf="@id/gradeHeaderAverageAllYear"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/gradeHeaderSubject"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Average: 6,00" />
<TextView
android:id="@+id/gradeHeaderAverageAllYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/gradeHeaderPointsSum"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverage"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Roczna: 5,00"
tools:visibility="gone" />
<TextView
android:id="@+id/gradeHeaderPointsSum"
android:layout_width="wrap_content"
@ -64,7 +81,7 @@
android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/gradeHeaderNumber"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverage"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverageAllYear"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Points: 123/200 (61,5%)" />

View File

@ -20,20 +20,80 @@
android:id="@+id/gradeSummaryItemTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="40dp"
android:layout_weight="1"
android:textSize="17sp"
tools:text="@tools:sample/lorem" />
</LinearLayout>
<LinearLayout
android:id="@+id/gradeSummaryItemAverageContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text="@string/grade_summary_average_semester"
android:textSize="14sp" />
<TextView
android:id="@+id/gradeSummaryItemAverage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="25dp"
android:gravity="end"
android:textSize="12sp"
tools:text="4.74" />
tools:text="2,50" />
</LinearLayout>
<View
android:id="@+id/gradeSummaryItemAverageDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/ic_all_divider" />
<LinearLayout
android:id="@+id/gradeSummaryItemAverageAllYearContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_weight="1"
android:text="@string/grade_summary_average_year"
android:textSize="14sp" />
<TextView
android:id="@+id/gradeSummaryItemAverageAllYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="25dp"
android:gravity="end"
android:textSize="12sp"
tools:text="4,50" />
</LinearLayout>
<View
android:id="@+id/gradeSummaryItemAverageAllYearDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/ic_all_divider" />
<LinearLayout
android:id="@+id/gradeSummaryItemPointsContainer"
android:layout_width="match_parent"
@ -63,9 +123,9 @@
</LinearLayout>
<View
android:id="@+id/gradeSummaryItemPointsDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:id="@+id/gradeSummaryItemPointsDivider"
android:background="@drawable/ic_all_divider" />
<LinearLayout
@ -97,8 +157,8 @@
</LinearLayout>
<View
android:layout_width="match_parent"
android:id="@+id/gradeSummaryItemPredictedDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@drawable/ic_all_divider" />
@ -131,9 +191,9 @@
</LinearLayout>
<View
android:id="@+id/gradeSummaryItemFinalDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:id="@+id/gradeSummaryItemFinalDivider"
android:background="@drawable/ic_all_divider" />
<LinearLayout

View File

@ -0,0 +1,153 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
tools:context=".ui.modules.timetable.TimetableAdapter">
<TextView
android:id="@+id/timetableItemNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:maxLength="2"
android:minWidth="40dp"
android:minHeight="40dp"
android:textColor="?android:textColorPrimary"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0" />
<TextView
android:id="@+id/timetableItemSubject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/timetableItemTimeBarrier"
app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart"
app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/timetableItemTimeStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintBottom_toTopOf="@id/timetableItemTimeFinish"
app:layout_constraintStart_toEndOf="@id/timetableItemNumber"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed"
tools:text="11:11" />
<TextView
android:id="@+id/timetableItemTimeFinish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/timetableItemNumber"
app:layout_constraintTop_toBottomOf="@id/timetableItemTimeStart"
tools:text="12:00" />
<TextView
android:id="@+id/timetableItemTeacher"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart"
app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
android:text="@string/timetable_additional_lesson"
tools:visibility="visible" />
<TextView
android:id="@+id/timetableItemDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="16dp"
android:textColor="?colorTimetableChange"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/timetableItemTimeFinish"
app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="Lekcja odwołana: uczniowie zwolnieni do domu"
tools:visibility="gone" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/timetableItemTimeBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="timetableItemTimeUntil,timetableItemTimeLeft" />
<TextView
android:id="@+id/timetableItemTimeUntil"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="?colorPrimary"
android:textSize="13sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="za 15 min"
tools:visibility="gone" />
<TextView
android:id="@+id/timetableItemTimeLeft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="4dp"
android:background="@drawable/background_timetable_time_left"
android:ellipsize="end"
android:gravity="center"
android:includeFontPadding="false"
android:maxLines="1"
android:paddingLeft="7dp"
android:paddingTop="2dp"
android:paddingRight="7dp"
android:paddingBottom="2dp"
android:textColor="?colorOnPrimary"
android:textSize="13sp"
android:visibility="gone"
app:backgroundTint="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
tools:text="jeszcze 15 min"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,10 +10,11 @@
tools:context=".ui.modules.grade.summary.GradeSummaryAdapter">
<LinearLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginEnd="15dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
@ -21,9 +22,9 @@
android:layout_height="wrap_content"
android:gravity="center"
android:minLines="2"
android:textStyle="bold"
android:text="@string/grade_summary_calculated_average"
android:textSize="16sp" />
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
@ -61,9 +62,64 @@
</LinearLayout>
<LinearLayout
android:layout_width="150dp"
android:layout_height="wrap_content"
android:id="@+id/gradeSummaryScrollableHeaderCalculatedAnnualContainer"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginEnd="15dp"
android:layout_weight="1"
android:orientation="vertical"
tools:visibility="visible">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minLines="2"
android:text="@string/grade_summary_calculated_average_annual"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/gradeSummaryScrollableHeaderCalculatedAnnual"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="21sp"
tools:text="6,00" />
<ImageButton
android:id="@+id/gradeSummaryCalculatedAverageHelpAnnual"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@+string/grade_summary_calculated_average_help_dialog_title"
app:srcCompat="@drawable/ic_help"
app:tint="?colorOnBackground" />
</LinearLayout>
<TextView
android:id="@+id/gradeSummaryScrollableHeaderCalculatedSubjectCountAnnual"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center_horizontal"
android:textSize="13sp"
tools:text="from 8 subjects" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
@ -71,9 +127,9 @@
android:layout_height="wrap_content"
android:gravity="center"
android:minLines="2"
android:textStyle="bold"
android:text="@string/grade_summary_final_average"
android:textSize="16sp" />
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
@ -103,8 +159,8 @@
<TextView
android:id="@+id/gradeSummaryScrollableHeaderFinalSubjectCount"
android:layout_width="match_parent"
android:layout_marginTop="5dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center_horizontal"
android:textSize="13sp"
tools:text="from 5 subjects" />

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/attendance_calculator_menu_settings"
android:icon="@drawable/ic_more_settings"
android:orderInCategory="2"
android:title="@string/pref_attendance_calculator_appearance_settings_title"
app:iconTint="?colorControlNormal"
app:showAsAction="ifRoom" />
</menu>

View File

@ -56,6 +56,11 @@
<item>Pouze mezi lekcemi</item>
<item>Před a mezi lekcemi</item>
</string-array>
<string-array name="timetable_show_additional_lessons_entries">
<item>Nezobrazovat</item>
<item>Zobrazit v řadě</item>
<item>Zobrazit pod pravidelnými hodinami</item>
</string-array>
<string-array name="dashboard_tile_entries">
<item>Šťastné číslo</item>
<item>Nepřečtené zprávy</item>

Some files were not shown because too many files have changed in this diff Show More