1
0

Compare commits

...

160 Commits
2.0.0 ... 2.2.4

Author SHA1 Message Date
aba08e6aa9 Merge branch 'release/2.2.4' 2023-10-27 14:49:46 +02:00
124b6dfd79 Version 2.2.4 2023-10-27 14:49:41 +02:00
1dbaa8bfdc Migrate to separate app-update and review artifacts from play:core (#2336) 2023-10-27 14:09:42 +02:00
25ac171298 Merge branch 'release/2.2.3' into develop 2023-10-26 18:32:01 +02:00
387ff1cba7 Merge branch 'release/2.2.3' 2023-10-26 18:31:56 +02:00
eef3464d0b Version 2.2.3 2023-10-26 18:31:51 +02:00
61297a01c7 New Crowdin updates (#2334) 2023-10-26 14:01:45 +02:00
762d4b1393 Timetable timers fixes (#2333) 2023-10-26 10:06:54 +02:00
2e86b67eec Merge branch 'release/2.2.2' into develop 2023-10-23 20:02:32 +02:00
6071b7571b Merge branch 'release/2.2.2' 2023-10-23 20:02:25 +02:00
fcea2218b5 Version 2.2.2 2023-10-23 19:56:46 +02:00
a4a191700e Bump com.google.firebase:firebase-bom from 32.3.1 to 32.4.0 (#2331) 2023-10-23 17:28:38 +00:00
3d76d41b55 Bump com.squareup.okhttp3:logging-interceptor from 4.11.0 to 4.12.0 (#2330) 2023-10-23 16:49:58 +00:00
0e1c20a952 Bump room from 2.5.2 to 2.6.0 (#2329) 2023-10-23 16:48:23 +00:00
5d14ee7f4e Bump androidx.recyclerview:recyclerview from 1.3.1 to 1.3.2 (#2332) 2023-10-23 16:47:03 +00:00
83527d91f3 Allow direct access to weekend from day navigation when there is any lesson during weekend (#2326) 2023-10-23 13:05:46 +02:00
9d62410530 Sort teachers by name in school and teachers tab (#2327) 2023-10-23 13:05:05 +02:00
5dffbdadfa Points statistics improvements (#2328) 2023-10-23 13:04:42 +02:00
516922d5aa Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2324) 2023-10-14 19:18:28 +00:00
9098e74065 Bump com.google.android.material:material from 1.9.0 to 1.10.0 (#2325) 2023-10-14 19:17:56 +00:00
2f5577cc54 Update SDK to 34 (#2322) 2023-10-06 10:07:55 +02:00
3272c38356 Merge branch 'release/2.2.1' into develop 2023-10-03 01:14:25 +02:00
bcd305bef3 Merge branch 'release/2.2.1' 2023-10-03 01:14:17 +02:00
fc5ad16cb7 Version 2.2.1 2023-10-03 01:14:10 +02:00
c8332a0642 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2321) 2023-10-02 22:49:26 +00:00
3212efe21e Bump com.android.tools.build:gradle from 8.1.1 to 8.1.2 (#2320) 2023-10-02 22:49:05 +00:00
693ce8217d New Crowdin updates (#2313) 2023-10-03 00:48:37 +02:00
2cdd322ed4 Add missing class_id colum in JOIN clause of students with semesters (#2317) 2023-10-02 12:22:02 +02:00
c04b3e40d2 Add negative e-mail validation in school input on support dialog (#2315) 2023-10-02 12:21:04 +02:00
d1d665bbdf Fix student auto selection if there is already active some students logged (#2314) 2023-10-02 12:20:34 +02:00
d70568c446 Merge branch 'release/2.2.0' into develop 2023-09-26 23:13:46 +02:00
1d8378e136 Merge branch 'release/2.2.0' 2023-09-26 23:13:40 +02:00
4a2bf539f0 Version 2.2.0 2023-09-26 23:13:32 +02:00
4d085f8266 New Crowdin updates (#2311) 2023-09-26 21:04:27 +00:00
fca69e7234 Add form dialog to login e-mail support (#2306) 2023-09-26 22:27:08 +02:00
711de0f77f Fix average calculation when there is no real semesters available (#2310) 2023-09-26 22:26:19 +02:00
58d5196ac9 Improve symbol input field (#2312) 2023-09-26 22:25:23 +02:00
26a95ecb99 Auto select students for login (#2307)
* Auto select students for login

* Add sign in icon to sign in button on student select screen
2023-09-26 21:02:36 +02:00
1835446468 Fix password reset related issues (#2308)
* Fix login hint in password reset field

* Don't hide first password reset button

* Change recover button label
2023-09-26 21:01:59 +02:00
4d3b16ec80 Fix password toggle icon tint after clearing error (#2309) 2023-09-26 21:01:25 +02:00
95b4d53fac Add schools API integration (#2302) 2023-09-25 19:44:13 +02:00
0fa197d520 New Crowdin updates (#2303) 2023-09-25 19:43:57 +02:00
646b4a149d Bump about_libraries from 10.8.3 to 10.9.0 (#2304) 2023-09-25 17:43:41 +00:00
afd0c8513a Bump mockk from 1.13.7 to 1.13.8 (#2305) 2023-09-25 17:43:16 +00:00
c4a3da93ca Bump com.huawei.hms:hianalytics from 6.10.0.303 to 6.12.0.300 (#2294) 2023-09-20 21:00:06 +00:00
ff2aa6f195 Bump com.huawei.agconnect:agcp from 1.9.1.300 to 1.9.1.301 (#2297) 2023-09-20 20:59:38 +00:00
1d8d71709f Bump com.huawei.agconnect:agconnect-crash from 1.9.1.300 to 1.9.1.301 (#2298) 2023-09-20 20:49:46 +00:00
aabd7345c1 Bump com.google.firebase:firebase-bom from 32.2.3 to 32.3.1 (#2299) 2023-09-20 20:49:25 +00:00
09d16cf6d8 Bump androidx.annotation:annotation from 1.6.0 to 1.7.0 (#2293) 2023-09-20 20:33:50 +00:00
81d8f7ea48 Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.6.1 to 2.6.2 (#2295) 2023-09-20 20:33:30 +00:00
05a804832b Bump com.google.gms:google-services from 4.3.15 to 4.4.0 (#2300) 2023-09-20 20:33:10 +00:00
db02f0c1e1 Bump com.google.android.gms:play-services-ads from 22.3.0 to 22.4.0 (#2301) 2023-09-20 20:32:56 +00:00
0a40237809 Bump com.github.bastienpaulfr:Treessence from 1.0.5 to 1.1.2 (#2289) 2023-09-04 18:30:02 +00:00
017d46e5db Bump hilt_version from 2.47 to 2.48 (#2290) 2023-09-04 18:13:39 +00:00
8478b8b7ed Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2291) 2023-09-04 18:13:24 +00:00
8cc69728aa Bump kotlin_version from 1.9.0 to 1.9.10 (#2281) 2023-09-01 17:29:02 +00:00
c82e6ae95b New Crowdin updates (#2287) 2023-09-01 19:06:48 +02:00
50a177d18c Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2282) 2023-09-01 17:05:47 +00:00
a77b3d4cd7 Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.8 to 2.9.9 (#2283) 2023-09-01 17:05:29 +00:00
aff56a8311 Bump com.google.firebase:firebase-bom from 32.2.2 to 32.2.3 (#2284) 2023-09-01 17:05:12 +00:00
5238e4d187 Bump com.google.android.gms:play-services-ads from 22.2.0 to 22.3.0 (#2285) 2023-09-01 17:04:57 +00:00
10f9812495 Bump com.android.tools.build:gradle from 8.1.0 to 8.1.1 (#2286) 2023-09-01 17:04:34 +00:00
ab1de323d4 Merge branch 'release/2.1.0' into develop 2023-08-25 00:01:47 +02:00
af346842a3 Merge branch 'release/2.1.0' 2023-08-25 00:01:42 +02:00
8f78324940 Version 2.1.0 2023-08-25 00:01:36 +02:00
3dfc55c4d1 Add admin messages to login screen (#2280) 2023-08-24 11:33:40 +02:00
fbce9e58d0 New Crowdin updates (#2277) 2023-08-23 19:46:53 +02:00
2e2b13384a Try to switch to next school year before it starts (#2278) 2023-08-23 12:24:17 +02:00
533157709b Add option to show empty tiles in the timetable (#2236)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2023-08-22 23:47:12 +02:00
024ca89708 Bump mockk from 1.13.5 to 1.13.7 (#2275) 2023-08-22 21:39:04 +00:00
7d5a29d405 Bump org.gradle.toolchains.foojay-resolver-convention (#2276) 2023-08-22 21:20:45 +00:00
8fbe341607 Bump com.huawei.hms:hianalytics from 6.10.0.302 to 6.10.0.303 (#2272) 2023-08-22 21:20:19 +00:00
e21c17ea99 Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.7 to 2.9.8 (#2270) 2023-08-22 21:20:05 +00:00
c4396036ce Bump com.google.firebase:firebase-bom from 32.2.0 to 32.2.2 (#2271) 2023-08-22 21:19:49 +00:00
722b4e5812 Bump androidx.preference:preference-ktx from 1.2.0 to 1.2.1 (#2274) 2023-08-22 21:19:33 +00:00
74820f9571 New Crowdin updates (#2265) 2023-07-31 21:32:07 +02:00
50326c7a48 Bump androidx.recyclerview:recyclerview from 1.3.0 to 1.3.1 (#2268) 2023-07-31 19:00:37 +00:00
0f129109ba Bump com.android.tools.build:gradle from 8.0.2 to 8.1.0 (#2266) 2023-07-31 19:00:17 +00:00
fc2adff997 Bump androidx.fragment:fragment-ktx from 1.6.0 to 1.6.1 (#2269) 2023-07-31 17:35:38 +00:00
7f6a13a9ee Bump coroutines from 1.7.2 to 1.7.3 (#2267) 2023-07-31 17:35:29 +00:00
64cc24ae60 Add incognito mode in messages (#1970)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2023-07-26 22:17:58 +02:00
91d7ee442e New Crowdin updates (#2257) 2023-07-26 19:37:06 +02:00
b296926423 Timetable widget improvements (#2219) 2023-07-25 21:05:14 +00:00
398bc513fb Bump about_libraries from 10.8.0 to 10.8.3 (#2263) 2023-07-25 10:00:31 +00:00
5b2e2ffb34 Remove tests deprecations (#2260) 2023-07-25 11:37:43 +02:00
e79c5d4d2b Bump hilt_version from 2.46.1 to 2.47 (#2261) 2023-07-24 21:36:06 +00:00
c0161f38c6 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2262) 2023-07-24 21:33:17 +00:00
ef72218906 Bump org.gradle.toolchains.foojay-resolver-convention (#2264) 2023-07-24 21:30:37 +00:00
05741761a2 Bump kotlin_version from 1.8.22 to 1.9.0 (#2255) 2023-07-17 20:08:56 +00:00
86c7de6595 Bump room from 2.5.1 to 2.5.2 (#2250) 2023-07-17 19:15:48 +00:00
88ea753fc6 Bump coroutines from 1.7.1 to 1.7.2 (#2251) 2023-07-17 19:15:34 +00:00
d0819928f3 Bump com.huawei.agconnect:agconnect-crash from 1.9.0.300 to 1.9.1.300 (#2252) 2023-07-17 19:15:21 +00:00
8564e12b01 Bump com.huawei.agconnect:agcp from 1.9.0.300 to 1.9.1.300 (#2254) 2023-07-17 19:15:06 +00:00
29a36aaf6e Bump com.huawei.hms:hianalytics from 6.10.0.301 to 6.10.0.302 (#2253) 2023-07-17 18:11:57 +00:00
dbe608f2dd Bump about_libraries from 10.7.0 to 10.8.0 (#2249) 2023-07-17 17:54:26 +00:00
bb79b33b6d Bump com.google.android.gms:play-services-ads from 22.1.0 to 22.2.0 (#2256) 2023-07-17 17:53:58 +00:00
6e7c12a118 Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.5 to 2.9.7 (#2258) 2023-07-17 17:53:38 +00:00
03cd3aeab7 Bump com.google.firebase:firebase-bom from 32.1.0 to 32.2.0 (#2259) 2023-07-17 17:53:21 +00:00
df8849639b Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2242) 2023-06-14 10:56:32 +00:00
8913b22a20 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2237) 2023-06-08 23:25:46 +00:00
f20ffe44d5 Bump kotlin_version from 1.8.21 to 1.8.22 (#2238) 2023-06-08 23:24:27 +00:00
2f749a690b Bump androidx.fragment:fragment-ktx from 1.5.7 to 1.6.0 (#2239) 2023-06-08 23:18:23 +00:00
ae1951bf58 Merge branch 'release/2.0.8' into develop 2023-06-01 23:43:12 +02:00
391f38485d Merge branch 'release/2.0.8' 2023-06-01 23:43:06 +02:00
fecd5c707d Version 2.0.8 2023-06-01 23:43:01 +02:00
fa44295d59 New Crowdin updates (#2231) 2023-06-01 23:37:01 +02:00
5306044173 Add custom register host field on login screen (#2221) 2023-06-01 23:22:10 +02:00
c1b86674c2 Merge branch 'release/2.0.7' into develop 2023-06-01 11:00:01 +02:00
fd482777e8 Merge branch 'release/2.0.7' 2023-06-01 10:59:55 +02:00
d4ae0d56d6 Version 2.0.7 2023-06-01 10:59:50 +02:00
63487249b8 New Crowdin updates (#2211) 2023-06-01 10:31:42 +02:00
1bc0f2d214 Add character limit to attendance excuse content (#2222) 2023-06-01 10:30:50 +02:00
41bde45731 Bump androidx.viewpager2:viewpager2 from 1.1.0-beta01 to 1.1.0-beta02 (#2227) 2023-05-31 15:19:25 +00:00
556f42195b Bump com.android.tools.build:gradle from 8.0.1 to 8.0.2 (#2228) 2023-05-31 15:18:55 +00:00
06fd7b0c36 Bump com.google.firebase:firebase-bom from 32.0.0 to 32.1.0 (#2225) 2023-05-31 15:18:30 +00:00
db4e4d8cef Bump com.huawei.hms:hianalytics from 6.10.0.300 to 6.10.0.301 (#2224) 2023-05-31 15:18:07 +00:00
48e4a9fec5 Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2223) 2023-05-31 15:17:48 +00:00
70333737cf Bump androidx.activity:activity-ktx from 1.7.1 to 1.7.2 (#2226) 2023-05-31 15:16:42 +00:00
3096fa1538 Bump about_libraries from 10.6.3 to 10.7.0 (#2214) 2023-05-24 21:50:17 +00:00
19ed121466 Bump io.coil-kt:coil from 2.3.0 to 2.4.0 (#2215) 2023-05-24 21:32:28 +00:00
e7733bfa2a Bump com.google.android.gms:play-services-ads from 22.0.0 to 22.1.0 (#2216) 2023-05-24 21:31:38 +00:00
b9b464ea9b Merge branch 'release/2.0.6' into develop 2023-05-23 16:26:43 +02:00
cc46b3b124 Merge branch 'release/2.0.6' 2023-05-23 16:26:36 +02:00
092e86b621 Version 2.0.6 2023-05-23 16:26:31 +02:00
c170614461 Add R8 rule for Wulkanowy SDK (#2220) 2023-05-23 14:09:48 +02:00
6ce8e00ebf Merge branch 'release/2.0.5' into develop 2023-05-23 02:37:55 +02:00
c40cdf88ad Merge branch 'release/2.0.5' 2023-05-23 02:37:43 +02:00
4c1fe233c7 Version 2.0.5 2023-05-23 02:37:38 +02:00
aca88b57e0 Add r8 rules for HMS SDK (#2217) 2023-05-23 02:16:42 +02:00
a603c12625 Merge branch 'release/2.0.4' into develop 2023-05-22 17:10:09 +02:00
5c440010e2 Merge branch 'release/2.0.4' 2023-05-22 17:10:02 +02:00
4920317573 Version 2.0.4 2023-05-22 17:09:57 +02:00
9466482893 Add foojay-resolver and update dependencies (#2212) 2023-05-18 14:14:14 +00:00
48bcf581cf Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2209) 2023-05-15 18:36:50 +00:00
8a7b7103eb Bump coroutines from 1.7.0 to 1.7.1 (#2207) 2023-05-15 18:36:26 +00:00
ea312c3e12 Bump androidx.core:core-ktx from 1.10.0 to 1.10.1 (#2208) 2023-05-15 17:13:02 +00:00
5b0fe2c006 Bump ru.cian:huawei-publish-gradle-plugin from 1.3.5 to 1.4.0 (#2185) 2023-05-14 19:28:49 +00:00
a06add070e Bump com.android.tools.build:gradle from 7.4.2 to 8.0.1 (#2187) 2023-05-14 18:20:56 +00:00
dce491bffe Bump hilt_version from 2.45 to 2.46.1 (#2205) 2023-05-14 17:28:29 +00:00
adf418cc68 Fix delete user homework button visibility (#2204) 2023-05-13 10:44:09 +02:00
defcfec971 Merge branch 'release/2.0.3' into develop 2023-05-12 22:59:59 +02:00
d08f195968 Merge branch 'release/2.0.3' 2023-05-12 22:59:45 +02:00
1e9a6a5c42 Version 2.0.3 2023-05-12 22:59:40 +02:00
cc752ab0ad New Crowdin updates (#2201) 2023-05-12 22:45:50 +02:00
f2faa7e8b7 Fix button color in high priority admin message (#2202) 2023-05-12 22:45:24 +02:00
030fe8c218 Bump coroutines from 1.6.4 to 1.7.0 (#2186) 2023-05-12 04:45:15 +00:00
c33b309cf0 Bump org.robolectric:robolectric from 4.10 to 4.10.2 (#2188) 2023-05-12 04:34:48 +00:00
a0af55825d Bump about_libraries from 10.6.2 to 10.6.3 (#2189) 2023-05-12 04:34:32 +00:00
cb8303f33d Bump com.google.android.material:material from 1.8.0 to 1.9.0 (#2191) 2023-05-12 04:34:14 +00:00
54fbd56b73 Bump com.google.firebase:firebase-bom from 31.5.0 to 32.0.0 (#2190) 2023-05-12 04:33:56 +00:00
70f50cd51b Merge branch 'release/2.0.2' into develop 2023-05-12 00:46:02 +02:00
88c38c4a8d Merge branch 'release/2.0.2' 2023-05-12 00:45:55 +02:00
b8ac72c247 Version 2.0.2 2023-05-12 00:45:48 +02:00
cbef160ada New Crowdin updates (#2193) 2023-05-12 00:41:53 +02:00
e77894bf3e Merge branch 'release/2.0.1' into develop 2023-05-12 00:21:36 +02:00
9697a39464 Merge branch 'release/2.0.1' 2023-05-12 00:21:28 +02:00
5a2622871f Version 2.0.1 2023-05-12 00:21:24 +02:00
52218e800c Add auth dialog (#2198)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2023-05-11 21:45:20 +00:00
3fdd47c221 Fix ime overlaping message content (#2199) 2023-05-11 16:44:10 +02:00
d8f71f48f3 Merge branch 'release/2.0.0' into develop 2023-05-07 23:40:31 +02:00
188 changed files with 9022 additions and 926 deletions

View File

@ -13,10 +13,10 @@ jobs:
environment: google-play
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |
@ -49,10 +49,10 @@ jobs:
environment: app-gallery
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |

View File

@ -19,10 +19,10 @@ jobs:
environment: app-center
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |
@ -89,10 +89,10 @@ jobs:
if: github.event_name != 'pull_request_target'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |

View File

@ -19,10 +19,10 @@ jobs:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |
@ -45,10 +45,10 @@ jobs:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |
@ -71,10 +71,10 @@ jobs:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 11
java-version: 17
- uses: actions/cache@v3
with:
path: |

View File

@ -1,8 +1,11 @@
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import ru.cian.huawei.publish.ReleaseNote
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.devtools.ksp'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
@ -10,37 +13,29 @@ apply plugin: 'com.github.triplet.play'
apply plugin: 'ru.cian.huawei-publish'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'com.huawei.agconnect'
apply plugin: 'kotlin-kapt'
apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
android {
namespace 'io.github.wulkanowy'
compileSdkVersion 33
compileSdk 34
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 33
versionCode 122
versionName "2.0.0"
targetSdkVersion 34
versionCode 136
versionName "2.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: ""
]
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true"
]
}
}
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
@ -73,6 +68,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
}
debug {
minifyEnabled false
@ -82,10 +78,11 @@ android {
versionNameSuffix "-dev"
ext.enableCrashlytics = project.hasProperty("enableFirebase")
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
}
}
flavorDimensions "platform"
flavorDimensions += "platform"
productFlavors {
hms {
@ -124,20 +121,20 @@ android {
}
}
testOptions.unitTests {
includeAndroidResources = true
testOptions {
unitTests.includeAndroidResources = true
// workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750
all { jvmArgs '-noverify' }
unitTests.all { jvmArgs '-noverify' }
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
}
@ -156,13 +153,16 @@ android {
kapt {
correctErrorTypes true
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
play {
defaultToAppBundles = false
track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.50d
updatePriority = 2
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.01d
updatePriority = 0
enabled.set(false)
}
@ -171,7 +171,13 @@ huaweiPublish {
hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "aab"
deployType = "draft"
deployType = "publish"
releaseNotes = [
new ReleaseNote(
"pl-PL",
"$projectDir/src/main/play/release-notes/pl-PL/default.txt"
)
]
}
}
}
@ -179,34 +185,34 @@ huaweiPublish {
ext {
work_manager = "2.8.1"
android_hilt = "1.0.0"
room = "2.5.1"
room = "2.6.0"
chucker = "3.5.2"
mockk = "1.13.5"
coroutines = "1.6.4"
mockk = "1.13.8"
coroutines = "1.7.3"
}
dependencies {
implementation "io.github.wulkanowy:sdk:2.0.0"
implementation 'io.github.wulkanowy:sdk:2.2.4'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.10.0"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.7.1"
implementation "androidx.activity:activity-ktx:1.8.0"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.7"
implementation "androidx.annotation:annotation:1.6.0"
implementation "androidx.fragment:fragment-ktx:1.6.1"
implementation "androidx.annotation:annotation:1.7.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.3.0"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
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.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "com.google.android.material:material:1.8.0"
implementation "com.google.android.material:material:1.10.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.3.0'
@ -214,11 +220,11 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room"
kapt "androidx.room:room-compiler:$room"
ksp "androidx.room:room-compiler:$room"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
@ -230,29 +236,30 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.3.0"
implementation "io.coil-kt:coil:2.4.0"
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.10.0'
playImplementation platform('com.google.firebase:firebase-bom:31.5.0')
playImplementation platform('com.google.firebase:firebase-bom:32.4.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx'
playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:22.0.0'
playImplementation 'com.google.android.gms:play-services-ads:22.4.0'
playImplementation "com.google.android.play:integrity:1.2.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300'
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -265,7 +272,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.10'
testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0"

View File

@ -1,16 +1,16 @@
apply plugin: "jacoco"
jacoco {
toolVersion "0.8.7"
toolVersion "0.8.10"
reportsDirectory.set(file("$buildDir/reports"))
}
tasks.withType(Test) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
task jacocoTestReport(type: JacocoReport) {
tasks.register('jacocoTestReport', JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports"
@ -33,19 +33,19 @@ task jacocoTestReport(type: JacocoReport) {
'**/*_Factory.*']
classDirectories.setFrom(fileTree(
dir: "$buildDir/intermediates/classes/debug",
excludes: excludes
dir: "$buildDir/intermediates/classes/debug",
excludes: excludes
) + fileTree(
dir: "$buildDir/tmp/kotlin-classes/fdroidDebug",
excludes: excludes
dir: "$buildDir/tmp/kotlin-classes/fdroidDebug",
excludes: excludes
))
sourceDirectories.setFrom(files([
"src/main/java",
"src/fdroid/java"
"src/main/java",
"src/fdroid/java"
]))
executionData.setFrom(fileTree(
dir: project.projectDir,
includes: ["**/*.exec", "**/*.ec"]
dir: project.projectDir,
includes: ["**/*.exec", "**/*.ec"]
))
}

View File

@ -1,5 +1,6 @@
# General
-dontobfuscate
-ignorewarnings
#Config for wulkanowy
@ -24,3 +25,18 @@
#Config for Material Components
-keep class com.google.android.material.tabs.** { *; }
#Config for HMS SDK
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keep class com.huawei.agconnect.**{*;}
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
#Config for Wulkanowy SDK
-keep,allowobfuscation,allowshrinking class retrofit2.Response

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.utils
import android.view.View
import javax.inject.Inject
class InAppUpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates() {}
fun onResume() {}
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.utils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class IntegrityHelper @Inject constructor() {
@Suppress("UNUSED_PARAMETER")
fun getIntegrityToken(requestId: String): String? = null
}

View File

@ -1,17 +0,0 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.view.View
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class UpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates(activity: Activity) {}
fun onActivityResult(requestCode: Int, resultCode: Int) {}
fun onResume(activity: Activity) {}
}

View File

@ -2,8 +2,8 @@ package io.github.wulkanowy.utils
import android.util.Log
import com.huawei.agconnect.crash.AGConnectCrash
import fr.bipi.tressence.base.FormatterPriorityTree
import fr.bipi.tressence.common.StackTraceRecorder
import fr.bipi.treessence.base.FormatterPriorityTree
import fr.bipi.treessence.common.StackTraceRecorder
class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) {

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.utils
import android.view.View
import javax.inject.Inject
class InAppUpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates() {}
fun onResume() {}
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.utils
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class IntegrityHelper @Inject constructor() {
@Suppress("UNUSED_PARAMETER")
fun getIntegrityToken(requestId: String): String? = null
}

View File

@ -1,17 +0,0 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.view.View
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class UpdateHelper @Inject constructor() {
lateinit var messageContainer: View
fun checkAndInstallUpdates(activity: Activity) {}
fun onActivityResult(requestCode: Int, resultCode: Int) {}
fun onResume(activity: Activity) {}
}

View File

@ -50,5 +50,9 @@
{
"displayName": "Tomasz F.",
"githubUsername": "Pengwius"
},
{
"displayName": "Antoni Paduch",
"githubUsername": "janAte1"
}
]

View File

@ -6,7 +6,7 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.HiltAndroidApp
import fr.bipi.tressence.file.FileLoggerTree
import fr.bipi.treessence.file.FileLoggerTree
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.*

View File

@ -14,6 +14,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -82,19 +83,29 @@ internal class DataModule {
@Singleton
@Provides
fun provideRetrofit(
fun provideAdminMessageService(
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
): Retrofit = Retrofit.Builder()
): AdminMessageService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create()
@Singleton
@Provides
fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create()
fun provideSchoolsService(
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo,
): SchoolsService = Retrofit.Builder()
.baseUrl(appInfo.schoolsBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create()
@Singleton
@Provides

View File

@ -148,7 +148,7 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline mapResult: (ResultType) -> T
crossinline mapResult: (ResultType) -> T,
) = flow {
emit(Resource.Loading())

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
import retrofit2.http.Body
import retrofit2.http.POST
import javax.inject.Singleton
@Singleton
interface SchoolsService {
@POST("/log/loginEvent")
suspend fun logLoginEvent(@Body request: IntegrityRequest<LoginEvent>)
}

View File

@ -49,6 +49,8 @@ import javax.inject.Singleton
AutoMigration(from = 47, to = 48),
AutoMigration(from = 51, to = 52),
AutoMigration(from = 54, to = 55, spec = Migration55::class),
AutoMigration(from = 55, to = 56),
AutoMigration(from = 56, to = 57, spec = Migration57::class),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -57,7 +59,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 55
const val VERSION_SCHEMA = 57
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.db
import androidx.room.TypeConverter
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.serialization.SerializationException
@ -68,4 +69,9 @@ class Converters {
@TypeConverter
fun stringToDestination(destination: String): Destination = json.decodeFromString(destination)
@TypeConverter
fun messageTypesToString(types: List<MessageType>): String = json.encodeToString(types)
@TypeConverter
fun stringToMessageTypes(text: String): List<MessageType> = json.decodeFromString(text)
}

View File

@ -22,4 +22,4 @@ abstract class AdminMessageDao : BaseDao<AdminMessage> {
deleteAll(oldMessages)
insertAll(newMessages)
}
}
}

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.*
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
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 javax.inject.Singleton
@ -19,6 +21,9 @@ abstract class StudentDao {
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Update(entity = Student::class)
abstract suspend fun update(studentName: StudentName)
@Query("SELECT * FROM Students WHERE is_current = 1")
abstract suspend fun loadCurrent(): Student?
@ -29,12 +34,12 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students")
abstract suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Transaction
@Query("SELECT * FROM Students WHERE id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): StudentWithSemesters?
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
abstract suspend fun updateCurrent(id: Long)

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.Serializable
@Serializable
@ -33,7 +34,8 @@ data class AdminMessage(
val priority: String,
val type: String,
@ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),
@ColumnInfo(name = "is_dismissible")
val isDismissible: Boolean = false

View File

@ -19,6 +19,9 @@ data class Student(
@ColumnInfo(name = "scrapper_base_url")
val scrapperBaseUrl: String,
@ColumnInfo(name = "scrapper_domain_suffix", defaultValue = "")
val scrapperDomainSuffix: String,
@ColumnInfo(name = "mobile_base_url")
val mobileBaseUrl: String,

View File

@ -0,0 +1,18 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity
data class StudentName(
@ColumnInfo(name = "student_name")
val studentName: String
) : Serializable {
@PrimaryKey
var id: Long = 0
}

View File

@ -1,13 +1,8 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
import java.io.Serializable
data class StudentWithSemesters(
@Embedded
val student: Student,
@Relation(parentColumn = "student_id", entityColumn = "student_id")
val semesters: List<Semester>
) : Serializable

View File

@ -0,0 +1,10 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
@DeleteColumn(
tableName = "AdminMessages",
columnName = "type",
)
class Migration57 : AutoMigrationSpec

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.data.enums
enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.enums
enum class TimetableGapsMode(val value: String) {
NO_GAPS("no_gaps"),
BETWEEN_LESSONS("between"),
BETWEEN_AND_BEFORE_LESSONS("before_and_between");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: BETWEEN_LESSONS
}
}

View File

@ -55,6 +55,7 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
fun RegisterStudent.mapToStudentWithSemesters(
user: RegisterUser,
scrapperDomainSuffix: String,
symbol: RegisterSymbol,
unit: RegisterUnit,
colors: List<Long>,
@ -76,6 +77,7 @@ fun RegisterStudent.mapToStudentWithSemesters(
studentName = "$studentName $studentSurname",
loginMode = user.loginMode.name,
scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(),
scrapperDomainSuffix = scrapperDomainSuffix,
mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(),
certificateKey = symbol.keyId.orEmpty(),
privateKey = symbol.privatePem.orEmpty(),

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.pojos
import kotlinx.serialization.Serializable
@Serializable
data class LoginEvent(
val uuid: String,
val schoolName: String,
val schoolShort: String,
val schoolAddress: String,
val scraperBaseUrl: String,
val symbol: String,
val schoolId: String,
val loginType: String,
)
@Serializable
data class IntegrityRequest<T>(
val tokenString: String,
val data: T,
)

View File

@ -1,10 +1,11 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -13,34 +14,20 @@ import javax.inject.Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
private val appInfo: AppInfo
) {
private val saveFetchResultMutex = Mutex()
suspend fun getAdminMessages(student: Student) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it == null },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
showSavedOnLoading = false,
mapResult = { adminMessages ->
adminMessages.filter { adminMessage ->
val isCorrectRegister = adminMessage.targetRegisterHost?.let {
student.scrapperBaseUrl.contains(it, true)
} ?: true
val isCorrectFlavor =
adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
val isCorrectMaxVersion =
adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion =
adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true
isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion
}.maxByOrNull { it.id }
}
)
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
showSavedOnLoading = false,
)
}

View File

@ -3,18 +3,26 @@ package io.github.wulkanowy.data.repositories
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
@ -25,7 +33,6 @@ import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
@ -97,7 +104,7 @@ class MessageRepository @Inject constructor(
shouldFetch = {
checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isBlank()
(it.message.unread && markAsRead) || it.message.content.isBlank()
},
query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
@ -113,7 +120,10 @@ class MessageRepository @Inject constructor(
messagesDb.updateAll(
listOf(old.message.apply {
id = message.id
unread = !markAsRead
unread = when {
markAsRead -> false
else -> unread
}
sender = new.sender
recipients = new.recipients.singleOrNull() ?: "Wielu adresatów"
content = content.ifBlank { new.content }
@ -123,7 +133,7 @@ class MessageRepository @Inject constructor(
items = new.attachments.mapToEntities(message.messageGlobalKey),
)
Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read")
Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read: $markAsRead")
}
)

View File

@ -15,7 +15,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
@ -195,10 +194,12 @@ class PreferencesRepository @Inject constructor(
)
)
val showTimetableTimers: Boolean
get() = getBoolean(
R.string.pref_key_timetable_show_timers,
R.bool.pref_default_timetable_show_timers
val showTimetableGaps: TimetableGapsMode
get() = TimetableGapsMode.getByValue(
getString(
R.string.pref_key_timetable_show_gaps,
R.string.pref_default_timetable_show_gaps
)
)
val showSubjectsWithoutGrades: Boolean
@ -343,6 +344,12 @@ class PreferencesRepository @Inject constructor(
)
}
var isIncognitoMode: Boolean
get() = getBoolean(R.string.pref_key_incognito_moge, R.bool.pref_default_incognito_mode)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_incognito_moge), value)
}
var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }

View File

@ -0,0 +1,68 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.IntegrityHelper
import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.init
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.seconds
@Singleton
class SchoolsRepository @Inject constructor(
private val integrityHelper: IntegrityHelper,
private val schoolsService: SchoolsService,
private val sdk: Sdk,
) {
suspend fun logSchoolLogin(loginData: LoginData, students: List<StudentWithSemesters>) {
students.forEach {
runCatching {
withTimeout(10.seconds) {
logLogin(loginData, it.student, it.semesters.getCurrentOrLast())
}
}
.onFailure { Timber.e(it) }
}
}
private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) {
val requestId = UUID.randomUUID().toString()
val token = integrityHelper.getIntegrityToken(requestId) ?: return
val schoolInfo = sdk
.init(student.copy(password = loginData.password))
.switchDiary(
diaryId = semester.diaryId,
kindergartenDiaryId = semester.kindergartenDiaryId,
schoolYear = semester.schoolYear
)
.getSchool()
schoolsService.logLoginEvent(
IntegrityRequest(
tokenString = token,
data = LoginEvent(
uuid = requestId,
schoolAddress = schoolInfo.address,
schoolName = schoolInfo.name,
schoolShort = student.schoolShortName,
scraperBaseUrl = student.scrapperBaseUrl,
loginType = student.loginType,
symbol = student.symbol,
schoolId = student.schoolSymbol,
)
)
)
}
}

View File

@ -41,7 +41,7 @@ class SemesterRepository @Inject constructor(
val isRefreshOnModeChangeRequired = when {
Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> {
semesters.firstOrNull { it.isCurrent }?.let {
semesters.firstOrNull { it.isCurrent() }?.let {
0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true
}
@ -49,7 +49,7 @@ class SemesterRepository @Inject constructor(
}
val isRefreshOnNoCurrentAppropriate =
refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent }
refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent() }
return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate
}

View File

@ -6,7 +6,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
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
@ -14,6 +16,7 @@ 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.init
import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt
import kotlinx.coroutines.withContext
@ -40,22 +43,14 @@ class StudentRepository @Inject constructor(
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
suspend fun getStudentsScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getUserSubjectsFromScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
domainSuffix: String,
symbol: String
): RegisterUser = sdk
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid(
@ -67,20 +62,28 @@ class StudentRepository @Inject constructor(
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
suspend fun getSavedStudents(decryptPass: Boolean = true) =
studentDb.loadStudentsWithSemesters()
.map {
it.apply {
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
StudentWithSemesters(
student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
}
}
}
}
},
semesters = semesters,
)
}
}
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) =
studentDb.loadStudentWithSemestersById(id)?.apply {
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true): StudentWithSemesters? =
studentDb.loadStudentWithSemestersById(id).let { res ->
StudentWithSemesters(
student = res.keys.firstOrNull() ?: return null,
semesters = res.values.first(),
)
}.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
@ -146,4 +149,21 @@ class StudentRepository @Inject constructor(
suspend fun isOneUniqueStudent() = getSavedStudents(false)
.distinctBy { it.student.studentName }.size == 1
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getCurrentStudent() ?: return
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
}
}

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.domain.adminmessage
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
operator fun invoke(student: Student, type: MessageType): Flow<Resource<AdminMessage?>> {
return invoke(student.scrapperBaseUrl, type)
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }
.filter { it.isVersionMatch() }
.filter { it.isRegisterHostMatch(scrapperBaseUrl) }
.filter { it.isFlavorMatch() }
.filter { it.isTypeMatch(type) }
.maxByOrNull { it.id }
}
}
private fun AdminMessage.isNotDismissed(): Boolean {
return id !in preferencesRepository.dismissedAdminMessageIds
}
private fun AdminMessage.isRegisterHostMatch(scrapperBaseUrl: String): Boolean {
return targetRegisterHost?.let {
scrapperBaseUrl.contains(it, true)
} ?: true
}
private fun AdminMessage.isFlavorMatch(): Boolean {
return targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
}
private fun AdminMessage.isVersionMatch(): Boolean {
val isCorrectMaxVersion = versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion = versionMin?.let { it <= appInfo.versionCode } ?: true
return isCorrectMaxVersion && isCorrectMinVersion
}
private fun AdminMessage.isTypeMatch(messageType: MessageType): Boolean {
if (messageType in types) return true
if (MessageType.GENERAL_MESSAGE in types) return true
return false
}
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.widget.RemoteViewsService
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
@ -26,10 +27,19 @@ class TimetableWidgetService : RemoteViewsService() {
@Inject
lateinit var sharedPref: SharedPrefProvider
@Inject
lateinit var prefRepository: PreferencesRepository
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
Timber.d("TimetableWidgetFactory created")
return TimetableWidgetFactory(
timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent
timetableRepository = timetableRepo,
studentRepository = studentRepo,
semesterRepository = semesterRepo,
sharedPref = sharedPref,
prefRepository = prefRepository,
context = applicationContext,
intent = intent,
)
}
}

View File

@ -10,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
@ -76,6 +77,10 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
.show()
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(supportFragmentManager, "auth_dialog")
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
messageContainer?.let {
Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG)

View File

@ -5,10 +5,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.elevation.SurfaceColors
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.lifecycleAwareVariable
import javax.inject.Inject
@ -40,17 +40,19 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
(activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl)
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(SurfaceColors.SURFACE_3.getColor(requireContext()))
}
@CallSuper
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@ -7,6 +7,7 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.lifecycleAwareVariable
abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragment(layoutId),
@ -42,6 +43,10 @@ abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragme
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}

View File

@ -1,10 +1,15 @@
package io.github.wulkanowy.ui.base
import io.github.wulkanowy.data.repositories.StudentRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import timber.log.Timber
open class BasePresenter<T : BaseView>(
@ -26,6 +31,7 @@ open class BasePresenter<T : BaseView>(
onSessionExpired = view::showExpiredDialog
onNoCurrentStudent = view::openClearLoginView
onPasswordChangeRequired = view::showChangePasswordSnackbar
onAuthorizationRequired = view::showAuthDialog
}
}

View File

@ -8,6 +8,8 @@ interface BaseView {
fun showExpiredDialog()
fun showAuthDialog()
fun openClearLoginView()
fun showErrorDetailsDialog(error: Throwable)

View File

@ -3,6 +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.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import io.github.wulkanowy.utils.getErrorString
@ -20,6 +21,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var onPasswordChangeRequired: (String) -> Unit = {}
var onAuthorizationRequired: () -> Unit = {}
fun dispatch(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error)
@ -31,6 +34,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
}
}
@ -39,5 +43,6 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
onSessionExpired = {}
onNoCurrentStudent = {}
onPasswordChangeRequired = {}
onAuthorizationRequired = {}
}
}

View File

@ -148,6 +148,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
binding.attendanceNavDate.text = date
}
override fun showNavigation(show: Boolean) {
binding.attendanceNavContainer.isVisible = show
}
override fun clearData() {
with(attendanceAdapter) {
items = emptyList()
@ -281,7 +285,9 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
presenter.currentDate?.let {
outState.putLong(SAVED_DATE_KEY, it.toEpochDay())
}
}
override fun onDestroyView() {

View File

@ -3,10 +3,14 @@ package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.*
@ -14,6 +18,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDate.now
import java.time.LocalDate.ofEpochDay
@ -28,9 +33,10 @@ class AttendancePresenter @Inject constructor(
private val analytics: AnalyticsHelper
) : BasePresenter<AttendanceView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = now().previousOrSameSchoolDay
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate
var currentDate: LocalDate? = null
private set
private lateinit var lastError: Throwable
@ -44,27 +50,34 @@ class AttendancePresenter @Inject constructor(
view.initView()
Timber.i("Attendance view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
reloadView(ofEpochDay(date ?: baseDate.toEpochDay()))
currentDate = date?.let(::ofEpochDay)
loadData()
if (currentDate.isHolidays) setBaseDateOnHolidays()
}
fun onPreviousDay() {
val date = if (isWeekendHasLessons) {
currentDate?.minusDays(1)
} else currentDate?.previousSchoolDay
view?.finishActionMode()
attendanceToExcuseList.clear()
reloadView(currentDate.previousSchoolDay)
reloadView(date ?: return)
loadData()
}
fun onNextDay() {
val date = if (isWeekendHasLessons) {
currentDate?.plusDays(1)
} else currentDate?.nextSchoolDay
view?.finishActionMode()
attendanceToExcuseList.clear()
reloadView(currentDate.nextSchoolDay)
reloadView(date ?: return)
loadData()
}
fun onPickDate() {
view?.showDatePickerDialog(currentDate)
view?.showDatePickerDialog(currentDate ?: return)
}
fun onDateSet(year: Int, month: Int, day: Int) {
@ -93,10 +106,8 @@ class AttendancePresenter @Inject constructor(
Timber.i("Attendance view is reselected")
view?.let { view ->
if (view.currentStackSize == 1) {
baseDate = now().previousOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
if (currentDate != initialDate) {
reloadView(initialDate ?: return)
loadData()
} else if (!view.isViewEmpty) {
view.resetView()
@ -188,19 +199,6 @@ class AttendancePresenter @Inject constructor(
return true
}
private fun setBaseDateOnHolidays() {
flow {
val student = studentRepository.getCurrentStudent()
emit(semesterRepository.getCurrentSemester(student))
}.catch {
Timber.i("Loading semester result: An exception occurred")
}.onEach {
baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear)
currentDate = baseDate
reloadNavigation()
}.launch("holidays")
}
private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading attendance data started")
@ -211,11 +209,13 @@ class AttendancePresenter @Inject constructor(
isParent = student.isParent
val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester)
attendanceRepository.getAttendance(
student = student,
semester = semester,
start = currentDate,
end = currentDate,
start = currentDate ?: now(),
end = currentDate ?: now(),
forceRefresh = forceRefresh
)
}
@ -231,6 +231,8 @@ class AttendancePresenter @Inject constructor(
}.sortedBy { item -> item.number }
}
.onResourceData {
isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it)
view?.run {
enableSwipe(true)
showProgress(false)
@ -238,6 +240,7 @@ class AttendancePresenter @Inject constructor(
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
updateData(it)
reloadNavigation()
}
}
.onResourceIntermediate { view?.showRefresh(true) }
@ -263,6 +266,43 @@ class AttendancePresenter @Inject constructor(
.launch()
}
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) {
if (initialDate == null) {
val lessons = attendanceRepository.getAttendance(
student = student,
semester = semester,
start = now().monday,
end = now().sunday,
forceRefresh = false,
).toFirstResult().dataOrNull.orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester)
}
if (currentDate == null) {
currentDate = initialDate
}
}
private fun isWeekendHasLessons(
lessons: List<Attendance>,
): Boolean = lessons.any {
it.date.dayOfWeek in listOf(
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY,
)
}
private fun getInitialDate(semester: Semester): LocalDate {
val now = now()
return when {
now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear)
isWeekendHasLessons -> now
else -> now.previousOrSameSchoolDay
}
}
private fun excuseAbsence(reason: String?, toExcuseList: List<Attendance>) {
resourceFlow {
val student = studentRepository.getCurrentStudent()
@ -311,7 +351,7 @@ class AttendancePresenter @Inject constructor(
private fun reloadView(date: LocalDate) {
currentDate = date
Timber.i("Reload attendance view with the date ${currentDate.toFormattedString()}")
Timber.i("Reload attendance view with the date ${currentDate?.toFormattedString()}")
view?.apply {
showProgress(true)
enableSwipe(false)
@ -326,10 +366,13 @@ class AttendancePresenter @Inject constructor(
@SuppressLint("DefaultLocale")
private fun reloadNavigation() {
val currentDate = currentDate ?: return
view?.apply {
showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise())
showNavigation(true)
}
}
}

View File

@ -40,6 +40,8 @@ interface AttendanceView : BaseView {
fun showContent(show: Boolean)
fun showNavigation(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)

View File

@ -0,0 +1,81 @@
package io.github.wulkanowy.ui.modules.auth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.databinding.DialogAuthBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
@Inject
lateinit var presenter: AuthPresenter
companion object {
fun newInstance() = AuthDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return DialogAuthBinding.inflate(inflater).apply { binding = this }.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(this)
binding.authInput.doOnTextChanged { text, _, _, _ ->
presenter.onPeselChange(text?.toString())
}
binding.authButton.setOnClickListener { presenter.authorize() }
binding.authSuccessButton.setOnClickListener {
activity?.recreate()
dismiss()
}
binding.authButtonSkip.setOnClickListener { dismiss() }
}
override fun enableAuthButton(isEnabled: Boolean) {
binding.authButton.isEnabled = isEnabled
}
override fun showProgress(show: Boolean) {
binding.authProgress.isVisible = show
}
override fun showPeselError(show: Boolean) {
binding.authInputLayout.error = getString(R.string.auth_api_error).takeIf { show }
}
override fun showInvalidPeselError(show: Boolean) {
binding.authInputLayout.error = getString(R.string.auth_invalid_error).takeIf { show }
}
override fun showSuccess(show: Boolean) {
binding.authSuccess.isVisible = show
}
override fun showContent(show: Boolean) {
binding.authForm.isVisible = show
}
override fun showDescriptionWithName(name: String) {
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
}
}

View File

@ -0,0 +1,100 @@
package io.github.wulkanowy.ui.modules.auth
import io.github.wulkanowy.data.repositories.SemesterRepository
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 javax.inject.Inject
class AuthPresenter @Inject constructor(
private val semesterRepository: SemesterRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AuthView>(errorHandler, studentRepository) {
private var pesel: String = ""
override fun onAttachView(view: AuthView) {
super.onAttachView(view)
view.enableAuthButton(pesel.length == 11)
view.showSuccess(false)
view.showProgress(false)
loadName()
}
private fun loadName() {
presenterScope.launch {
runCatching { studentRepository.getCurrentStudent(false) }
.onSuccess { view?.showDescriptionWithName(it.studentName) }
.onFailure { errorHandler.dispatch(it) }
}
}
fun onPeselChange(newPesel: String?) {
pesel = newPesel.orEmpty()
view?.enableAuthButton(pesel.length == 11)
view?.showPeselError(false)
view?.showInvalidPeselError(false)
}
fun authorize() {
presenterScope.launch {
view?.showProgress(true)
view?.showContent(false)
if (!isValidPESEL(pesel)) {
view?.showInvalidPeselError(true)
view?.showProgress(false)
view?.showContent(true)
return@launch
}
runCatching {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
if (isSuccess) {
studentRepository.refreshStudentName(student, semester)
}
isSuccess
}
.onFailure { errorHandler.dispatch(it) }
.onSuccess {
if (it) {
view?.showSuccess(true)
view?.showContent(false)
view?.showPeselError(false)
} else {
view?.showSuccess(false)
view?.showContent(true)
view?.showPeselError(true)
}
}
view?.showProgress(false)
}
}
private fun isValidPESEL(peselString: String): Boolean {
if (peselString.length != 11) {
return false
}
val weights = intArrayOf(1, 3, 7, 9, 1, 3, 7, 9, 1, 3)
var sum = 0
for (i in 0 until 10) {
sum += weights[i] * Character.getNumericValue(peselString[i])
}
sum %= 10
sum = 10 - sum
sum %= 10
return sum == Character.getNumericValue(peselString[10])
}
}

View File

@ -0,0 +1,20 @@
package io.github.wulkanowy.ui.modules.auth
import io.github.wulkanowy.ui.base.BaseView
interface AuthView : BaseView {
fun enableAuthButton(isEnabled: Boolean)
fun showProgress(show: Boolean)
fun showPeselError(show: Boolean)
fun showInvalidPeselError(show: Boolean)
fun showSuccess(show: Boolean)
fun showContent(show: Boolean)
fun showDescriptionWithName(name: String)
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.dashboard
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import java.util.*
class DashboardItemMoveCallback(
@ -55,5 +56,5 @@ class DashboardItemMoveCallback(
}
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
get() = this is AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
}

View File

@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
@ -32,7 +34,7 @@ class DashboardPresenter @Inject constructor(
private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val adsHelper: AdsHelper
) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
@ -159,19 +161,23 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Type.ACCOUNT -> {
updateData(DashboardItem.Account(student), forceRefresh)
}
DashboardItem.Type.HORIZONTAL_GROUP -> {
loadHorizontalGroup(student, forceRefresh)
}
DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Type.ANNOUNCEMENTS -> {
loadSchoolAnnouncements(student, forceRefresh)
}
DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh)
DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh)
}
DashboardItem.Type.ADS -> loadAds(forceRefresh)
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
}
@ -355,6 +361,7 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.GRADES
}
}
is Resource.Success -> {
Timber.i("Loading dashboard grades result: Success")
updateData(
@ -365,6 +372,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh
)
}
is Resource.Error -> {
Timber.i("Loading dashboard grades result: An exception occurred")
errorHandler.dispatch(it.error)
@ -378,7 +386,7 @@ class DashboardPresenter @Inject constructor(
private fun loadLessons(student: Student, forceRefresh: Boolean) {
flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay
val date = LocalDate.now()
timetableRepository.getTimetable(
student = student,
@ -402,12 +410,14 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.LESSONS
}
}
is Resource.Success -> {
Timber.i("Loading dashboard lessons result: Success")
updateData(
DashboardItem.Lessons(it.data), forceRefresh
)
}
is Resource.Error -> {
Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error)
@ -457,10 +467,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.HOMEWORK
}
}
is Resource.Success -> {
Timber.i("Loading dashboard homework result: Success")
updateData(DashboardItem.Homework(it.data), forceRefresh)
}
is Resource.Error -> {
Timber.i("Loading dashboard homework result: An exception occurred")
errorHandler.dispatch(it.error)
@ -489,10 +501,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
}
}
is Resource.Success -> {
Timber.i("Loading dashboard announcements result: Success")
updateData(DashboardItem.Announcements(it.data), forceRefresh)
}
is Resource.Error -> {
Timber.i("Loading dashboard announcements result: An exception occurred")
errorHandler.dispatch(it.error)
@ -530,10 +544,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.EXAMS
}
}
is Resource.Success -> {
Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data), forceRefresh)
}
is Resource.Error -> {
Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error)
@ -569,10 +585,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.CONFERENCES
}
}
is Resource.Success -> {
Timber.i("Loading dashboard conferences result: Success")
updateData(DashboardItem.Conferences(it.data), forceRefresh)
}
is Resource.Error -> {
Timber.i("Loading dashboard conferences result: An exception occurred")
errorHandler.dispatch(it.error)
@ -584,12 +602,12 @@ class DashboardPresenter @Inject constructor(
}
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
flatResourceFlow { adminMessageRepository.getAdminMessages(student) }
.filter {
val data = it.dataOrNull ?: return@filter true
val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds
!isDismissed
}
flatResourceFlow {
getAppropriateAdminMessageUseCase(
student = student,
type = MessageType.DASHBOARD_MESSAGE,
)
}
.onEach {
when (it) {
is Resource.Loading -> {
@ -597,6 +615,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh)
}
is Resource.Success -> {
Timber.i("Loading dashboard admin message result: Success")
updateData(
@ -604,6 +623,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh
)
}
is Resource.Error -> {
Timber.i("Loading dashboard admin message result: An exception occurred")
Timber.e(it.error)

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
import android.os.Handler
import android.os.Looper
@ -24,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.utils.*
import timber.log.Timber
import java.time.Duration
@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false)
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
)
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position)
is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
is AdsViewHolder -> bindAdsViewHolder(holder, position)
}
}
@ -733,38 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
private fun bindAdminMessage(adminMessageViewHolder: AdminMessageViewHolder, position: Int) {
val item = (items[position] as DashboardItem.AdminMessages).adminMessage ?: return
val context = adminMessageViewHolder.binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorPrimary) to
context.getThemeAttrColor(R.attr.colorOnPrimary)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(adminMessageViewHolder.binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) {
val item = (items[position] as DashboardItem.Ads).adBanner ?: return
val binding = adsViewHolder.binding
@ -818,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() }
}
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
RecyclerView.ViewHolder(binding.root)

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.ui.modules.dashboard.viewholders
import android.content.res.ColorStateList
import android.graphics.Color
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.getThemeAttrColor
class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?) {
item ?: return
val context = binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
}

View File

@ -58,7 +58,7 @@ class GradeAverageProvider @Inject constructor(
when (params.gradeAverageMode) {
ONE_SEMESTER -> getGradeSubjects(
student = student,
semester = semesters.single { it.semesterId == semesterId },
semester = semesters.first { it.semesterId == semesterId },
forceRefresh = forceRefresh,
params = params,
)

View File

@ -22,6 +22,8 @@ import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.roundToInt
class GradeStatisticsAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -269,7 +271,7 @@ class GradeStatisticsAdapter @Inject constructor() :
valueTextSize = 12f
valueTextColor = binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}"
}
colors = gradePointsColors
}
@ -304,15 +306,20 @@ class GradeStatisticsAdapter @Inject constructor() :
}
xAxis.setDrawLabels(false)
xAxis.setDrawGridLines(false)
val yMaxFromValues = (max(points.others, points.student)).roundToInt() + 30f
val yMaxFromValuesWithMargin = ((yMaxFromValues / 10.0).roundToInt() * 10).toFloat()
val yMax = yMaxFromValuesWithMargin.coerceAtLeast(100f)
val yLabelCount = (yMax / 10).toInt() + 1
with(axisLeft) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
axisMaximum = yMax
labelCount = yLabelCount
}
with(axisRight) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
axisMaximum = yMax
labelCount = yLabelCount
}
invalidate()
}

View File

@ -23,7 +23,7 @@ import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.notifications.NotificationsFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.UpdateHelper
import io.github.wulkanowy.utils.InAppUpdateHelper
import javax.inject.Inject
@AndroidEntryPoint
@ -33,7 +33,7 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
override lateinit var presenter: LoginPresenter
@Inject
lateinit var updateHelper: UpdateHelper
lateinit var inAppUpdateHelper: InAppUpdateHelper
@Inject
lateinit var appInfo: AppInfo
@ -47,10 +47,10 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root)
setSupportActionBar(binding.loginToolbar)
messageContainer = binding.loginContainer
updateHelper.messageContainer = binding.loginContainer
inAppUpdateHelper.messageContainer = binding.loginContainer
presenter.onAttachView(this)
updateHelper.checkAndInstallUpdates(this)
inAppUpdateHelper.checkAndInstallUpdates()
if (savedInstanceState == null) {
openFragment(LoginFormFragment.newInstance(), clearBackStack = true)
@ -117,14 +117,6 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
override fun onResume() {
super.onResume()
updateHelper.onResume(this)
}
//https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode)
inAppUpdateHelper.onResume()
}
}

View File

@ -6,5 +6,6 @@ data class LoginData(
val login: String,
val password: String,
val baseUrl: String,
val domainSuffix: String,
val symbol: String?,
) : Serializable

View File

@ -5,6 +5,7 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.ArrayAdapter
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -55,6 +56,9 @@ class LoginAdvancedFragment :
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formDomainSuffix: String
get() = binding.loginFormDomainSuffix.text.toString().trim()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
@ -279,6 +283,7 @@ class LoginAdvancedFragment :
loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE
loginFormDomainSuffixLayout.isVisible = true
loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE
@ -290,6 +295,7 @@ class LoginAdvancedFragment :
loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE
loginFormDomainSuffixLayout.isVisible = true
loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE
@ -301,6 +307,7 @@ class LoginAdvancedFragment :
loginFormUsernameLayout.visibility = GONE
loginFormPassLayout.visibility = GONE
loginFormHostLayout.visibility = GONE
loginFormDomainSuffixLayout.isVisible = false
loginFormPinLayout.visibility = VISIBLE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = VISIBLE

View File

@ -154,6 +154,7 @@ class LoginAdvancedPresenter @Inject constructor(
login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim(),
domainSuffix = view?.formDomainSuffix.orEmpty().trim(),
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
)
when (it.data.symbols.size) {
@ -186,6 +187,7 @@ class LoginAdvancedPresenter @Inject constructor(
val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty()
val endpoint = view?.formHostValue.orEmpty()
val domainSuffix = view?.formDomainSuffix.orEmpty()
val pin = view?.formPinValue.orEmpty()
val symbol = view?.formSymbolValue.orEmpty()
@ -193,8 +195,8 @@ class LoginAdvancedPresenter @Inject constructor(
return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) {
Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token)
Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper(
email, password, endpoint, symbol
Sdk.Mode.SCRAPPER -> studentRepository.getUserSubjectsFromScrapper(
email, password, endpoint, domainSuffix, symbol
)
Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid(

View File

@ -12,6 +12,8 @@ interface LoginAdvancedView : BaseView {
val formHostValue: String
val formDomainSuffix: String
val formHostSymbol: String
val formLoginType: String

View File

@ -9,12 +9,16 @@ 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.FragmentLoginFormBinding
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
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.*
import javax.inject.Inject
@ -45,6 +49,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formDomainSuffix: String
get() = binding.loginFormDomainSuffix.text.toString()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
@ -179,7 +186,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
override fun clearPassError() {
binding.loginFormPassLayout.error = null
binding.loginFormPassLayout.setEndIconTintList(null)
binding.loginFormPassLayout.setEndIconTintList(
requireContext().getAttrColorStateList(R.attr.colorOnSurface)
)
binding.loginFormErrorBox.isVisible = false
}
@ -204,6 +213,23 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormContainer.visibility = if (show) VISIBLE else GONE
}
override fun showAdminMessage(message: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun showDomainSuffixInput(show: Boolean) {
binding.loginFormDomainSuffixLayout.isVisible = show
}
override fun showOtherOptionsButton(show: Boolean) {
binding.loginFormAdvancedButton.isVisible = show
}
@ -214,8 +240,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
override fun showContact(show: Boolean) {
binding.loginFormContact.visibility = if (show) VISIBLE else GONE
binding.loginFormRecoverLink.visibility = if (show) GONE else VISIBLE
binding.loginFormContact.isVisible = show
}
override fun openPrivacyPolicyPage() {
@ -256,22 +281,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
override fun onResume() {
super.onResume()
presenter.updateUsernameLabel()
presenter.updateCustomDomainSuffixVisibility()
}
override fun openEmail(lastError: String) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$formHostValue/$formHostSymbol",
preferencesRepository.installationId,
lastError
)
)
override fun openEmail(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
}

View File

@ -1,11 +1,23 @@
package io.github.wulkanowy.ui.modules.login.form
import androidx.core.net.toUri
import io.github.wulkanowy.data.*
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.onResourceLoading
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
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.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank
@ -17,7 +29,9 @@ class LoginFormPresenter @Inject constructor(
studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler,
private val appInfo: AppInfo,
private val analytics: AnalyticsHelper
private val analytics: AnalyticsHelper,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
@ -36,6 +50,31 @@ class LoginFormPresenter @Inject constructor(
Timber.i("Entered wrong username or password")
}
}
reloadAdminMessage()
}
private fun reloadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = view?.formHostValue.orEmpty(),
type = MessageType.LOGIN_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch()
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
}
fun onPrivacyLinkClick() {
@ -56,7 +95,15 @@ class LoginFormPresenter @Inject constructor(
} else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") {
setCredentials("", "")
}
updateCustomDomainSuffixVisibility()
updateUsernameLabel()
reloadAdminMessage()
}
}
fun updateCustomDomainSuffixVisibility() {
view?.run {
showDomainSuffixInput("customSuffix" in formHostValue)
}
}
@ -87,20 +134,36 @@ class LoginFormPresenter @Inject constructor(
}
}
fun onSignInClick() {
private fun getLoginData(): LoginData {
val email = view?.formUsernameValue.orEmpty().trim()
val password = view?.formPassValue.orEmpty().trim()
val host = view?.formHostValue.orEmpty().trim()
val domainSuffix = view?.formDomainSuffix.orEmpty().trim().takeIf {
"customSuffix" in host
}.orEmpty()
val symbol = view?.formHostSymbol.orEmpty().trim()
if (!validateCredentials(email, password, host)) return
return LoginData(
login = email,
password = password,
baseUrl = host,
domainSuffix = domainSuffix,
symbol = symbol
)
}
fun onSignInClick() {
val loginData = getLoginData()
if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return
resourceFlow {
studentRepository.getUserSubjectsFromScrapper(
email = email,
password = password,
scrapperBaseUrl = host,
symbol = symbol
email = loginData.login,
password = loginData.password,
scrapperBaseUrl = loginData.baseUrl,
domainSuffix = loginData.domainSuffix,
symbol = loginData.symbol.orEmpty(),
)
}
.logResourceStatus("login")
@ -112,7 +175,6 @@ class LoginFormPresenter @Inject constructor(
}
}
.onResourceSuccess {
val loginData = LoginData(email, password, host, symbol)
when (it.symbols.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(loginData, it)
@ -120,7 +182,7 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent(
"registration_form",
"success" to true,
"scrapperBaseUrl" to host,
"scrapperBaseUrl" to loginData.baseUrl,
"error" to "No error"
)
}
@ -137,7 +199,7 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent(
"registration_form",
"success" to false,
"scrapperBaseUrl" to host,
"scrapperBaseUrl" to loginData.baseUrl,
"error" to it.message.ifNullOrBlank { "No message" }
)
}
@ -149,7 +211,14 @@ class LoginFormPresenter @Inject constructor(
}
fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { "none" })
view?.openEmail(
LoginSupportInfo(
loginData = getLoginData(),
lastErrorMessage = lastError?.message,
registerUser = null,
enteredSymbol = null,
)
)
}
fun onRecoverClick() {

View File

@ -1,8 +1,10 @@
package io.github.wulkanowy.ui.modules.login.form
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
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginFormView : BaseView {
@ -14,6 +16,8 @@ interface LoginFormView : BaseView {
val formHostValue: String
val formDomainSuffix: String
val formHostSymbol: String
val nicknameLabel: String
@ -56,6 +60,12 @@ interface LoginFormView : BaseView {
fun showContent(show: Boolean)
fun showAdminMessage(message: AdminMessage?)
fun openInternetBrowser(url: String)
fun showDomainSuffixInput(show: Boolean)
fun showOtherOptionsButton(show: Boolean)
fun showVersion()
@ -70,7 +80,7 @@ interface LoginFormView : BaseView {
fun openFaqPage()
fun openEmail(lastError: String)
fun openEmail(supportInfo: LoginSupportInfo)
fun openAdvancedLogin()

View File

@ -46,7 +46,7 @@ class LoginRecoverPresenter @Inject constructor(
fun updateFields() {
view?.run {
setUsernameHint(if ("standard" in recoverHostValue) emailHintString else loginPeselEmailHintString)
setUsernameHint(if ("email" in recoverHostValue) emailHintString else loginPeselEmailHintString)
}
}
@ -92,7 +92,7 @@ class LoginRecoverPresenter @Inject constructor(
isCorrect = false
}
if ("standard" in host && "@" !in username) {
if ("email" in host && "@" !in username) {
view?.setUsernameError(view?.invalidEmailString.orEmpty())
isCorrect = false
}

View File

@ -10,8 +10,11 @@ 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.auth.AuthDialog
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
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
@ -106,21 +109,8 @@ class LoginStudentSelectFragment :
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
override fun openEmail(lastError: String) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"Select users to log in",
preferencesRepository.installationId,
lastError
)
)
override fun openEmail(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
override fun onDestroyView() {

View File

@ -9,23 +9,25 @@ 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.SchoolsRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank
import io.github.wulkanowy.utils.isCurrent
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository,
private val schoolsRepository: SchoolsRepository,
private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
@ -71,7 +73,14 @@ class LoginStudentSelectPresenter @Inject constructor(
students = it.dataOrNull.orEmpty()
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> refreshItems()
is Resource.Success -> {
getStudentsWithCurrentlyActiveSemesters()
selectedStudents.clear()
selectedStudents.addAll(getStudentsWithCurrentlyActiveSemesters())
view?.enableSignIn(selectedStudents.isNotEmpty())
refreshItems()
}
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
@ -81,6 +90,23 @@ class LoginStudentSelectPresenter @Inject constructor(
}.launch()
}
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
val students = registerUser.symbols.flatMap { symbol ->
symbol.schools.flatMap { unit ->
unit.students.map {
createStudentItem(it, symbol, unit, students)
}
}
}
return students
.filter { it.isEnabled }
.filter { student ->
student.student.semesters.any { semester ->
semester.isCurrent()
}
}
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
@ -236,16 +262,20 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun registerStudents(students: List<LoginStudentSelectItem>) {
val studentsWithSemesters = students
.filterIsInstance<LoginStudentSelectItem.Student>().map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow { studentRepository.saveStudents(studentsWithSemesters) }
val filteredStudents = students.filterIsInstance<LoginStudentSelectItem.Student>()
val studentsWithSemesters = filteredStudents.map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
scrapperDomainSuffix = loginData.domainSuffix,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow {
studentRepository.saveStudents(studentsWithSemesters)
schoolsRepository.logSchoolLogin(loginData, studentsWithSemesters)
}
.logResourceStatus("registration")
.onEach {
when (it) {
@ -253,11 +283,13 @@ class LoginStudentSelectPresenter @Inject constructor(
showProgress(true)
showContent(false)
}
is Resource.Success -> {
syncManager.startOneTimeSyncWorker(quiet = true)
view?.navigateToNext()
logRegisterEvent(studentsWithSemesters)
}
is Resource.Error -> {
view?.apply {
showProgress(false)
@ -280,28 +312,14 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank {
loginData.baseUrl + "/" + loginData.symbol + "\n" + registerUser.symbols.filterNot {
(it.error is AccountPermissionException || it.error is InvalidSymbolException) && it.symbol != loginData.symbol
}.joinToString(";\n") { symbol ->
buildString {
append(" -")
append(symbol.symbol)
append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})")
if (symbol.schools.isNotEmpty()) {
append(": ")
}
append(symbol.schools.joinToString(", ") { unit ->
buildString {
append(unit.schoolShortName)
append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})")
}
})
}
} + "\nPozostałe: " + registerUser.symbols.filter {
it.error is AccountPermissionException || it.error is InvalidSymbolException
}.joinToString(", ") { it.symbol }
})
view?.openEmail(
LoginSupportInfo(
loginData = loginData,
registerUser = registerUser,
lastErrorMessage = lastError?.message,
enteredSymbol = loginData.symbol,
)
)
}
private fun logRegisterEvent(

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginStudentSelectView : BaseView {
@ -23,5 +24,5 @@ interface LoginStudentSelectView : BaseView {
fun openDiscordInvite()
fun openEmail(lastError: String)
fun openEmail(supportInfo: LoginSupportInfo)
}

View File

@ -0,0 +1,149 @@
package io.github.wulkanowy.ui.modules.login.support
import android.os.Bundle
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.DialogLoginSupportBinding
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
class LoginSupportDialog : BaseDialogFragment<DialogLoginSupportBinding>() {
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
private lateinit var supportInfo: LoginSupportInfo
companion object {
private const val ARGUMENT_KEY = "info"
fun newInstance(info: LoginSupportInfo) = LoginSupportDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to info)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_FRAME, R.style.WulkanowyTheme_NoActionBar)
supportInfo = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DialogLoginSupportBinding.inflate(inflater)
.apply { binding = this }
binding.dialogLoginSupportToolbar.setNavigationOnClickListener { dismiss() }
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
dialogLoginSupportSchoolInput.doOnTextChanged { _, _, _, _ ->
with(dialogLoginSupportSchoolLayout) {
isErrorEnabled = false
error = null
}
}
dialogLoginSupportSubmit.setOnClickListener { onSubmitClick() }
}
}
private fun onSubmitClick() {
when {
binding.dialogLoginSupportSchoolInput.text.isNullOrBlank() -> {
with(binding.dialogLoginSupportSchoolLayout) {
isErrorEnabled = true
error = getString(R.string.error_field_required)
}
}
Patterns.EMAIL_ADDRESS.matcher(
binding.dialogLoginSupportSchoolInput.text.toString()
).matches() -> {
with(binding.dialogLoginSupportSchoolLayout) {
isErrorEnabled = true
error = getString(R.string.login_support_school_invalid)
}
}
else -> {
openEmailClientWithFilledTemplate()
dismiss()
}
}
}
private fun openEmailClientWithFilledTemplate() {
with(binding) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
supportInfo.loginData.baseUrl + "/" + supportInfo.loginData.symbol,
preferencesRepository.installationId,
getLastErrorFromStudentSelectScreen(),
dialogLoginSupportSchoolInput.text.takeIf { !it.isNullOrBlank() }
?: return@with,
dialogLoginSupportAdditionalInput.text,
)
)
}
}
private fun getLastErrorFromStudentSelectScreen(): String {
if (!supportInfo.lastErrorMessage.isNullOrBlank()) {
return supportInfo.lastErrorMessage!!
}
if (supportInfo.registerUser?.symbols.isNullOrEmpty()) {
return ""
}
return "\n" + supportInfo.registerUser?.symbols?.filterNot {
(it.error is AccountPermissionException || it.error is InvalidSymbolException) &&
it.symbol != supportInfo.enteredSymbol
}?.joinToString(";\n") { symbol ->
buildString {
append(" -")
append(symbol.symbol)
append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})")
if (symbol.schools.isNotEmpty()) {
append(": ")
}
append(symbol.schools.joinToString(", ") { unit ->
buildString {
append(unit.schoolShortName)
append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})")
}
})
}
} + "\nPozostałe: " + supportInfo.registerUser?.symbols?.filter {
it.error is AccountPermissionException || it.error is InvalidSymbolException
}?.joinToString(", ") { it.symbol }
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.login.support
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.modules.login.LoginData
import java.io.Serializable
data class LoginSupportInfo(
val loginData: LoginData,
val registerUser: RegisterUser?,
val lastErrorMessage: String?,
val enteredSymbol: String?,
) : Serializable

View File

@ -18,7 +18,13 @@ import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject
@AndroidEntryPoint
@ -100,6 +106,13 @@ class LoginSymbolFragment :
}
}
override fun setErrorSymbolDefinitelyInvalid() {
with(binding.loginSymbolNameLayout) {
requestFocus()
error = getString(R.string.login_invalid_symbol_definitely)
}
}
override fun setErrorSymbolRequire() {
setErrorSymbol(getString(R.string.error_field_required))
}
@ -163,20 +176,7 @@ class LoginSymbolFragment :
)
}
override fun openEmail(host: String, lastError: String) {
context?.openEmailClient(
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$host/${binding.loginSymbolName.text}",
preferencesRepository.installationId,
lastError
)
)
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
}

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach
@ -53,6 +54,10 @@ class LoginSymbolPresenter @Inject constructor(
view?.setErrorSymbolRequire()
return
}
if (isFormDefinitelyInvalid()) {
view?.setErrorSymbolDefinitelyInvalid()
return
}
loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(),
@ -62,6 +67,7 @@ class LoginSymbolPresenter @Inject constructor(
email = loginData.login,
password = loginData.password,
scrapperBaseUrl = loginData.baseUrl,
domainSuffix = loginData.domainSuffix,
symbol = loginData.symbol.orEmpty(),
)
}.onEach { user ->
@ -73,6 +79,7 @@ class LoginSymbolPresenter @Inject constructor(
showProgress(true)
showContent(false)
}
is Resource.Success -> {
when (user.data.symbols.size) {
0 -> {
@ -82,6 +89,7 @@ class LoginSymbolPresenter @Inject constructor(
showContact(true)
}
}
else -> {
val enteredSymbolDetails = user.data.symbols
.firstOrNull()
@ -106,6 +114,7 @@ class LoginSymbolPresenter @Inject constructor(
"error" to "No error"
)
}
is Resource.Error -> {
Timber.i("Login with symbol result: An exception occurred")
analytics.logEvent(
@ -129,17 +138,25 @@ class LoginSymbolPresenter @Inject constructor(
}.launch("login")
}
private fun isFormDefinitelyInvalid(): Boolean {
val definitelyInvalidSymbols = listOf("vulcan", "uonet", "wulkanowy", "standardowa")
val normalizedSymbol = view?.symbolValue.orEmpty().getNormalizedSymbol()
return normalizedSymbol in definitelyInvalidSymbols
}
fun onFaqClick() {
view?.openFaqPage()
}
fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank {
registerUser?.symbols?.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}?.filterNotNull()?.distinct()?.joinToString(";") {
it.take(46) + "..."
} ?: "blank"
})
view?.openSupportDialog(
LoginSupportInfo(
loginData = loginData,
registerUser = registerUser,
lastErrorMessage = lastError?.message,
enteredSymbol = view?.symbolValue,
)
)
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
interface LoginSymbolView : BaseView {
@ -18,6 +19,8 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolInvalid()
fun setErrorSymbolDefinitelyInvalid()
fun setErrorSymbolRequire()
fun setErrorSymbol(message: String)
@ -40,5 +43,5 @@ interface LoginSymbolView : BaseView {
fun openFaqPage()
fun openEmail(host: String, lastError: String)
fun openSupportDialog(supportInfo: LoginSupportInfo)
}

View File

@ -45,7 +45,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
lateinit var analytics: AnalyticsHelper
@Inject
lateinit var updateHelper: UpdateHelper
lateinit var inAppUpdateHelper: InAppUpdateHelper
@Inject
lateinit var inAppReviewHelper: InAppReviewHelper
@ -100,7 +100,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer
messageAnchor = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer
inAppUpdateHelper.messageContainer = binding.mainFragmentContainer
onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) {
presenter.onBackPressed()
}
@ -109,20 +109,12 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
?.takeIf { savedInstanceState == null }
presenter.onAttachView(this, destination)
updateHelper.checkAndInstallUpdates(this)
inAppUpdateHelper.checkAndInstallUpdates()
}
override fun onResume() {
super.onResume()
updateHelper.onResume(this)
}
//https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode)
inAppUpdateHelper.onResume()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -11,7 +11,9 @@ import androidx.core.view.updateMargins
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.MessageFolder.*
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.databinding.FragmentMessageBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
@ -49,6 +51,7 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMessageBinding.bind(view)
messageContainer = binding.messageViewPager
presenter.onAttachView(this)
}
@ -95,6 +98,10 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showMessage(messageId: Int) {
showMessage(getString(messageId))
}
override fun showNewMessage(show: Boolean) {
binding.openSendMessageButton.run {
if (show) show() else hide()

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.message
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -9,7 +11,8 @@ import javax.inject.Inject
class MessagePresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<MessageView>(errorHandler, studentRepository) {
override fun onAttachView(view: MessageView) {
@ -19,6 +22,14 @@ class MessagePresenter @Inject constructor(
Timber.i("Message view was initialized")
loadData()
}
showIncognitoModeReminderMessage()
}
private fun showIncognitoModeReminderMessage() {
if (preferencesRepository.isIncognitoMode) {
view?.showMessage(R.string.message_incognito_mode_on)
}
}
fun onPageSelected(index: Int) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message
import androidx.annotation.StringRes
import io.github.wulkanowy.ui.base.BaseView
interface MessageView : BaseView {
@ -12,6 +13,8 @@ interface MessageView : BaseView {
fun showProgress(show: Boolean)
fun showMessage(@StringRes messageId: Int)
fun showNewMessage(show: Boolean)
fun showTabLayout(show: Boolean)

View File

@ -12,6 +12,7 @@ import android.view.View.VISIBLE
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.StringRes
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
@ -164,6 +165,10 @@ class MessagePreviewFragment :
binding.messagePreviewErrorRetry.setOnClickListener { callback() }
}
override fun showMessage(@StringRes messageId: Int) {
showMessage(getString(messageId))
}
override fun openMessageReply(message: Message?) {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) }
}

View File

@ -2,11 +2,13 @@ package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -20,6 +22,7 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -54,7 +57,11 @@ class MessagePreviewPresenter @Inject constructor(
private fun loadData(messageToLoad: Message) {
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
messageRepository.getMessage(student, messageToLoad, true)
messageRepository.getMessage(
student = student,
message = messageToLoad,
markAsRead = !preferencesRepository.isIncognitoMode,
)
}
.logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData {
@ -65,6 +72,10 @@ class MessagePreviewPresenter @Inject constructor(
setMessageWithAttachment(it)
showContent(true)
initOptions()
if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description)
}
}
} else {
view?.run {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.preview
import androidx.annotation.StringRes
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.ui.base.BaseView
@ -43,4 +44,6 @@ interface MessagePreviewView : BaseView {
fun popView()
fun printDocument(html: String, jobName: String)
fun showMessage(@StringRes messageId: Int)
}

View File

@ -142,10 +142,15 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
private fun initializeMessageContainer() {
ViewCompat.setOnApplyWindowInsetsListener(binding.sendMessageScroll) { view, insets ->
val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = bottomInsets.bottom
bottomMargin = if (imeInsets.bottom > navigationBarInsets.bottom) {
imeInsets.bottom
} else {
navigationBarInsets.bottom
}
}
WindowInsetsCompat.CONSUMED
}

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.view.View.*
import android.widget.CompoundButton
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
@ -134,14 +135,20 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
}
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.action_menu_message_tab, menu)
val searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.queryHint = getString(R.string.all_search_hint)
searchView.maxWidth = Int.MAX_VALUE
initializeSearchView(menu)
}
private fun initializeSearchView(menu: Menu) {
val searchView = (menu.findItem(R.id.action_search).actionView as SearchView).apply {
queryHint = getString(R.string.all_search_hint)
maxWidth = Int.MAX_VALUE
}
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(query: String): Boolean {
@ -207,8 +214,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
binding.messageTabSwipe.isRefreshing = show
}
override fun showMessagesDeleted() {
showMessage(getString(R.string.message_messages_deleted))
override fun showMessage(@StringRes messageId: Int) {
showMessage(getString(messageId))
}
override fun notifyParentShowNewMessage(show: Boolean) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.R
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
@ -26,7 +27,7 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val analytics: AnalyticsHelper
private val analytics: AnalyticsHelper,
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
lateinit var folder: MessageFolder
@ -135,7 +136,7 @@ class MessageTabPresenter @Inject constructor(
messageRepository.deleteMessages(student, selectedMailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() }
.onSuccess { view?.showMessage(R.string.message_messages_deleted) }
}
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab
import androidx.annotation.StringRes
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BaseView
@ -26,7 +27,7 @@ interface MessageTabView : BaseView {
fun showEmpty(show: Boolean)
fun showMessagesDeleted()
fun showMessage(@StringRes messageId: Int)
fun showErrorView(show: Boolean)

View File

@ -58,7 +58,10 @@ class TeacherPresenter @Inject constructor(
.logResourceStatus("load teachers data")
.onResourceData {
view?.run {
updateData(it.filter { item -> item.name.isNotBlank() })
updateData(it
.filter { item -> item.name.isNotBlank() }
.sortedBy { it.name }
)
showContent(it.isNotEmpty())
showEmpty(it.isEmpty())
showErrorView(false)

View File

@ -31,4 +31,6 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin
override fun showErrorDetailsDialog(error: Throwable) {}
override fun showChangePasswordSnackbar(redirectUrl: String) {}
override fun showAuthDialog() {}
}

View File

@ -8,6 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@ -34,7 +35,7 @@ class AdvancedFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_advanced, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}
@ -62,6 +63,10 @@ class AdvancedFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)

View File

@ -18,7 +18,8 @@ class AdvancedPresenter @Inject constructor(
Timber.i("Settings advanced view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
analytics.logEvent("setting_changed", "name" to key)
}

View File

@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@ -38,7 +39,7 @@ class AppearanceFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}
@ -78,6 +79,10 @@ class AppearanceFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)

View File

@ -22,7 +22,8 @@ class AppearancePresenter @Inject constructor(
Timber.i("Settings appearance view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
preferencesRepository.apply {

View File

@ -21,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openInternetBrowser
@ -113,7 +114,7 @@ class NotificationsFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_notifications, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}
@ -148,6 +149,10 @@ class NotificationsFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun showFixSyncDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_notify_fix_sync_issues)

View File

@ -38,7 +38,8 @@ class NotificationsPresenter @Inject constructor(
Timber.i("Settings notifications view was initialized")
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
preferencesRepository.apply {

View File

@ -10,6 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
@ -51,7 +52,7 @@ class SyncFragment : PreferenceFragmentCompat(),
setPreferencesFromResource(R.xml.scheme_preferences_sync, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}
@ -99,6 +100,10 @@ class SyncFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, "error_details")
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)

View File

@ -31,7 +31,8 @@ class SyncPresenter @Inject constructor(
setSyncDateInView()
}
fun onSharedPreferenceChanged(key: String) {
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change settings $key")
preferencesRepository.apply {
@ -52,10 +53,12 @@ class SyncPresenter @Inject constructor(
Timber.i("Setting sync now started")
analytics.logEvent("sync_now", "status" to "started")
}
WorkInfo.State.SUCCEEDED -> {
showMessage(syncSuccessString)
analytics.logEvent("sync_now", "status" to "success")
}
WorkInfo.State.FAILED -> {
showError(
syncFailedString,
@ -66,6 +69,7 @@ class SyncPresenter @Inject constructor(
)
analytics.logEvent("sync_now", "status" to "failed")
}
else -> Timber.d("Sync now state: ${workInfo?.state}")
}
if (workInfo?.state?.isFinished == true) {

View File

@ -12,7 +12,9 @@ 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.ItemTimetableSmallBinding
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
@ -29,9 +31,14 @@ class TimetableAdapter @Inject constructor() :
TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false)
)
TimetableItemType.NORMAL -> NormalViewHolder(
ItemTimetableBinding.inflate(inflater, parent, false)
)
TimetableItemType.EMPTY -> EmptyViewHolder(
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
)
}
}
@ -40,12 +47,12 @@ class TimetableAdapter @Inject constructor() :
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isEmpty()) return super.onBindViewHolder(holder, position, payloads)
if (holder is NormalViewHolder) updateTimeLeft(
binding = holder.binding,
timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft,
)
if (payloads.isNotEmpty() && holder is NormalViewHolder) {
updateTimeLeft(
binding = holder.binding,
timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft,
)
} else super.onBindViewHolder(holder, position, payloads)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
@ -54,10 +61,16 @@ 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,
)
}
}
@ -100,6 +113,19 @@ class TimetableAdapter @Inject constructor() :
}
}
private fun bindEmptyView(binding: ItemTimetableEmptyBinding, item: TimetableItem.Empty) {
with(binding) {
timetableEmptyItemNumber.text = when (item.numFrom) {
item.numTo -> item.numFrom.toString()
else -> "${item.numFrom}-${item.numTo}"
}
timetableEmptyItemSubject.text = timetableEmptyItemSubject.context.getPlural(
R.plurals.timetable_no_lesson,
item.numTo - item.numFrom + 1
)
}
}
private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) {
with(binding) {
when {
@ -137,6 +163,7 @@ class TimetableAdapter @Inject constructor() :
timetableItemTimeLeft.visibility = VISIBLE
timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished)
}
else -> {
timetableItemTimeUntil.visibility = GONE
timetableItemTimeLeft.visibility = GONE
@ -191,7 +218,8 @@ class TimetableAdapter @Inject constructor() :
)
} else {
timetableItemDescription.visibility = GONE
timetableItemRoom.isVisible = lesson.room.isNotBlank() || lesson.roomOld.isNotBlank()
timetableItemRoom.isVisible =
lesson.room.isNotBlank() || lesson.roomOld.isNotBlank()
timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank()
timetableItemTeacher.visibility = VISIBLE
}
@ -274,6 +302,9 @@ class TimetableAdapter @Inject constructor() :
private class SmallViewHolder(val binding: ItemTimetableSmallBinding) :
RecyclerView.ViewHolder(binding.root)
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 =
@ -281,9 +312,11 @@ class TimetableAdapter @Inject constructor() :
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start
}
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start
}
else -> oldItem == newItem
}
@ -292,7 +325,7 @@ class TimetableAdapter @Inject constructor() :
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
if (oldItem.lesson == newItem.lesson && oldItem.timeLeft != newItem.timeLeft) {
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

@ -9,6 +9,7 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -160,6 +161,10 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
binding.timetableRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showNavigation(show: Boolean) {
binding.timetableNavContainer.isVisible = true
}
override fun showPreButton(show: Boolean) {
binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE
}
@ -193,7 +198,9 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
presenter.currentDate?.toEpochDay()?.let {
outState.putLong(SAVED_DATE_KEY, it)
}
}
override fun onDestroyView() {

View File

@ -16,6 +16,11 @@ sealed class TimetableItem(val type: TimetableItemType) {
val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL)
data class Empty(
val numFrom: Int,
val numTo: Int
) : TimetableItem(TimetableItemType.EMPTY)
}
data class TimeLeft(
@ -27,4 +32,5 @@ data class TimeLeft(
enum class TimetableItemType {
SMALL,
NORMAL,
EMPTY
}

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