Compare commits

...

209 Commits

Author SHA1 Message Date
4316d4ba10 merge: v2.6.13 (#8)
Reviewed-on: #8
2024-05-24 01:10:46 -05:00
5e78b01415 merge: v2.6.13
All checks were successful
Generate APK / build (pull_request) Successful in 28m2s
2024-05-24 08:08:23 +02:00
Mikołaj Pich
528a947ffd
Merge branch 'release/2.6.13' into develop 2024-05-23 23:26:24 +02:00
Mikołaj Pich
ca9caeca66
Version 2.6.13 2024-05-23 23:26:06 +02:00
Mikołaj Pich
e971eb7821
Add full panic mode (#2569) 2024-05-23 23:15:47 +02:00
7266d1fcd3 merge: v2.6.12 + cache fix (#7)
Reviewed-on: #7
2024-05-23 09:08:17 -05:00
e048fe4c35 Merge branch 'develop' into release/2.6.12
All checks were successful
Generate APK / build (pull_request) Successful in 34m25s
2024-05-23 09:07:44 -05:00
49541e9a7d chore: updated workflow 2024-05-23 16:07:23 +02:00
72c93594f6 merge: v2.6.12 (#6)
Reviewed-on: #6
2024-05-23 07:56:14 -05:00
b249eab1eb bump version to 2.6.12
Some checks failed
Generate APK / build (pull_request) Has been cancelled
2024-05-23 14:53:15 +02:00
Mikołaj Pich
c7afa262d9
Merge branch 'release/2.6.12' into develop 2024-05-23 07:55:17 +02:00
Mikołaj Pich
b622c09e56
Version 2.6.12 2024-05-23 07:55:02 +02:00
Mikołaj Pich
87fb1916d8
Add panic button 2024-05-23 07:37:49 +02:00
Mikołaj Pich
5ca9ac3978
Version 2.6.11 2024-05-22 22:05:11 +02:00
Mikołaj Pich
c76ace40eb
Fix mapping refresh key 2024-05-22 21:05:17 +02:00
Mikołaj Pich
4a585fc56e
Fix mapping refresh key 2024-05-22 21:00:09 +02:00
Mikołaj Pich
f3afe7fdb7
Bump sdk to 2.6.10-SNAPSHOT 2024-05-22 20:31:40 +02:00
Mikołaj Pich
859c6ef154
Add auto refresh to wulkanowy repo 2024-05-22 20:23:57 +02:00
Mikołaj Pich
24abe47332
Version 2.6.10 2024-05-22 19:26:01 +02:00
Mikołaj Pich
52c1878f6b
Bump sdk to 2.6.9-SNAPSHOT 2024-05-22 19:26:01 +02:00
Rafał Borcz
4060240368
New Crowdin updates (#2551) 2024-05-17 22:22:43 +02:00
dependabot[bot]
f69816fbac
Bump coroutines from 1.8.0 to 1.8.1 (#2560) 2024-05-17 20:21:52 +00:00
Mikołaj Pich
1a41e9e3ee
Merge branch 'release/2.6.9' into develop 2024-05-17 21:23:40 +02:00
Mikołaj Pich
1c0df6c145
Version 2.6.9 2024-05-17 21:23:29 +02:00
Mikołaj Pich
2b61e883c5
Bump sdk to 2.6.8-SNAPSHOT 2024-05-17 19:45:53 +02:00
d29d95bfae create v2.6.8 release
Reviewed-on: #5
2024-05-17 02:09:31 -05:00
65a0457675 merge latest develop branch 2024-05-17 09:08:33 +02:00
b25c61a485 merge: v2.6.8 2024-05-17 09:08:20 +02:00
e33350e153 Merge branch 'develop' into release/2.6.8
All checks were successful
Generate APK / build (pull_request) Successful in 1h9m54s
2024-05-17 02:07:44 -05:00
8109459ee2 merge: v2.6.8 2024-05-17 09:04:58 +02:00
Mikołaj Pich
31a7ae6d15
Fix tests 2024-05-17 08:04:28 +02:00
Mikołaj Pich
4b3b4a21fa
Merge branch 'release/2.6.8' into develop 2024-05-17 07:39:16 +02:00
Mikołaj Pich
9a6b17c9d9
Version 2.6.8 2024-05-17 07:38:13 +02:00
Mikołaj Pich
729e0f547b
Add sandbox isolate 2024-05-16 23:23:06 +02:00
Mikołaj Pich
faa8d34e79
Bump sdk to 2.6.7-SNAPSHOT 2024-05-16 18:55:02 +02:00
e553d9cdcf merge: v2.6.7 (#4)
Reviewed-on: #4
2024-05-16 08:18:37 -05:00
Mikołaj Pich
b6f5ac91ad
Merge branch 'release/2.6.7' into develop 2024-05-16 08:50:37 +02:00
Mikołaj Pich
d1d0caa1e3
Version 2.6.7 2024-05-16 08:50:28 +02:00
eed3a93196 merge: v2.6.7
All checks were successful
Generate APK / build (pull_request) Successful in 1h1m19s
2024-05-15 15:15:10 +02:00
Mikołaj Pich
c40779f48f
Merge branch 'release/2.6.6' into develop 2024-05-14 22:31:01 +02:00
Mikołaj Pich
594d2dbec5
Version 2.6.6 2024-05-14 22:30:51 +02:00
f6b8c33121 merge: go-hashfiles action URL (#2)
Reviewed-on: #2
2024-05-13 03:35:00 -05:00
7b728d910e
fix go-hashfiles action URL
All checks were successful
Generate APK / build (pull_request) Successful in 55m42s
2024-05-13 10:28:36 +02:00
7302bb3336 merge: workaround for unsupported hashFiles function (#1)
Reviewed-on: #1
2024-05-13 03:24:43 -05:00
5886ed2bce
update Go
Some checks failed
Generate APK / build (pull_request) Failing after 36s
2024-05-13 10:24:11 +02:00
3fdd92ea35
workaround for unsupported hashFiles function 2024-05-13 10:22:26 +02:00
708e25c622
improvement: trigger workflow only when closing pull request 2024-05-13 10:13:53 +02:00
36d9e02de7
adapt README
Some checks failed
Generate APK / build (push) Has been cancelled
2024-05-13 10:06:04 +02:00
ebe6bf1cc0
remove release due to signing issues 2024-05-13 10:04:52 +02:00
4bd95237c6
final fix for workflows
Some checks failed
Generate debug APK / build (push) Has been cancelled
2024-05-13 09:50:08 +02:00
cb553b1a33
merge: v2.6.5
All checks were successful
Generate debug APK / build (push) Successful in 47m47s
2024-05-12 20:59:38 +02:00
f5a95421a1
refresh & normalize READMEs 2024-05-12 19:47:36 +02:00
f2857bdece
add Gradle caching + rename artifacts
Some checks failed
Generate debug APK / build (push) Has been cancelled
2024-05-12 19:19:14 +02:00
3c1497394c
fix: artifacts (second try) 2024-05-12 19:08:38 +02:00
Mikołaj Pich
26596e8254
Merge branch 'release/2.6.5' into develop 2024-05-12 17:51:07 +02:00
Mikołaj Pich
6e472d6c5c
Version 2.6.5 2024-05-12 17:50:56 +02:00
9f6ae0e6b5
switch to unofficial upload-artifact due to GHES compatibility issue
Some checks failed
Generate debug APK / build (push) Has been cancelled
2024-05-12 16:38:42 +02:00
Rafał Borcz
6f4826249c
Add remote mapping for Wulkanowy SDK (#2550)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-12 16:21:28 +02:00
01fa007c89
fix indentation
Some checks failed
Generate debug APK / build (push) Failing after 32m0s
2024-05-12 13:41:36 +02:00
3f7db44ea7
fix: release workflow & update README
Some checks are pending
Generate debug APK / build (push) Waiting to run
2024-05-12 13:39:55 +02:00
c426701b27
add: missing .sk translation
Some checks failed
Generate debug APK / build (push) Has been cancelled
2024-05-12 13:08:43 +02:00
2d8db2b071
fix: normalize case in README 2024-05-12 13:07:13 +02:00
01f6f402b4
update: README 2024-05-12 13:06:06 +02:00
e058134740
fix: release workflow 2024-05-12 13:03:43 +02:00
0aefab9463
upload debug apks only to artifacts 2024-05-12 13:01:11 +02:00
748e9f62f3
rename: build_android -> build_android_debug 2024-05-12 12:50:21 +02:00
c40e371843
patch: disable release signing 2024-05-12 12:49:57 +02:00
484996120e
add cache support to build workflow
Some checks failed
Generate APK / build (push) Has been cancelled
2024-05-11 15:44:25 +02:00
2de3362e0b
fix: add Android SDK build step
All checks were successful
Generate APK / build (push) Successful in 37m45s
2024-05-11 15:11:22 +02:00
470827fc42
fix: workflow syntax
Some checks failed
Generate APK / build (push) Failing after 14m13s
2024-05-11 14:48:14 +02:00
6a524deca0
add: build workflow
Some checks failed
Generate APK / build (push) Failing after 0s
2024-05-11 14:45:15 +02:00
f9fcfe6d1a
sync with origin 2024-05-11 08:43:29 +02:00
02f8b45ad5
remove workflows 2024-05-11 08:42:45 +02:00
6ccb2e9b5c
merge: v2.6.4
Some checks failed
Generate APK / build (push) Failing after 1h51m59s
2024-05-10 20:23:11 +02:00
cbf405fb16
apply mod changes to original repository 2024-05-10 20:22:58 +02:00
Mikołaj Pich
2f3e1b6aae
Merge branch 'release/2.6.4' into develop 2024-05-10 12:35:39 +02:00
Mikołaj Pich
567d868f76
Version 2.6.4 2024-05-10 12:30:30 +02:00
Mikołaj Pich
12030efee2
Version 2.6.3 2024-05-09 22:59:20 +02:00
Kacper Majcher
04c382643d
Fix a typo in settings - apperance & behaviour - menu configuration (#2547) 2024-05-09 18:17:10 +02:00
58f7c84cd5
fix: invalid order of actions 2024-05-09 14:36:06 +02:00
a2e641523a
fix: adapt actions to gitea 2024-05-09 14:33:52 +02:00
3b2eed487a
fix: adapt actions to gitea (first try) 2024-05-09 14:30:07 +02:00
bc999f1b9c
omit VULCAN block, sync with latest SDK 2024-05-09 12:46:53 +02:00
Mikołaj Pich
fc140ad9c1
Merge branch 'release/2.6.2' into develop 2024-05-08 01:55:42 +02:00
Mikołaj Pich
78a2cc89e9
Version 2.6.2 2024-05-08 01:55:09 +02:00
dependabot[bot]
dbfe5c8918
Bump com.android.tools.build:gradle from 8.3.2 to 8.4.0 (#2539) 2024-05-05 08:27:41 +00:00
dependabot[bot]
c697ca7ad1
Bump androidx.fragment:fragment-ktx from 1.6.2 to 1.7.0 (#2538) 2024-05-05 08:27:28 +00:00
dependabot[bot]
1b00f4e518
Bump about_libraries from 11.1.3 to 11.1.4 (#2540) 2024-05-05 08:27:10 +00:00
dependabot[bot]
fb77bf882f
Bump androidx.viewpager2:viewpager2 from 1.1.0-beta02 to 1.1.0-rc01 (#2542) 2024-05-05 08:26:55 +00:00
dependabot[bot]
466ebbef3a
Bump com.google.firebase:firebase-bom from 32.8.1 to 33.0.0 (#2544) 2024-05-05 08:26:29 +00:00
dependabot[bot]
458a4c8164
Bump androidx.core:core-ktx from 1.13.0 to 1.13.1 (#2545) 2024-05-05 08:26:14 +00:00
Mikołaj Pich
0363c0854f
Merge branch 'release/2.6.1' into develop 2024-05-02 15:20:56 +02:00
Mikołaj Pich
233ddc955b
Version 2.6.1 2024-05-02 15:20:45 +02:00
Mikołaj Pich
065c711f91
Add missing release client_info definition to conform gms plugin in hms release (#2537) 2024-05-02 14:10:33 +02:00
Mikołaj Pich
49655c11c9
Revert "Bump com.google.android.gms:play-services-ads from 22.6.0 to 23.0.0 (…" (#2536)
This reverts commit a7c2009e4971891f8e756ccca504cddee727c689.
2024-05-02 12:43:03 +02:00
Mikołaj Pich
38fd4eda22
Merge branch 'release/2.6.0' into develop 2024-05-01 22:30:53 +02:00
Mikołaj Pich
b71630246a
Version 2.6.0 2024-05-01 22:30:41 +02:00
Rafał Borcz
fd2eac1f08
New Crowdin updates (#2535) 2024-05-01 19:28:49 +02:00
Damian Czupryn
1545ff65d3
CS and SK listing update (#2534) 2024-05-01 19:07:41 +02:00
Rafał Borcz
ff32c82851
New Crowdin updates (#2532)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-01 18:59:57 +02:00
Mikołaj Pich
d531a94594
Update screenshots (#2533) 2024-05-01 18:50:57 +02:00
Rafał Borcz
71ab9586ac
Add admin message to LoginStudentSelect and LoginSymbol (#2531)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-01 16:57:31 +02:00
Rafał Borcz
e1a19be06c
New Crowdin updates (#2530) 2024-05-01 12:29:41 +02:00
Michael
6f2168d641
Additional lessons in timetable view (#2491)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-04-26 23:33:45 +02:00
Rafał Borcz
cde2121b60
New Crowdin updates (#2527) 2024-04-26 22:05:46 +02:00
d7fec4806e
update git artifacts 2024-04-26 16:00:49 +02:00
4fba76d327
merge: v2.8.5 2024-04-25 20:48:55 +02:00
Mikołaj Pich
a0bc37e826
Merge branch 'bugfix/2.5.8' into develop 2024-04-25 12:45:26 +02:00
Michael
ad5381ce34
Fix race condition of showing empty view in timetable (#2486)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-04-24 22:46:55 +02:00
Michael
dbc7587741
Add settings button to attendance calculator (#2492) 2024-04-24 22:44:59 +02:00
Rafał Borcz
bc3aa7b8dc
New Crowdin updates (#2526) 2024-04-24 22:28:16 +02:00
Rafał Borcz
6bf6a9da11
New Crowdin updates (#2480) 2024-04-24 19:27:11 +02:00
Mikołaj Pich
ab175bdd9a
Merge branch 'bugfix/2.5.7' into develop 2024-04-22 22:42:22 +02:00
Mikołaj Pich
2816d7217a
Version 2.5.6 2024-04-22 00:39:44 +02:00
Mikołaj Pich
2fa868173b
Show graduated students on top of student select items list 2024-04-22 00:39:44 +02:00
Mikołaj Pich
622c75bb42
Don't display brackets in login student select items when schoolShortName is blank 2024-04-22 00:39:44 +02:00
Mikołaj Pich
2121125283
Migrate away from userLoginId to studentId due to vulcan last changes 2024-04-22 00:39:44 +02:00
Mikołaj Pich
c72a117e34
Bump sdk to 2.5.6-SNAPSHOT 2024-04-22 00:39:44 +02:00
dependabot[bot]
860095e862
Bump androidx.activity:activity-ktx from 1.8.2 to 1.9.0 (#2522) 2024-04-21 02:31:28 +00:00
dependabot[bot]
ff9be43291
Bump org.apache.commons:commons-text from 1.11.0 to 1.12.0 (#2523) 2024-04-21 02:25:16 +00:00
dependabot[bot]
a487378daf
Bump androidx.core:core-ktx from 1.12.0 to 1.13.0 (#2521) 2024-04-21 02:24:04 +00:00
dependabot[bot]
895f5cbb76
Bump com.android.tools.build:gradle from 8.2.2 to 8.3.2 (#2517) 2024-04-14 14:24:38 +00:00
Rafał Borcz
8b9b1460ab
Update AuthDialog text (#2506) 2024-04-13 22:03:23 +02:00
dependabot[bot]
7edd3df074
Bump about_libraries from 11.1.1 to 11.1.3 (#2518) 2024-04-13 19:56:16 +00:00
dependabot[bot]
16c51f7b07
Bump com.google.firebase:firebase-bom from 32.8.0 to 32.8.1 (#2519) 2024-04-13 19:55:50 +00:00
Rafał Borcz
7a3a97447f
Add isAdded check in AdditionalLessonAddDialog (#2515) 2024-04-08 20:51:05 +02:00
dependabot[bot]
b500d8e204
Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2511) 2024-04-08 18:09:38 +00:00
dependabot[bot]
c34a369286
Bump org.robolectric:robolectric from 4.11.1 to 4.12.1 (#2514) 2024-04-08 18:08:20 +00:00
dependabot[bot]
b9f3ab2e56
Bump com.squareup.retrofit2:retrofit from 2.10.0 to 2.11.0 (#2513) 2024-04-08 18:08:01 +00:00
dependabot[bot]
6b59973624
Bump hilt_version from 2.51 to 2.51.1 (#2510) 2024-04-08 17:41:28 +00:00
Mikołaj Pich
d18485293d
Merge branch 'bugfix/2.5.5' into develop 2024-03-26 20:38:07 +01:00
Mikołaj Pich
596e8df4fc
Merge branch 'bugfix/2.5.4' into develop 2024-03-26 13:29:54 +01:00
dependabot[bot]
f13ce6e2b4
Bump com.squareup.retrofit2:retrofit from 2.9.0 to 2.10.0 (#2502) 2024-03-25 12:32:28 +00:00
dependabot[bot]
8c10606b61
Bump about_libraries from 11.1.0 to 11.1.1 (#2503) 2024-03-25 12:32:07 +00:00
dependabot[bot]
7fda4276d6
Bump com.google.firebase:firebase-bom from 32.7.4 to 32.8.0 (#2504) 2024-03-25 12:31:46 +00:00
Mikołaj Pich
7993366bfc
Merge branch 'bugfix/2.5.3' into develop 2024-03-25 00:05:11 +01:00
ecae5d96c4
pr: merge #11
add developer mode
2024-03-22 17:07:45 +01:00
sadorowo
ddd49575d3 bump version 2024-03-22 17:05:06 +01:00
sadorowo
5b0748a254 translation updates 2024-03-22 17:04:30 +01:00
sadorowo
47ff549d4b impr: code improvements 2024-03-22 17:00:41 +01:00
sadorowo
a1203b360e add: translations (2/2) 2024-03-22 16:46:54 +01:00
sadorowo
c80c041dd0 add: developer mode + translations (1/2) 2024-03-22 16:45:34 +01:00
667b1b14d2
update: new release.yml file 2024-03-22 08:21:18 +01:00
3f2a02f242
add: missing play resource 2024-03-22 06:27:12 +01:00
sadorowo
29196c62fa fix: missing play resource 2024-03-22 06:26:16 +01:00
e5412086d5
add: new settings view
merge #9
2024-03-21 23:41:09 +01:00
sadorowo
ce114e1824 version bump 2024-03-21 23:39:45 +01:00
sadorowo
b0d221473c update: README 2024-03-21 23:38:58 +01:00
sadorowo
ee2803b0d3 fix: typo 2024-03-21 23:31:10 +01:00
sadorowo
47817c47f8 add: language translations 2024-03-21 23:27:40 +01:00
sadorowo
92bad9d112 add: hiding grades, notes and attendance entries 2024-03-21 23:21:06 +01:00
sadorowo
d17614fa64 add: new settings fragment + basic functionality 2024-03-21 20:17:05 +01:00
sadorowo
d068371fb1 fix: invalid build directory + auto generated release notes 2024-03-20 21:55:04 +01:00
sadorowo
af71023bfb fix 2024-03-20 21:10:52 +01:00
sadorowo
b5adc27efa final fix 2024-03-20 21:08:16 +01:00
sadorowo
de0b9cf221 remove unneccessary if statement 2024-03-20 21:03:30 +01:00
sadorowo
425d28d563 switch to new release action 2024-03-20 20:53:10 +01:00
sadorowo
31744e7e77 remove deprecated code 2024-03-20 20:07:52 +01:00
sadorowo
925ad9616d fix: github actions (attempt 4) 2024-03-20 20:05:59 +01:00
sadorowo
878e7b4247 fix: github actions (attempt 3) 2024-03-20 20:03:00 +01:00
sadorowo
e347045ac8 github actions fix (second try) 2024-03-20 19:48:40 +01:00
sadorowo
8eb65196cb separate locales (avoid future conflicts) + rewrite github actions 2024-03-20 19:36:07 +01:00
sadorowo
f99df4ad46 sync fork with v2.5.2 + fix eduOne 2024-03-20 18:15:23 +01:00
Mikołaj Pich
27eb0588d7
Merge branch 'bugfix/2.5.2' into develop 2024-03-20 02:24:26 +01:00
Michael
34d34a050a
Add widget updating on data sync (#2487)
---------

Co-authored-by: Faierbel <RafalBO99@outlook.com>
2024-03-17 21:06:40 +01:00
Michael
961bc24f27
Add docs to Resource, changing networkBoundResource generics naming (#2483) 2024-03-13 19:13:56 +01:00
Michael
8a90b61b97
Refactor networkBoundResource (#2482)
---------

Co-authored-by: Faierbel <RafalBO99@outlook.com>
2024-03-13 13:01:00 +01:00
Rafał Borcz
6a8f6f9496
Add WulkanowySdkFactory (#2479) 2024-03-11 23:38:39 +01:00
Rafał Borcz
afb5ae741c
Fix lateness color in attendance (#2481) 2024-03-11 23:38:17 +01:00
Michael
95e41b5570
Handle subjects with no attendances in attendance calculator better (#2478)
---------

Co-authored-by: Faierbel <RafalBO99@outlook.com>
2024-03-11 19:19:24 +00:00
Rafał Borcz
eb6fdd900e
New Crowdin updates (#2470) 2024-03-11 11:47:13 +01:00
Rafał Borcz
88def5eff8
Remove savedInstance in MessagePreviewFragment (#2477) 2024-03-11 11:45:28 +01:00
Rafał Borcz
0e99c81eb8
Add missing onDetachView in AutDialog (#2476) 2024-03-11 11:45:15 +01:00
Rafał Borcz
38c00ddab5
Fix task description color crash (#2475) 2024-03-11 11:44:59 +01:00
Rafał Borcz
c72cc39920
Separate strings from array to avoid duplications (#2473) 2024-03-09 21:01:58 +01:00
Rafał Borcz
4ef9fb1f28
Update preferences strings (#2472) 2024-03-09 10:05:12 +01:00
Rafał Borcz
5dd5697f65
Remove firebase disable flag (#2471) 2024-03-09 10:03:36 +01:00
dependabot[bot]
a7c2009e49
Bump com.google.android.gms:play-services-ads from 22.6.0 to 23.0.0 (#2469) 2024-03-08 20:16:03 +00:00
dependabot[bot]
a71ef4a4b2
Bump com.google.firebase:firebase-bom from 32.7.3 to 32.7.4 (#2467) 2024-03-08 20:14:33 +00:00
dependabot[bot]
30413086fc
Bump kotlin_version from 1.9.22 to 1.9.23 (#2466) 2024-03-08 20:13:58 +00:00
Rafał Borcz
98ddf97855
Update gradle wrapper to 8.6 (#2468) 2024-03-08 20:57:26 +01:00
Michael
8f5a210ec7
Add attendance calculator (#1597)
---------

Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
Co-authored-by: Faierbel <RafalBO99@outlook.com>
2024-03-08 20:36:43 +01:00
Mikołaj Pich
ce09b07cfd
Merge branch 'release/2.5.1' into develop 2024-03-03 11:19:48 +01:00
sadorowo
0264682d0d setup APK build action 2024-03-01 09:18:08 +01:00
sadorowo
c395abb7e4 sync with original repository 2024-03-01 09:13:18 +01:00
franek
dfd5e10fbf
Delete Icon 2024-03-01 09:08:57 +01:00
franek
130f6164c4
add: gradle auto-build 2024-02-29 15:57:28 +01:00
sadorowo
b2191ab21b update: README 2024-02-29 10:41:24 +01:00
sadorowo
eb08a2cddb add: allow selecting grades to hide 2024-02-29 10:40:33 +01:00
franek
eb63e5eb04
remove: attendance always 100% [SK] 2024-02-29 09:46:04 +01:00
franek
7a8ab6fb52
remove: attendance always 100% [CZ] 2024-02-29 09:45:51 +01:00
franek
0eadf36222
remove: attendance always 100% [DE] 2024-02-29 09:45:40 +01:00
franek
85e3bb4d17
remove: attendance always 100% 2024-02-29 09:45:24 +01:00
sadorowo
84969e6f9b Merge branch 'develop' of https://github.com/sadorowo/wulkanowy-mod into develop 2024-02-29 09:42:29 +01:00
sadorowo
d39998000b fix: remove WIP code 2024-02-29 09:42:24 +01:00
franek
b6cbd0523b
Merge branch 'wulkanowy:develop' into develop 2024-02-29 09:40:58 +01:00
sadorowo
4d74c252c8 add: custom attendance percentage 2024-02-29 09:40:29 +01:00
sadorowo
c1687f5856 sync fork with v2.4.2 2024-02-25 20:14:04 +01:00
sadorowo
f7b25139c0 update: README 2024-01-04 09:41:22 +01:00
sadorowo
6e751c83f6 fix: bad attendance always visible 2024-01-03 20:23:12 +01:00
sadorowo
43a01e4e04 fix: always empty notes 2024-01-03 19:41:05 +01:00
sadorowo
ee906d02ae Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt
2024-01-03 19:25:38 +01:00
sadorowo
83cf39a28c fix: double attendance + add: fake 100% attendance 2024-01-03 19:24:53 +01:00
sador
3c5c50f84d
Merge branch 'develop' into develop 2024-01-02 22:01:41 +01:00
sadorowo
d0d3b11662 add: hidden settings. almost full customization. 2024-01-02 20:31:36 +01:00
sadorowo
d3ff6682ca update build tools 2023-12-30 20:27:38 +01:00
sador
a571dabb71
Merge branch 'wulkanowy:develop' into develop 2023-12-14 18:17:32 +01:00
sadorowo
5c27edbe3c
Merge branch 'wulkanowy:develop' into develop 2023-12-01 13:38:57 +01:00
sadorowo
598edaadb7 completely hide notes & exclude META-INF/* 2023-11-06 21:05:31 +01:00
193 changed files with 6235 additions and 1595 deletions

View File

@ -162,7 +162,7 @@ jobs:
openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks
- run: - run:
name: Publish release name: Publish release
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PdisablePreDex
workflows: workflows:
version: 2 version: 2

View File

@ -1,7 +1,7 @@
[*] [*]
charset=utf-8 charset=utf-8
end_of_line=lf end_of_line=lf
insert_final_newline=true insert_final_newline=Advanced
indent_style=space indent_style=space
indent_size=4 indent_size=4

18
.gitea/release.yml Normal file
View File

@ -0,0 +1,18 @@
changelog:
exclude:
labels:
- "release ignore"
categories:
- title: breaking changes
labels:
- major
- title: new features
labels:
- minor
- fr:approved
- title: translation updates
labels:
- translation
- title: features
labels:
- "*"

View File

@ -0,0 +1,84 @@
name: Generate APK
env:
main_project_module: app
on:
pull_request:
types:
- closed
jobs:
build:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Set current date as env variable
run: echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
- name: Set repository name as env variable
run: echo "repository_name=$(echo '${{ gitea.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Set up Go environment
uses: actions/setup-go@v3
with:
go-version: '1.22'
- name: Get hash of Gradle files
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: get-hash
with:
patterns: |-
**/*.gradle*
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ steps.get-hash.outputs.hash }}
- name: Get app version
id: get_version
run: echo "VERSION_NAME=$(grep -m1 "versionName" app/build.gradle | awk '{print $2}' | tr -d \'\'\"\')" >> $GITHUB_ENV
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Upload artifacts
uses: actions/upload-artifact@v3 # not v4 because of GHES
with:
name: wulkanowy_mod_debug_builds
path: |
app/build/outputs/**/*-debug.apk
- name: Create release
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch'
with:
files: |
app/build/outputs/**/*-debug.apk
name: Release ${{ env.VERSION_NAME }} (${{ env.date_today }})
tag_name: v${{ env.VERSION_NAME }}

View File

@ -1,79 +0,0 @@
name: Deploy release
on:
release:
types: [ created ]
jobs:
deploy-google-play:
name: Google Play
runs-on: ubuntu-latest
timeout-minutes: 10
environment: google-play
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Decrypt keys
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}
ADMOB_PROJECT_ID: ${{ secrets.ADMOB_PROJECT_ID }}
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }}
DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }}
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
deploy-app-gallery:
name: AppGallery
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-gallery
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Decrypt keys
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Prepare credentials
env:
AGC_CREDENTIALS: ${{ secrets.AGC_CREDENTIALS }}
run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json
- name: Build and publish HMS version
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

View File

@ -1,146 +0,0 @@
name: Deploy DEV
on:
push:
# branches: [ develop ]
branches: [ '!*' ]
pull_request_target:
# branches: [ develop ]
branches: [ '!*' ]
workflow_dispatch:
jobs:
deploy-appcenter:
name: App Center
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-center
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Set run number with offset
env:
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
- name: Prepare build configuration
run: |
sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json
sed -i -e '/versionNameSuffix/d' app/build.gradle
- name: Add signing config
run: |
cat >> app/build.gradle <<EOF
android.signingConfigs.debug {
storeFile file("bitrise.jks")
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
keyAlias System.getenv("BITRISE_KEY_ALIAS")
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
}
EOF
- name: Decrypt keys
env:
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
- name: Bump version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle
versionCode: ${{ env.RUN_NUMBER }}
versionName: ${{ env.RUN_NUMBER }}-${{ github.head_ref }}
- name: Build apk
env:
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assembleFdroidDebug --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v3
with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
- name: Deploy to app center
uses: wzieba/AppCenter-Github-Action@v1
with:
appName: wulkanowy/wulkanowy
token: ${{ secrets.APP_CENTER_TOKEN }}
group: Testers
file: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
notifyTesters: true
debug: true
deploy-app-distribution:
name: App Distribution
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-distribution
if: github.event_name != 'pull_request_target'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Set run number with offset
env:
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
- name: Add signing config
run: |
cat >> app/build.gradle <<EOF
android.signingConfigs.debug {
storeFile file("bitrise.jks")
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
keyAlias System.getenv("BITRISE_KEY_ALIAS")
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
}
EOF
- name: Decrypt keys
env:
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
BITRISE_SERVICES_ENCRYPT_KEY: ${{ secrets.BITRISE_SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$BITRISE_SERVICES_ENCRYPT_KEY ./app/src/debug/google-services.json.gpg
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
- name: Bump version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle
versionCode: ${{ env.RUN_NUMBER }}
versionName: ${{ env.RUN_NUMBER }}
- name: Build apk
env:
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v3
with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk
path: app/build/outputs/apk/play/debug/app-play-debug.apk
- name: Deploy to app distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: discord
file: app/build/outputs/apk/play/debug/app-play-debug.apk

View File

@ -1,90 +0,0 @@
name: Tests
on:
push:
branches:
- master
- develop
- 'hotfix/**'
tags: [ '*' ]
pull_request:
jobs:
tests-fdroid:
name: F-Droid
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testFdroidDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit
tests-play:
name: Play
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testPlayDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit
tests-hms:
name: HMS
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testHmsDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit

12
.gitignore vendored
View File

@ -117,12 +117,14 @@ Thumbs.db
*.ear *.ear
### AndroidStudio Patch ### ### AndroidStudio Patch ###
!/gradle/wrapper/gradle-wrapper.jar !/gradle/wrapper/gradle-wrapper.jar
.idea/jarRepositories.xml .idea/jarRepositories.xml
### Services config files
agconnect-services.json
agconnect-credentials.json
google-services.json
!app/google-services.json
app/src/release/agconnect-services.json
app/src/release/agconnect-credentials.json .idea/appInsightsSettings.xml
.idea/deploymentTargetDropDown.xml
.idea/kotlinc.xml

View File

@ -61,7 +61,7 @@ script:
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg; gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
./gradlew publishPlayRelease -PenableFirebase --stacktrace; ./gradlew publishPlayRelease --stacktrace;
fi fi
after_success: after_success:

View File

@ -1,73 +1,33 @@
Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md) Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy # Wulkanowy MOD
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) ## Funkce:
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) * skrýt známky
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) * Skrýt jednotlivé záznamy o docházce.
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) * Skrýt komentáře.
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) * falešná docházka %
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče Chcete-li se dostat na skrytý panel:
1. Přejděte na kartu „Další“.
2. Přejděte na panel „Nastavení“.
3. Přejděte na panel „O aplikaci“.
4. Klikněte 5x na logo aplikace
5. Přejděte na domovskou obrazovku
6. Přejděte do nastavení
7. Zadejte „tajná nastavení“
# Instalace
## Funkce | Název souboru | Přizpůsobeno |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
* přihlášení pomocí emailu a hesla Stáhněte si vybranou verzi z [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
* funkce z webové stránky deníku: Doporučujeme stáhnout nejnovější dostupnou verzi.
* známky
* statistiky známek
* frekvence
* procento frekvence
* zkoušky
* plán lekce
* dokončené lekce
* zprávy
* domácí úkoly
* poznámky
* šťastné číslo
* další lekce
* školní setkání
* informace o žáku a škole
* výpočet průměru nezávisle na preferencích školy
* upozornění, např. o nových známkách
* podpora více účtů s možností přejmenování žáků
* tmavý a černý (AMOLED) motiv
* offline režim
* volitelné reklamy na podporu projektu
## Stáhnout # O projektu Wulkanowy
Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGallery Chcete si přečíst více o projektu Wulkanowy? [Klikněte sem](https://github.com/wulkanowy/wulkanowy)
[<img src="https://play.google.com/intl/cs-CZ/badges/images/generic/cs_badge_web_generic.png"
alt="Nyní na Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Stáhnout s F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="https://i.imgur.com/baTGiDP.png"
alt="Objevuj v AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání
## Postaveno s pomocí
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Spolupráce
Přispějte do projektu vytvořením PR nebo odesláním issue na GitHub.
Pro zájemce o překlad aplikace do různých jazyků poskytujeme Crowdin:
https://crowdin.com/project/wulkanowy2
## Licence
Tento projekt je licencován pod licencí Apache License 2.0 - podrobnosti v souboru [LICENSE](LICENSE)

View File

@ -1,73 +1,33 @@
[Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md) [Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy # Wulkanowy MOD
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) ## Funktionen:
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) * Noten ausblenden
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) * Individuelle Anwesenheitslisten ausblenden.
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) * Kommentare ausblenden.
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) * Anwesenheit fälschen %
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern So gelangen Sie zum ausgeblendeten Bereich:
1. Gehen Sie zur Registerkarte „Mehr“.
2. Gehen Sie zum Bereich „Einstellungen“.
3. Gehen Sie zum Bereich „Über die Anwendung“.
4. Klicken Sie fünfmal auf das Anwendungslogo
5. Gehen Sie zum Startbildschirm
6. Gehen Sie zu den Einstellungen
7. Geben Sie „Geheime Einstellungen“ ein
# Installation
## Merkmale | Dateiname | Angepasst an |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
* Einloggen mit E-Mail und Passwort Laden Sie die ausgewählte Version von [hier](https://git.sador.me/sadorowo/wulkanowy-mod/releases) herunter.
* Funktionen von der Registerwebsite: Wir empfehlen, die neueste verfügbare Version herunterzuladen.
* Noten
* Notenstatistik
* Anwesenheit
* Prozentsatz der Anwesenheit
* Prüfungen
* Stundenplan
* abgeschlossene Unterrichtsstunden
* Nachrichten
* Hausaufgaben
* Anmerkungen
* Glückszahl
* Zusätzliche Lektionen
* Schulkonferenzen
* Schüler- und Schulinformationen
* Berechnung des Durchschnitts unabhängig von den Präferenzen der Schule
* Benachrichtigungen, z. B. über eine neue Note
* Unterstützung für mehrere Konten mit der Möglichkeit, den Namen des Schülers zu ändern
* dunkles und schwarzes (AMOLED) Thema
* Offline-Modus
* optionale Werbungen, die es uns ermöglichen das Projekt zu unterstützen
## Herunterladen # Über das Wulkanowy-Projekt
Die aktuelle Version können Sie von der Google Play, F-Droid oder Huawei AppGallery store herunterladen Möchten Sie mehr über das Wulkanowy-Projekt lesen? [Hier klicken](https://github.com/wulkanowy/wulkanowy)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Explore it on AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Sie können auch eine [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) die beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden
## Gebaut mit
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Beitragen
Bitte tragen Sie zum Projekt bei, indem Sie entweder eine PR erstellen oder ein Issue auf GitHub einreichen.
Für Personen, die daran interessiert sind, die Anwendung in verschiedene Sprachen zu übersetzen, bieten wir Crowdin
https://crowdin.com/project/wulkanowy2
## Lizenz
Dieses Projekt ist unter der Apache License 2.0 lizenziert - siehe die [LIZENZ](LICENSE) Datei für Details

View File

@ -1,73 +1,33 @@
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md) [Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy # Wulkanowy MOD
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) ## Functions:
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) * hide grades
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) * hide individual attendance entries
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) * hide comments
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) * fake attendance %.
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Unofficial android VULCAN UONET+ register client for both students and their parents To get to the hidden panel:
1. Go to the "More" tab
2. Go to the "Settings" panel
3. Go to the "About application" panel
4. Click on the application logo 5 times
5. Go to the home screen
6. Go to settings
7. Enter "secret settings"
# Installation
## Features | File name | Adapted to |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
* logging in using the email and password Download application from [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
* functions from the register website: We recommend downloading the latest available version.
* grades
* grade statistics
* attendance
* percentage of attendance
* exams
* timetable
* completed lessons
* messages
* homework
* notes
* lucky number
* additional lessons
* school conferences
* student and school information
* calculation of the average independently of school's preferences
* notifications, e.g. about a new grade
* support for multiple accounts with the ability to rename students
* dark and black (AMOLED) theme
* offline mode
* optional ads which allow to support the project
## Download # About the Wulkanowy project
You can download the current version from the Google Play, F-Droid or Huawei AppGallery store Want to read more about the Wulkanowy project? [Click here](https://github.com/wulkanowy/wulkanowy)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Explore it on AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
You can also download a [development version](https://wulkanowy.github.io/#download) that includes new features being prepared for the next release
## Built With
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Contributing
Please contribute to the project either by creating a PR or submitting an issue on GitHub.
For people interested in translating the application into different languages, we provide Crowdin
https://crowdin.com/project/wulkanowy2
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details

View File

@ -1,74 +1,33 @@
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md) [Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md)
# Wulkanowy # Wulkanowy MOD
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) ## Funkcje:
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) * ukryj oceny
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) * ukryj poszczególne wpisy frekwencji
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) * ukryj uwagi
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) * sfałszuj % frekwencji
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica Aby dostać się do ukrytego panelu:
1. Przejdź do karty "Więcej"
2. Przejdź do panelu "Ustawienia"
3. Przejdź do panelu "O aplikacji"
4. Kliknij 5 razy w logo aplikacji
5. Przejdź na ekran główny
6. Wejdź w ustawienia
7. Wejdź w "sekretne ustawienia"
# Instalacja
## Funkcje | Nazwa pliku | Przystosowana do |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Sklep Play |
* logowanie za pomocą e-maila i hasła Pobierz wybraną wersję z [wydań](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
* funkcje ze strony internetowej dziennika: Zalecamy pobranie najnowszej dostępnej wersji.
* oceny
* statystyki ocen
* frekwencja
* procent frekwencji
* sprawdziany
* plan lekcji
* lekcje zrealizowane
* wiadomości
* zadania domowe
* uwagi
* szczęśliwy numerek
* dodatkowe lekcje
* zebrania w szkole
* informacje o uczniu i szkole
* obliczanie średniej niezależnie od preferencji szkoły
* powiadomienia np. o nowej ocenie
* obsługa wielu kont wraz z możliwością zmiany nazwy ucznia
* ciemny i czarny (AMOLED) motyw
* tryb offline
* opcjonalne reklamy umożliwiające wsparcie projektu
## Pobierz # O projekcie Wulkanowy
Aktualną wersję możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery Chcesz poczytać więcej o projekcie Wulkanowy? [Kliknij tutaj](https://github.com/wulkanowy/wulkanowy)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Pobierz z Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Pobierz z F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Odkrywaj w AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#download), która zawiera nowe funkcje przygotowywane do następnego wydania
## Zbudowana za pomocą
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Współpraca
Wnieś swój wkład w projekt, tworząc PR lub wysyłając issue na GitHub.
Dla osób zainteresowanych tłumaczeniem aplikacji na różne języki udostępniamy Crowdina
https://crowdin.com/project/wulkanowy2
## Licencja
Ten projekt udostępniany jest na licencji Apache License 2.0 - szczegóły w pliku [LICENSE](LICENSE)

View File

@ -1,73 +1,33 @@
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia [Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia
# Wulkanowy # Wulkanowy MOD
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) ## Funkcie:
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy) * skryť známky
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr) * Skryť individuálne záznamy o dochádzke.
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/) * Skryť komentáre.
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases) * falošná dochádzka %
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov Ak chcete prejsť na skrytý panel:
1. Prejdite na kartu „Viac“.
2. Prejdite na panel „Nastavenia“.
3. Prejdite na panel „O aplikácii“.
4. Kliknite 5-krát na logo aplikácie
5. Prejdite na domovskú obrazovku
6. Prejdite do nastavení
7. Zadajte „tajné nastavenia“
# Inštalácia
## Funkcie | Názov súboru | Prispôsobené |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
* prihlásenie pomocou emailu a hesla Stiahnite si vybranú verziu z [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
* funkcie z webovej stránky denníka: Odporúčame stiahnuť najnovšiu dostupnú verziu.
* známky
* štatistiky známok
* frekvencia
* percento frekvencie
* skúšky
* plán lekcie
* dokončené lekcie
* správy
* domáce úlohy
* poznámky
* šťastné číslo
* ďalšie lekcie
* školské stretnutie
* informácie o žiakovi a škole
* výpočet priemeru nezávisle od preferencií školy
* upozornenia, napr. o nových známkach
* podpora viacerých účtov s možnosťou premenovania žiakov
* tmavý a čierny (AMOLED) motív
* offline režim
* voliteľné reklamy na podporu projektu
## Stiahnuť # O projekte Wulkanowy
Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGallery Chcete si prečítať viac o projekte Wulkanowy? [Kliknite sem](https://github.com/wulkanowy/wulkanowy)
[<img src="https://play.google.com/intl/sk/badges/images/generic/sk_badge_web_generic.png"
alt="Nyní na Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Stiahnuť s F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="https://i.imgur.com/sX8UyAw.png"
alt="Objavíte v AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie
## Postavené s pomocou
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Spolupráca
Prispejte do projektu vytvorením PR alebo odoslaním issue na GitHub.
Pre záujemcov o preklad aplikácie do rôznych jazykov poskytujeme Crowdin:
https://crowdin.com/project/wulkanowy2
## Licencia
Tento projekt je licencovaný pod licenciou Apache License 2.0 - podrobnosti v súbore [LICENSE](LICENSE)

View File

@ -27,15 +27,12 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 157 versionCode 173
versionName "2.5.8" versionName "2.6.13"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
manifestPlaceholders = [ manifestPlaceholders = [admob_project_id: ""]
firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: ""
]
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
@ -65,8 +62,8 @@ android {
release { release {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release // signingConfig signingConfigs.release
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
} }
@ -76,7 +73,6 @@ android {
resValue "string", "app_name", "Wulkanowy DEV" resValue "string", "app_name", "Wulkanowy DEV"
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionNameSuffix "-dev" versionNameSuffix "-dev"
ext.enableCrashlytics = project.hasProperty("enableFirebase")
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
} }
@ -164,8 +160,8 @@ play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d userFraction = 0.1d
updatePriority = 4 updatePriority = 2
enabled.set(false) enabled.set(false)
} }
@ -191,27 +187,29 @@ ext {
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.10" mockk = "1.13.10"
coroutines = "1.8.0" coroutines = "1.8.1"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.5.8' implementation 'io.github.wulkanowy:sdk:2.6.11'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.8.2" implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.6.2" implementation "androidx.fragment:fragment-ktx:1.7.0"
implementation "androidx.annotation:annotation:1.7.1" implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2" implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.viewpager2:viewpager2:1.1.0-rc01"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
@ -237,7 +235,7 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0" implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
@ -250,9 +248,9 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0' implementation 'org.apache.commons:commons-text:1.12.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.3') playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
@ -278,7 +276,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.11.1' testImplementation 'org.robolectric:robolectric:4.12.1'
testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0" testImplementation "androidx.test:core:1.5.0"

View File

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

View File

@ -1,7 +1,8 @@
#!/bin/bash - #!/bin/bash -
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
if [[ "${#content}" -gt 500 ]]; then content2=echo "$content" | dos2unix
if [[ "${#content2}" -gt 500 ]]; then
echo >&2 "Release notes content has reached the limit of 500 characters" echo >&2 "Release notes content has reached the limit of 500 characters"
exit 1 exit 1
fi fi

View File

@ -1,92 +0,0 @@
{
"agcgw": {
"backurl": "connect-dre.hispace.hicloud.com",
"url": "connect-dre.dbankcloud.cn",
"websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
"websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
},
"agcgw_all": {
"CN": "connect-drcn.dbankcloud.cn",
"CN_back": "connect-drcn.hispace.hicloud.com",
"DE": "connect-dre.dbankcloud.cn",
"DE_back": "connect-dre.hispace.hicloud.com",
"RU": "connect-drru.hispace.dbankcloud.ru",
"RU_back": "connect-drru.hispace.dbankcloud.cn",
"SG": "connect-dra.dbankcloud.cn",
"SG_back": "connect-dra.hispace.hicloud.com"
},
"websocketgw_all": {
"CN": "connect-ws-drcn.hispace.dbankcloud.cn",
"CN_back": "connect-ws-drcn.hispace.dbankcloud.com",
"DE": "connect-ws-dre.hispace.dbankcloud.cn",
"DE_back": "connect-ws-dre.hispace.dbankcloud.com",
"RU": "connect-ws-drru.hispace.dbankcloud.ru",
"RU_back": "connect-ws-drru.hispace.dbankcloud.cn",
"SG": "connect-ws-dra.hispace.dbankcloud.cn",
"SG_back": "connect-ws-dra.hispace.dbankcloud.com"
},
"client": {
"cp_id": "890048000024105546",
"product_id": "736430079244736562",
"client_id": "514530959291319360",
"client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B",
"project_id": "736430079244736562",
"app_id": "106552551",
"api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS",
"package_name": "io.github.wulkanowy.dev"
},
"oauth_client": {
"client_id": "106552551",
"client_type": 1
},
"app_info": {
"app_id": "106552551",
"package_name": "io.github.wulkanowy.dev"
},
"service": {
"analytics": {
"collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
"collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
"collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
"resource_id": "p1",
"channel_id": ""
},
"search":{
"url":"https://search-dre.cloud.huawei.com"
},
"cloudstorage": {
"storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
"storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
"storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
"storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu",
"storage_url_de": "https://ops-dre.agcstorage.link",
"storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn",
"storage_url_sg": "https://ops-dra.agcstorage.link",
"storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn",
"storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn"
},
"ml": {
"mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
}
},
"region": "DE",
"configuration_version": "3.0",
"appInfos": [
{
"package_name": "io.github.wulkanowy.dev",
"client": {
"app_id": "106552551"
},
"app_info": {
"package_name": "io.github.wulkanowy.dev",
"app_id": "106552551"
},
"oauth_client": {
"client_type": 1,
"client_id": "106552551"
}
}
]
}

View File

@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly"> android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -42,16 +44,16 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/WulkanowyTheme" android:theme="@style/WulkanowyTheme"
android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute"> tools:ignore="DataExtractionRules,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:exported="true" android:exported="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen" android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity"> tools:ignore="DiscouragedApi,LockedOrientationActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -155,33 +157,9 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:enabled="${firebase_enabled}"
android:exported="false"
tools:ignore="MissingClass" />
<meta-data <meta-data
android:name="install_channel" android:name="install_channel"
android:value="${install_channel}" /> android:value="${install_channel}" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_all" /> android:resource="@drawable/ic_stat_all" />

View File

@ -13,8 +13,8 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -71,7 +71,7 @@ internal class DataModule {
okHttpClient: OkHttpClient, okHttpClient: OkHttpClient,
json: Json, json: Json,
appInfo: AppInfo appInfo: AppInfo
): AdminMessageService = Retrofit.Builder() ): WulkanowyService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl) .baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

View File

@ -1,11 +1,17 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -14,16 +20,39 @@ import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
sealed class Resource<T> { sealed interface Resource<out T> {
/**
open class Loading<T> : Resource<T>() * The initial value of a resource flow. Indicates no data that is currently available to be shown,
* however with the expectation that the state will transition to another one soon.
*/
open class Loading<T> : Resource<T>
/**
* A semi-loading state with some data available to be displayed (usually cached data loaded from
* the database). Still not the target state and it's expected to transition into another one soon.
*/
data class Intermediate<T>(val data: T) : Loading<T>() data class Intermediate<T>(val data: T) : Loading<T>()
data class Success<T>(val data: T) : Resource<T>() /**
* The happy-path target state. Data can either be:
* - loaded from the database - while it may seem like this case is already handled by the
* Intermediate state, the difference here is semantic. Cached data is returned as Intermediate
* when there's a API request in progress (or soon expected to be), however when there is no
* intention of immediately querying the API, the cached data is returned as a Success.
* - fetched from the API.
*/
data class Success<T>(val data: T) : Resource<T>
data class Error<T>(val error: Throwable) : Resource<T>() /**
* Something bad happened and we were unable to get the requested data. This can be caused by
* a database error, a network error, or really just any other error. Upon receiving this state
* the UI can either: display a full screen error, or, when it has received any data previously,
* display a snack bar informing of the problem.
*/
data class Error<T>(val error: Throwable) : Resource<T>
} }
val <T> Resource<T>.dataOrNull: T? val <T> Resource<T>.dataOrNull: T?
@ -64,6 +93,22 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
is Resource.Error -> Resource.Error(this.error) is Resource.Error -> Resource.Error(this.error)
} }
/**
* Injects another flow into this flow's resource data.
*/
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
flow: Flow<T2>,
crossinline block: suspend (T1, T2) -> R
): Flow<Resource<R>> =
combine(flow) { resource, inject ->
when (resource) {
is Resource.Success -> Resource.Success(block(resource.data, inject))
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
is Resource.Loading -> Resource.Loading()
is Resource.Error -> Resource.Error(resource.error)
}
}
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach { fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
val description = when (it) { val description = when (it) {
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else "" is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
@ -74,8 +119,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
Timber.i("$name: $description") Timber.i("$name: $description")
} }
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map { inline fun <T, U> Flow<Resource<T>>.mapResourceData(crossinline block: suspend (T) -> U) = map {
it.mapData(block) when (it) {
is Resource.Success -> Resource.Success(block(it.data))
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
is Resource.Loading -> Resource.Loading()
is Resource.Error -> Resource.Error(it.error)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
) = flatMapLatest {
when (it) {
is Resource.Success -> block(it.data)
is Resource.Intermediate -> block(it.data).map { newRes ->
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
else newRes
}
is Resource.Loading -> flowOf(Resource.Loading())
is Resource.Error -> flowOf(Resource.Error(it.error))
}
} }
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach { fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
@ -105,13 +171,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
} }
} }
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach { fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
if (it is Resource.Error) { if (it is Resource.Error) {
block(it.error) block(it.error)
} }
} }
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach { fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
if (it !is Resource.Loading) { if (it !is Resource.Loading) {
block() block()
} }
@ -121,70 +187,99 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect() suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
inline fun <ResultType, RequestType> networkBoundResource( // Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
mutex: Mutex = Mutex(), // use `debounceIntermediates` to alleviate this behavior.
showSavedOnLoading: Boolean = true, inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
crossinline isResultEmpty: (ResultType) -> Boolean, combine(flows) { items ->
crossinline query: () -> Flow<ResultType>, var isIntermediate = false
crossinline fetch: suspend (ResultType) -> RequestType, val data = mutableListOf<T>()
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, for (item in items) {
crossinline onFetchFailed: (Throwable) -> Unit = { }, when (item) {
crossinline shouldFetch: (ResultType) -> Boolean = { true }, is Resource.Success -> data.add(item.data)
crossinline filterResult: (ResultType) -> ResultType = { it } is Resource.Intermediate -> {
) = flow { isIntermediate = true
emit(Resource.Loading()) data.add(item.data)
}
val data = query().first() is Resource.Loading -> return@combine Resource.Loading()
emitAll(if (shouldFetch(data)) { is Resource.Error -> continue
val filteredResult = filterResult(data) }
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
emit(Resource.Intermediate(filteredResult))
} }
if (data.isEmpty()) {
try { // All items have to be errors for this to happen, so just return the first one.
val newData = fetch(data) // mapData is functionally useless and exists only to satisfy the type checker
mutex.withLock { saveFetchResult(query().first(), newData) } items.first().mapData { listOf(it) }
query().map { Resource.Success(filterResult(it)) } } else if (isIntermediate) {
} catch (throwable: Throwable) { Resource.Intermediate(data)
onFetchFailed(throwable) } else {
flowOf(Resource.Error(throwable)) Resource.Success(data)
}
}
@OptIn(FlowPreview::class)
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
var wasIntermediate = false
emitAll(this@debounceIntermediates.debounce {
if (it is Resource.Intermediate) {
if (!wasIntermediate) {
wasIntermediate = true
Duration.ZERO
} else {
timeout
}
} else {
wasIntermediate = false
Duration.ZERO
} }
} else {
query().map { Resource.Success(filterResult(it)) }
}) })
} }
inline fun <OutputType, ApiType> networkBoundResource(
mutex: Mutex = Mutex(),
crossinline isResultEmpty: (OutputType) -> Boolean,
crossinline query: () -> Flow<OutputType>,
crossinline fetch: suspend () -> ApiType,
crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit,
crossinline shouldFetch: (OutputType) -> Boolean = { true },
crossinline filterResult: (OutputType) -> OutputType = { it }
) = networkBoundResource(
mutex = mutex,
isResultEmpty = isResultEmpty,
query = query,
fetch = fetch,
saveFetchResult = saveFetchResult,
shouldFetch = shouldFetch,
mapResult = filterResult
)
@JvmName("networkBoundResourceWithMap") @JvmName("networkBoundResourceWithMap")
inline fun <ResultType, RequestType, T> networkBoundResource( inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
mutex: Mutex = Mutex(), mutex: Mutex = Mutex(),
showSavedOnLoading: Boolean = true, crossinline isResultEmpty: (OutputType) -> Boolean,
crossinline isResultEmpty: (T) -> Boolean, crossinline query: () -> Flow<DatabaseType>,
crossinline query: () -> Flow<ResultType>, crossinline fetch: suspend () -> ApiType,
crossinline fetch: suspend (ResultType) -> RequestType, crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline mapResult: (DatabaseType) -> OutputType,
crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline mapResult: (ResultType) -> T,
) = flow { ) = flow {
emit(Resource.Loading()) emit(Resource.Loading())
val data = query().first() val data = query().first()
emitAll(if (shouldFetch(data)) { if (shouldFetch(data)) {
val mappedResult = mapResult(data) emit(Resource.Intermediate(data))
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
emit(Resource.Intermediate(mappedResult))
}
try { try {
val newData = fetch(data) val newData = fetch()
mutex.withLock { saveFetchResult(query().first(), newData) } mutex.withLock { saveFetchResult(query().first(), newData) }
query().map { Resource.Success(mapResult(it)) }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
onFetchFailed(throwable) emit(Resource.Error(throwable))
flowOf(Resource.Error(throwable)) return@flow
} }
} else { }
query().map { Resource.Success(mapResult(it)) }
}) emitAll(query().map { Resource.Success(it) })
} }
.mapResourceData { mapResult(it) }
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }

View File

@ -1,13 +1,21 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import android.content.Context
import android.os.Build
import androidx.javascriptengine.JavaScriptSandbox
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
import io.github.wulkanowy.utils.RemoteConfigHelper import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
@ -16,18 +24,24 @@ import javax.inject.Singleton
@Singleton @Singleton
class WulkanowySdkFactory @Inject constructor( class WulkanowySdkFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val chuckerInterceptor: ChuckerInterceptor, private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper, private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy, private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val wulkanowyRepository: WulkanowyRepository,
) { ) {
private val eduOneMutex = Mutex() private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>() private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sandbox: ListenableFuture<JavaScriptSandbox>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
JavaScriptSandbox.createConnectedInstanceAsync(context)
else null
private val sdk = Sdk().apply { private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE androidVersion = Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL buildTag = Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) } setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy) setAdditionalCookieManager(webkitCookieManagerProxy)
@ -36,14 +50,46 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true) addInterceptor(chuckerInterceptor, network = true)
} }
fun create() = sdk fun createBase() = sdk
suspend fun create(): Sdk {
val mapping = wulkanowyRepository.getMapping()
return createBase().apply {
if (mapping != null) {
endpointsMapping = mapping.endpoints
vTokenMapping = mapping.vTokens
vHeaders = mapping.vHeaders
vParamsEvaluation = createIsolate()
}
}
}
private suspend fun createIsolate(): suspend () -> EvaluateHandler {
return {
val isolate = sandbox?.await()?.createIsolate()
object : EvaluateHandler {
override suspend fun evaluate(code: String): String? {
return isolate?.evaluateJavaScriptAsync(code)?.await()
}
override fun close() {
isolate?.close()
}
}
}
}
suspend fun create(student: Student, semester: Semester? = null): Sdk { suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student) val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne) return buildSdk(student, semester, overrideIsEduOne)
} }
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk { private suspend fun buildSdk(
student: Student,
semester: Semester?,
isStudentEduOne: Boolean
): Sdk {
return create().apply { return create().apply {
email = student.email email = student.email
password = student.password password = student.password

View File

@ -0,0 +1,20 @@
package io.github.wulkanowy.data.api.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Mapping(
@SerialName("endpoints")
val endpoints: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokens")
val vTokens: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokenScheme")
val vTokenScheme: Map<String, Map<String, String>> = emptyMap(),
@SerialName("vHeaders")
val vHeaders: Map<String, Map<String, Map<String, String>>> = emptyMap(),
)

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.data.api package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.pojos.IntegrityRequest import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent import io.github.wulkanowy.data.pojos.LoginEvent

View File

@ -1,12 +1,16 @@
package io.github.wulkanowy.data.api package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET import retrofit2.http.GET
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
interface AdminMessageService { interface WulkanowyService {
@GET("/v1.json") @GET("/v1.json")
suspend fun getAdminMessages(): List<AdminMessage> suspend fun getAdminMessages(): List<AdminMessage>
}
@GET("/mapping2.json")
suspend fun getMapping(): Mapping
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Query import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Singleton import javax.inject.Singleton

View File

@ -12,4 +12,8 @@ interface GradeDao : BaseDao<Grade> {
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId") @Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId")
fun loadAll(semesterId: Int, studentId: Int): Flow<List<Grade>> fun loadAll(semesterId: Int, studentId: Int): Flow<List<Grade>>
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId " +
"AND entry NOT IN(:censoredEntries)")
fun loadAllCensored(semesterId: Int, studentId: Int, censoredEntries: Array<String>): Flow<List<Grade>>
} }

View File

@ -9,7 +9,6 @@ import javax.inject.Singleton
@Singleton @Singleton
@Dao @Dao
interface NoteDao : BaseDao<Note> { interface NoteDao : BaseDao<Note> {
@Query("SELECT * FROM Notes WHERE student_id = :studentId") @Query("SELECT * FROM Notes WHERE student_id = :studentId")
fun loadAll(studentId: Int): Flow<List<Note>> fun loadAll(studentId: Int): Flow<List<Note>>
} }

View File

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

View File

@ -7,6 +7,6 @@ enum class AppTheme(val value: String) {
BLACK("black"); BLACK("black");
companion object { companion object {
fun getByValue(value: String) = values().find { it.value == value } ?: LIGHT fun getByValue(value: String) = entries.find { it.value == value } ?: LIGHT
} }
} }

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.data.enums
enum class AttendanceCalculatorSortingMode(private val value: String) {
ALPHABETIC("alphabetic"),
ATTENDANCE("attendance_percentage"),
LESSON_BALANCE("lesson_balance");
companion object {
fun getByValue(value: String) =
AttendanceCalculatorSortingMode.values()
.find { it.value == value } ?: ALPHABETIC
}
}

View File

@ -8,6 +8,6 @@ enum class GradeColorTheme(val value: String) : Serializable {
GRADE_COLOR("grade_color"); GRADE_COLOR("grade_color");
companion object { companion object {
fun getByValue(value: String) = values().find { it.value == value } ?: VULCAN fun getByValue(value: String) = entries.find { it.value == value } ?: VULCAN
} }
} }

View File

@ -6,6 +6,6 @@ enum class GradeExpandMode(val value: String) {
ALWAYS_EXPANDED("always"); ALWAYS_EXPANDED("always");
companion object { companion object {
fun getByValue(value: String) = values().find { it.value == value } ?: ONE fun getByValue(value: String) = entries.find { it.value == value } ?: ONE
} }
} }

View File

@ -6,6 +6,6 @@ enum class GradeSortingMode(val value: String) {
AVERAGE("average"); AVERAGE("average");
companion object { companion object {
fun getByValue(value: String) = values().find { it.value == value } ?: ALPHABETIC fun getByValue(value: String) = entries.find { it.value == value } ?: ALPHABETIC
} }
} }

View File

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

View File

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

View File

@ -6,6 +6,6 @@ enum class TimetableMode(val value: String) {
SMALL_OTHER_GROUP("small"); SMALL_OTHER_GROUP("small");
companion object { companion object {
fun getByValue(value: String) = values().find { it.value == value } ?: ONLY_CURRENT_GROUP fun getByValue(value: String) = entries.find { it.value == value } ?: ONLY_CURRENT_GROUP
} }
} }

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.pojos
data class AttendanceData(
val subjectName: String,
val lessonBalance: Int,
val presences: Int,
val absences: Int,
) {
val total: Int
get() = presences + absences
val presencePercentage: Double
get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100
}

View File

@ -1,33 +0,0 @@
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.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
) {
private val saveFetchResultMutex = Mutex()
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

@ -9,12 +9,15 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.pojo.Absent import io.github.wulkanowy.sdk.pojo.Absent
import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@ -28,12 +31,71 @@ class AttendanceRepository @Inject constructor(
private val timetableDb: TimetableDao, private val timetableDb: TimetableDao,
private val wulkanowySdkFactory: WulkanowySdkFactory, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val preferencesRepository: PreferencesRepository
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance" private val cacheKey = "attendance"
private fun filterAttendance(
hiddenAttendanceTiles: List<DashboardItem.HiddenAttendanceTile>,
attendanceItem: Attendance
): Boolean {
return when {
attendanceItem.absence && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_ABSENCE
) -> false
attendanceItem.absence && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_ABSENCE
) -> false
attendanceItem.lateness && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_LATENESS
) -> false
attendanceItem.lateness && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_LATENESS
) -> false
attendanceItem.exemption && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.EXEMPTION) -> false
attendanceItem.deleted && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.DELETED) -> false
attendanceItem.presence && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.PRESENT) -> false
else -> !hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.UNKNOWN)
}
}
private fun filterAttendance(
hiddenAttendanceTiles: List<DashboardItem.HiddenAttendanceTile>,
attendanceItem: SdkAttendance
): Boolean {
return when {
attendanceItem.absence && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_ABSENCE
) -> false
attendanceItem.absence && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_ABSENCE
) -> false
attendanceItem.lateness && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_LATENESS
) -> false
attendanceItem.lateness && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_LATENESS
) -> false
attendanceItem.exemption && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.EXEMPTION) -> false
attendanceItem.deleted && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.DELETED) -> false
attendanceItem.presence && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.PRESENT) -> false
else -> !hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.UNKNOWN)
}
}
fun getAttendance( fun getAttendance(
student: Student, student: Student,
semester: Semester, semester: Semester,
@ -51,14 +113,24 @@ class AttendanceRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isExpired
}, },
query = { query = {
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
attendanceDb
.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
.map {
it.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
}
}, },
fetch = { fetch = {
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
val lessons = timetableDb.load( val lessons = timetableDb.load(
semester.diaryId, semester.studentId, start.monday, end.sunday semester.diaryId, semester.studentId, start.monday, end.sunday
) )
wulkanowySdkFactory.create(student, semester) wulkanowySdkFactory.create(student, semester)
.getAttendance(start.monday, end.sunday) .getAttendance(start.monday, end.sunday)
.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
.mapToEntities(semester, lessons) .mapToEntities(semester, lessons)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
@ -79,7 +151,13 @@ class AttendanceRepository @Inject constructor(
start: LocalDate, start: LocalDate,
end: LocalDate end: LocalDate
): Flow<List<Attendance>> { ): Flow<List<Attendance>> {
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end) val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
return attendanceDb
.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
.map {
it.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
}
} }
suspend fun updateTimetable(timetable: List<Attendance>) { suspend fun updateTimetable(timetable: List<Attendance>) {

View File

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,10 +31,18 @@ class GradeRepository @Inject constructor(
private val gradeDescriptiveDb: GradeDescriptiveDao, private val gradeDescriptiveDb: GradeDescriptiveDao,
private val wulkanowySdkFactory: WulkanowySdkFactory, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val preferencesRepository: PreferencesRepository
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private fun loadGrades(semesterId: Int, studentId: Int): Flow<List<Grade>> {
val hiddenGrades = preferencesRepository.hiddenGrades
Timber.i("Load grades for semester $semesterId student $studentId")
return gradeDb.loadAllCensored(semesterId, studentId, hiddenGrades.toTypedArray())
}
fun getGrades( fun getGrades(
student: Student, student: Student,
semester: Semester, semester: Semester,
@ -61,11 +70,15 @@ class GradeRepository @Inject constructor(
} }
}, },
fetch = { fetch = {
val hiddenGrades = preferencesRepository.hiddenGrades
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester) val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
.getGrades(semester.semesterId) .getGrades(semester.semesterId)
val censoredDetails = details.filterNot { it.entry in hiddenGrades }
Triple( Triple(
details.mapToEntities(semester), censoredDetails.mapToEntities(semester),
summary.mapToEntities(semester), summary.mapToEntities(semester),
descriptive.mapToEntities(semester) descriptive.mapToEntities(semester)
) )
@ -157,13 +170,13 @@ class GradeRepository @Inject constructor(
} }
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> { fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { return loadGrades(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isRead } it.filter { grade -> !grade.isRead }
} }
} }
fun getGradesFromDatabase(semester: Semester): Flow<List<Grade>> { fun getGradesFromDatabase(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId) return loadGrades(semester.semesterId, semester.studentId)
} }
fun getGradesPredictedFromDatabase(semester: Semester): Flow<List<GradeSummary>> { fun getGradesPredictedFromDatabase(semester: Semester): Flow<List<GradeSummary>> {

View File

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

View File

@ -122,7 +122,7 @@ class MessageRepository @Inject constructor(
fetch = { fetch = {
wulkanowySdkFactory.create(student) wulkanowySdkFactory.create(student)
.getMessageDetails( .getMessageDetails(
messageKey = it!!.message.messageGlobalKey, messageKey = message.messageGlobalKey,
markAsRead = message.unread && markAsRead, markAsRead = message.unread && markAsRead,
) )
}, },

View File

@ -21,6 +21,7 @@ class NoteRepository @Inject constructor(
private val noteDb: NoteDao, private val noteDb: NoteDao,
private val wulkanowySdkFactory: WulkanowySdkFactory, private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val preferencesRepository: PreferencesRepository
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -39,12 +40,16 @@ class NoteRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed( val isExpired = refreshHelper.shouldBeRefreshed(
getRefreshKey(cacheKey, semester) getRefreshKey(cacheKey, semester)
) )
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isExpired
}, },
query = { noteDb.loadAll(student.studentId) }, query = { noteDb.loadAll(student.studentId) },
fetch = { fetch = {
val showNotes = preferencesRepository.showNotes
wulkanowySdkFactory.create(student, semester) wulkanowySdkFactory.create(student, semester)
.getNotes() .getNotes()
.filter { showNotes }
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->

View File

@ -7,12 +7,16 @@ import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer import com.fredporciuncula.flow.preferences.Serializer
import com.fredporciuncula.flow.preferences.map
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.enums.AppTheme import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
import io.github.wulkanowy.data.enums.TimetableGapsMode import io.github.wulkanowy.data.enums.TimetableGapsMode
import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
@ -22,6 +26,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber
import java.time.Instant import java.time.Instant
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -34,6 +39,7 @@ class PreferencesRepository @Inject constructor(
private val flowSharedPref: FlowSharedPreferences, private val flowSharedPref: FlowSharedPreferences,
private val json: Json, private val json: Json,
) { ) {
private val NO_ATTENDANCE_VALUE = -1.0
val isShowPresent: Boolean val isShowPresent: Boolean
get() = getBoolean( get() = getBoolean(
@ -41,6 +47,27 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_attendance_present R.bool.pref_default_attendance_present
) )
val targetAttendanceFlow: Flow<Int>
get() = flowSharedPref.getInt(
context.getString(R.string.pref_key_attendance_target),
context.resources.getInteger(R.integer.pref_default_attendance_target)
).asFlow()
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
get() = flowSharedPref.getString(
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
/**
* Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0).
*/
val attendanceCalculatorShowEmptySubjects: Flow<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects),
context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects)
).asFlow()
private val gradeAverageModePref: Preference<GradeAverageMode> private val gradeAverageModePref: Preference<GradeAverageMode>
get() = getObjectFlow( get() = getObjectFlow(
R.string.pref_key_grade_average_mode, R.string.pref_key_grade_average_mode,
@ -191,6 +218,12 @@ class PreferencesRepository @Inject constructor(
) )
) )
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
get() = getString(
R.string.pref_key_timetable_show_additional_lessons,
R.string.pref_default_timetable_show_additional_lessons
).let { ShowAdditionalLessonsMode.getByValue(it) }
val gradeSortingMode: GradeSortingMode val gradeSortingMode: GradeSortingMode
get() = GradeSortingMode.getByValue( get() = GradeSortingMode.getByValue(
getString( getString(
@ -272,6 +305,60 @@ class PreferencesRepository @Inject constructor(
selectedDashboardTilesPreference.set(filteredValue) selectedDashboardTilesPreference.set(filteredValue)
} }
var attendancePercentage: Double?
get() = attendancePercentagePreference.get().takeIf { it != NO_ATTENDANCE_VALUE }
set(value) = attendancePercentagePreference.set(value ?: NO_ATTENDANCE_VALUE)
var hiddenAttendanceItems: List<DashboardItem.HiddenAttendanceTile>
get() = hiddenAttendanceItemsPreference.get().toList()
set(value) = hiddenAttendanceItemsPreference.set(value.toSet())
var hiddenGrades: List<String>
get() = hiddenGradesPreference.get().toList()
set(value) = hiddenGradesPreference.set(value.toSet())
var showNotes: Boolean
get() = showNotesPreference.get()
set(value) = showNotesPreference.set(value)
var developerMode: Boolean
get() = developerModePreference.get()
set(value) = developerModePreference.set(value)
private val developerModePreference: Preference<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_developer_mode),
context.resources.getBoolean(R.bool.pref_default_developer_mode)
)
private val hiddenGradesPreference: Preference<Set<String>>
get() {
val defaultSet = context.resources.getStringArray(R.array.pref_default_hidden_grades).toSet()
val prefKey = "hidden_grades"
return flowSharedPref.getStringSet(prefKey, defaultSet)
}
private val showNotesPreference: Preference<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_show_notes),
context.resources.getBoolean(R.bool.pref_default_show_notes)
)
private val hiddenAttendanceItemsPreference: Preference<Set<DashboardItem.HiddenAttendanceTile>>
get() {
val defaultSet =
context.resources.getStringArray(R.array.pref_default_hidden_attendance_items).toSet()
val prefKey = "attendance_items"
return flowSharedPref
.getStringSet(prefKey, defaultSet)
.map(
mapper = { it -> it.map { DashboardItem.HiddenAttendanceTile.valueOf(it) }.toSet() },
reverse = { it -> it.map { it.name }.toSet() }
)
}
private val selectedDashboardTilesPreference: Preference<Set<String>> private val selectedDashboardTilesPreference: Preference<Set<String>>
get() { get() {
val defaultSet = val defaultSet =
@ -281,6 +368,19 @@ class PreferencesRepository @Inject constructor(
return flowSharedPref.getStringSet(prefKey, defaultSet) return flowSharedPref.getStringSet(prefKey, defaultSet)
} }
private val attendancePercentagePreference: Preference<Double>
get() {
val prefKey = context.getString(R.string.pref_key_attendance_percentage)
val defaultValue = context.resources.getString(R.string.pref_default_attendance_percentage)
return flowSharedPref
.getString(prefKey, defaultValue)
.map(
mapper = { it.toDoubleOrNull() ?: NO_ATTENDANCE_VALUE },
reverse = { it.toString() }
)
}
var dismissedAdminMessageIds: List<Int> var dismissedAdminMessageIds: List<Int>
get() = sharedPref.getStringSet(PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS, emptySet()) get() = sharedPref.getStringSet(PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS, emptySet())
.orEmpty() .orEmpty()
@ -346,6 +446,15 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
var mapping: Mapping?
get() {
val value = sharedPref.getString("mapping", null)
return value?.let { json.decodeFromString(it) }
}
set(value) = sharedPref.edit(commit = true) {
putString("mapping", value?.let { json.encodeToString(it) })
}
init { init {
if (installationId.isEmpty()) { if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString() installationId = UUID.randomUUID().toString()

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters

View File

@ -199,7 +199,7 @@ class StudentRepository @Inject constructor(
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) { suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester) val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() } val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") } .onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return .getOrNull() ?: return

View File

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

View File

@ -0,0 +1,66 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WulkanowyRepository @Inject constructor(
private val wulkanowyService: WulkanowyService,
private val adminMessageDao: AdminMessageDao,
private val preferencesRepository: PreferencesRepository,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "mapping_refresh_key"
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { wulkanowyService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
suspend fun getMapping(): Mapping? {
var savedMapping = preferencesRepository.mapping
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey)
)
if (savedMapping == null || isExpired) {
fetchMapping()
savedMapping = preferencesRepository.mapping
}
return savedMapping
}
suspend fun fetchMapping() {
runCatching { wulkanowyService.getMapping() }
.onFailure { Timber.e(it) }
.onSuccess {
preferencesRepository.mapping = it
refreshHelper.updateLastRefreshTimestamp(cacheKey)
}
}
}

View File

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

View File

@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor( class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository, private val wulkanowyRepository: WulkanowyRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo private val appInfo: AppInfo
) { ) {
@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
} }
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> { operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages -> return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages adminMessages
.asSequence() .asSequence()
.filter { it.isNotDismissed() } .filter { it.isNotDismissed() }

View File

@ -0,0 +1,106 @@
package io.github.wulkanowy.domain.attendance
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.*
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SubjectRepository
import io.github.wulkanowy.utils.allAbsences
import io.github.wulkanowy.utils.allPresences
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import kotlin.math.ceil
import kotlin.math.floor
class GetAttendanceCalculatorDataUseCase @Inject constructor(
private val subjectRepository: SubjectRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val preferencesRepository: PreferencesRepository,
) {
operator fun invoke(
student: Student,
semester: Semester,
forceRefresh: Boolean,
): Flow<Resource<List<AttendanceData>>> =
subjectRepository.getSubjects(student, semester, forceRefresh)
.mapResourceData { subjects -> subjects.sortedBy(Subject::name) }
.combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair)
.flatMapResourceData { (subjects, targetFreq) ->
combineResourceFlows(subjects.map { subject ->
attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = subject.realId,
forceRefresh = forceRefresh
).mapResourceData { summaries ->
summaries.toAttendanceData(subject.name, targetFreq)
}
})
// Every individual combined flow causes separate network requests to update data.
// When there is N child flows, they can cause up to N-1 items to be emitted. Since all
// requests are usually completed in less than 5s, there is no need to emit multiple
// intermediates that will be visible for barely any time.
.debounceIntermediates()
}
.combineWithResourceData(preferencesRepository.attendanceCalculatorShowEmptySubjects) { attendanceDataList, showEmptySubjects ->
attendanceDataList.filter { it.total != 0 || showEmptySubjects }
}
.combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List<AttendanceData>::sortedBy)
}
private fun List<AttendanceSummary>.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData {
val presences = sumOf { it.allPresences }
val absences = sumOf { it.allAbsences }
return AttendanceData(
subjectName = subjectName,
lessonBalance = calcLessonBalance(
targetFreq.toDouble() / 100, presences, absences
),
presences = presences,
absences = absences,
)
}
private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int {
val total = presences + absences
// The `+ 1` is to avoid false positives in close cases. Eg.:
// target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as
// a positive balance of +1, however that is not actually true as skipping one class
// would make it so that the balance would actually be negative (-98). The `+ 1`
// fixes this and makes sure that in situations like these, it's not reporting incorrect
// balances
return when {
presences / (total + 1f) >= targetFreq -> calcMissingAbsences(
targetFreq, absences, presences
)
presences / (total + 0f) < targetFreq -> -calcMissingPresences(
targetFreq, absences, presences
)
else -> 0
}
}
private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) =
calcMinRequiredPresencesFor(targetFreq, absences) - presences
private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) =
ceil((targetFreq / (1 - targetFreq)) * absences).toInt()
private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) =
calcMinRequiredAbsencesFor(targetFreq, presences) - absences
private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) =
floor((presences * (1 - targetFreq)) / targetFreq).toInt()
private fun List<AttendanceData>.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
ALPHABETIC -> sortedBy(AttendanceData::subjectName)
ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
}

View File

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

View File

@ -19,16 +19,18 @@ class NewGradeNotification @Inject constructor(
) { ) {
suspend fun notifyDetails(items: List<Grade>, student: Student) { suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notificationDataList = items.map { val notificationDataList = items
NotificationData( .filter { !it.isNotified }
title = context.getPlural(R.plurals.grade_new_items, 1), .map {
content = buildString { NotificationData(
append("${it.subject}: ${it.entry}") title = context.getPlural(R.plurals.grade_new_items, 1),
if (it.comment.isNotBlank()) append(" (${it.comment})") content = buildString {
}, append("${it.subject}: ${it.entry}")
destination = Destination.Grade, if (it.comment.isNotBlank()) append(" (${it.comment})")
) },
} destination = Destination.Grade,
)
}
val groupNotificationData = GroupNotificationData( val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList, notificationDataList = notificationDataList,

View File

@ -3,14 +3,19 @@ package io.github.wulkanowy.ui.modules.about
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemAboutBinding import io.github.wulkanowy.databinding.ItemAboutBinding
import io.github.wulkanowy.databinding.ScrollableHeaderAboutBinding import io.github.wulkanowy.databinding.ScrollableHeaderAboutBinding
import javax.inject.Inject import javax.inject.Inject
class AboutAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class AboutAdapter @Inject constructor(
private val preferencesRepository: PreferencesRepository
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var developerModeClicks = 0
private enum class ViewType(val id: Int) { private enum class ViewType(val id: Int) {
ITEM_HEADER(1), ITEM_HEADER(1),
ITEM_ELEMENT(2) ITEM_ELEMENT(2)
@ -46,6 +51,19 @@ class AboutAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.Vie
private fun bindHeaderViewHolder(binding: ScrollableHeaderAboutBinding) { private fun bindHeaderViewHolder(binding: ScrollableHeaderAboutBinding) {
with(binding.aboutScrollableHeaderIcon) { with(binding.aboutScrollableHeaderIcon) {
setOnClickListener {
if (++developerModeClicks == 5 && !preferencesRepository.developerMode) {
preferencesRepository.developerMode = true
developerModeClicks = 0
Toast.makeText(
context,
"done!",
Toast.LENGTH_SHORT
).show()
}
}
setImageDrawable(ResourcesCompat.getDrawableForDensity( setImageDrawable(ResourcesCompat.getDrawableForDensity(
context.resources, context.applicationInfo.icon, 640, null) context.resources, context.applicationInfo.icon, 640, null)
) )

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogExcuseBinding import io.github.wulkanowy.databinding.DialogExcuseBinding
import io.github.wulkanowy.databinding.FragmentAttendanceBinding import io.github.wulkanowy.databinding.FragmentAttendanceBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected() return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
else false else false
} }
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance()) (activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
} }
override fun openCalculatorView() {
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
}
override fun startActionMode() { override fun startActionMode() {
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback) actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
} }

View File

@ -1,16 +1,36 @@
package io.github.wulkanowy.ui.modules.attendance package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint import android.annotation.SuppressLint
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceLoading
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isExcusableOrNotExcused
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor(
return true return true
} }
fun onCalculatorSwitchSelected(): Boolean {
view?.openCalculatorView()
return true
}
private fun loadData(forceRefresh: Boolean = false) { private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading attendance data started") Timber.i("Loading attendance data started")

View File

@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
fun openSummaryView() fun openSummaryView()
fun openCalculatorView()
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String) fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
fun startActionMode() fun startActionMode()

View File

@ -0,0 +1,67 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.roundToInt
class AttendanceCalculatorAdapter @Inject constructor() :
RecyclerView.Adapter<AttendanceCalculatorAdapter.ViewHolder>() {
var items = emptyList<AttendanceData>()
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAttendanceCalculatorHeaderBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(parent: ViewHolder, position: Int) {
val context = parent.binding.root.context
val item = items[position]
with(parent.binding) {
attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}"
attendanceCalculatorSummaryBalance.text = when {
item.lessonBalance > 0 -> {
context.getString(
R.string.attendance_calculator_summary_balance_positive,
item.lessonBalance
)
}
item.lessonBalance < 0 -> {
context.getString(
R.string.attendance_calculator_summary_balance_negative,
abs(item.lessonBalance)
)
}
else -> context.getString(R.string.attendance_calculator_summary_balance_neutral)
}
attendanceCalculatorWarning.isVisible = item.lessonBalance < 0
attendanceCalculatorTitle.text = item.subjectName
attendanceCalculatorSummaryValues.text = if (item.total == 0) {
context.getString(R.string.attendance_calculator_summary_values_empty)
} else {
context.getString(
R.string.attendance_calculator_summary_values,
item.presences,
item.total
)
}
}
}
class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -0,0 +1,133 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
class AttendanceCalculatorFragment :
BaseFragment<FragmentAttendanceCalculatorBinding>(R.layout.fragment_attendance_calculator),
AttendanceCalculatorView, MainView.TitledView {
@Inject
lateinit var presenter: AttendanceCalculatorPresenter
@Inject
lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter
override val titleStringId get() = R.string.attendance_title
companion object {
fun newInstance() = AttendanceCalculatorFragment()
}
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceCalculatorBinding.bind(view)
messageContainer = binding.attendanceCalculatorRecycler
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_attendance_calculator, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected()
else false
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target)))
}
override fun initView() {
with(binding.attendanceCalculatorRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = attendanceCalculatorAdapter
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun updateData(data: List<AttendanceData>) {
with(attendanceCalculatorAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun clearView() {
with(attendanceCalculatorAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun showEmpty(show: Boolean) {
binding.attendanceCalculatorEmpty.isVisible = show
}
override fun showErrorView(show: Boolean) {
binding.attendanceCalculatorError.isVisible = show
}
override fun setErrorDetails(message: String) {
binding.attendanceCalculatorErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.attendanceCalculatorProgress.isVisible = show
}
override fun enableSwipe(enable: Boolean) {
binding.attendanceCalculatorSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
binding.attendanceCalculatorRecycler.isVisible = show
}
override fun showRefresh(show: Boolean) {
binding.attendanceCalculatorSwipe.isRefreshing = show
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,94 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import timber.log.Timber
import javax.inject.Inject
class AttendanceCalculatorPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase,
) : BasePresenter<AttendanceCalculatorView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: AttendanceCalculatorView) {
super.onAttachView(view)
view.initView()
Timber.i("Attendance calculator view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onSwipeRefresh() {
Timber.i("Force refreshing the attendance calculator")
loadData(forceRefresh = true)
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData(forceRefresh: Boolean = false) {
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
getAttendanceCalculatorData(student, semester, forceRefresh)
}
.logResourceStatus("load attendance calculator")
.onResourceData {
view?.run {
showProgress(false)
showErrorView(false)
showContent(it.isNotEmpty())
showEmpty(it.isEmpty())
updateData(it)
}
}
.onResourceIntermediate { view?.showRefresh(true) }
.onResourceNotLoading {
view?.run {
enableSwipe(true)
showRefresh(false)
showProgress(false)
}
}
.onResourceError(errorHandler::dispatch)
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
fun onSettingsSelected(): Boolean {
view?.openSettingsView()
return true
}
}

View File

@ -0,0 +1,31 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.ui.base.BaseView
interface AttendanceCalculatorView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun showRefresh(show: Boolean)
fun showContent(show: Boolean)
fun showProgress(show: Boolean)
fun enableSwipe(enable: Boolean)
fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun updateData(data: List<AttendanceData>)
fun clearView()
fun openSettingsView()
}

View File

@ -5,6 +5,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemAttendanceSummaryBinding import io.github.wulkanowy.databinding.ItemAttendanceSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderAttendanceSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderAttendanceSummaryBinding
import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.calculatePercentage
@ -13,9 +14,13 @@ import java.time.Month
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class AttendanceSummaryAdapter @Inject constructor() : class AttendanceSummaryAdapter @Inject constructor(
private val preferencesRepository: PreferencesRepository
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val attendancePercentage = preferencesRepository.attendancePercentage
private enum class ViewType(val id: Int) { private enum class ViewType(val id: Int) {
HEADER(1), HEADER(1),
ITEM(2) ITEM(2)
@ -48,7 +53,10 @@ class AttendanceSummaryAdapter @Inject constructor() :
} }
private fun bindHeaderViewHolder(binding: ScrollableHeaderAttendanceSummaryBinding) { private fun bindHeaderViewHolder(binding: ScrollableHeaderAttendanceSummaryBinding) {
binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(items.calculatePercentage()) binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(
attendancePercentage ?:
items.calculatePercentage()
)
} }
private fun bindItemViewHolder(binding: ItemAttendanceSummaryBinding, position: Int) { private fun bindItemViewHolder(binding: ItemAttendanceSummaryBinding, position: Int) {
@ -60,8 +68,8 @@ class AttendanceSummaryAdapter @Inject constructor() :
else -> item.month.getFormattedName() else -> item.month.getFormattedName()
} }
attendanceSummaryPercentage.text = when (position) { attendanceSummaryPercentage.text = when (position) {
-1 -> formatPercentage(items.calculatePercentage()) -1 -> formatPercentage(attendancePercentage ?: item.calculatePercentage())
else -> formatPercentage(item.calculatePercentage()) else -> formatPercentage(attendancePercentage ?: item.calculatePercentage())
} }
attendanceSummaryPresent.text = item.presence.toString() attendanceSummaryPresent.text = item.presence.toString()

View File

@ -27,8 +27,12 @@ class AuthPresenter @Inject constructor(
private fun loadName() { private fun loadName() {
presenterScope.launch { presenterScope.launch {
runCatching { studentRepository.getCurrentStudent(false) } runCatching {
.onSuccess { view?.showDescriptionWithName(it.studentName) } studentRepository.getCurrentStudent(false)
.studentName
.replace(" ", "\u00A0")
}
.onSuccess { view?.showDescriptionWithName(it) }
.onFailure { errorHandler.dispatch(it) } .onFailure { errorHandler.dispatch(it) }
} }
} }

View File

@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
webView = this webView = this
with(settings) { with(settings) {
javaScriptEnabled = true javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.create().userAgent userAgentString = wulkanowySdkFactory.createBase().userAgent
} }
webViewClient = object : WebViewClient() { webViewClient = object : WebViewClient() {

View File

@ -30,6 +30,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
@ -125,6 +126,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
mainActivity.pushView(ConferenceFragment.newInstance()) mainActivity.pushView(ConferenceFragment.newInstance())
} }
onAdminMessageClickListener = presenter::onAdminMessageSelected onAdminMessageClickListener = presenter::onAdminMessageSelected
onPanicButtonClickListener = presenter::onPanicButtonClicked
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
@ -208,7 +210,11 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
binding = binding.dashboardErrorAdminMessage, binding = binding.dashboardErrorAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected, onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(adminMessageItem.adminMessage) onPanicButtonClickListener = presenter::onPanicButtonClicked,
).bind(
item = adminMessageItem.adminMessage,
showPanicButton = true,
)
} }
} }
@ -236,6 +242,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
requireContext().openInternetBrowser(url) requireContext().openInternetBrowser(url)
} }
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun onDestroyView() { override fun onDestroyView() {
dashboardAdapter.clearTimers() dashboardAdapter.clearTimers()
presenter.onDetachView() presenter.onDetachView()

View File

@ -147,6 +147,17 @@ sealed class DashboardItem(val type: Type) {
EXAMS, EXAMS,
CONFERENCES, CONFERENCES,
} }
enum class HiddenAttendanceTile {
UNEXCUSED_ABSENCE,
EXEMPTION,
EXCUSED_LATENESS,
UNEXCUSED_LATENESS,
PRESENT,
DELETED,
EXCUSED_ABSENCE,
UNKNOWN,
}
} }
fun DashboardItem.Tile.toDashboardItemType() = when (this) { fun DashboardItem.Tile.toDashboardItemType() = when (this) {

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.repositories.ExamRepository
@ -23,6 +24,7 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
@ -282,8 +285,26 @@ class DashboardPresenter @Inject constructor(
url?.let { view?.openInternetBrowser(it) } url?.let { view?.openInternetBrowser(it) }
} }
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow { flow {
val attendancePercentage = preferencesRepository.attendancePercentage
val selectedTiles = selectedDashboardTiles val selectedTiles = selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null)) val flowSuccess = flowOf(Resource.Success(null))
@ -336,7 +357,7 @@ class DashboardPresenter @Inject constructor(
} else null } else null
}, },
attendancePercentage = DashboardItem.HorizontalGroup.Cell( attendancePercentage = DashboardItem.HorizontalGroup.Cell(
data = attendanceResource.dataOrNull?.calculatePercentage(), data = attendancePercentage ?: attendanceResource.dataOrNull?.calculatePercentage(),
error = attendanceResource.errorOrNull != null, error = attendanceResource.errorOrNull != null,
isLoading = attendanceResource is Resource.Loading, isLoading = attendanceResource is Resource.Loading,
), ),

View File

@ -31,4 +31,6 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView() fun openNotificationsCenterView()
fun openInternetBrowser(url: String) fun openInternetBrowser(url: String)
fun openPanicWebView(url: String)
} }

View File

@ -59,6 +59,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onAdminMessageClickListener: (String?) -> Unit = {} var onAdminMessageClickListener: (String?) -> Unit = {}
var onPanicButtonClickListener: () -> Unit = {}
var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {} var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {}
val items = mutableListOf<DashboardItem>() val items = mutableListOf<DashboardItem>()
@ -86,35 +88,46 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder( DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder(
ItemDashboardAccountBinding.inflate(inflater, parent, false) ItemDashboardAccountBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder( DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder(
ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false) ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.GRADES.ordinal -> GradesViewHolder( DashboardItem.Type.GRADES.ordinal -> GradesViewHolder(
ItemDashboardGradesBinding.inflate(inflater, parent, false) ItemDashboardGradesBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder( DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder(
ItemDashboardLessonsBinding.inflate(inflater, parent, false) ItemDashboardLessonsBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder( DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder(
ItemDashboardHomeworkBinding.inflate(inflater, parent, false) ItemDashboardHomeworkBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder( DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder(
ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false) ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder( DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder(
ItemDashboardExamsBinding.inflate(inflater, parent, false) ItemDashboardExamsBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder( DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
ItemDashboardConferencesBinding.inflate(inflater, parent, false) ItemDashboardConferencesBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false), ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener, onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener, onAdminMessageClickListener = onAdminMessageClickListener,
onPanicButtonClickListener = onPanicButtonClickListener,
) )
DashboardItem.Type.ADS.ordinal -> AdsViewHolder( DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false) ItemDashboardAdsBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
@ -129,7 +142,11 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position) is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage) is AdminMessageViewHolder -> holder.bind(
(items[position] as DashboardItem.AdminMessages).adminMessage,
showPanicButton = true
)
is AdsViewHolder -> bindAdsViewHolder(holder, position) is AdsViewHolder -> bindAdsViewHolder(holder, position)
} }
} }
@ -240,12 +257,15 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
attendancePercentage == null || attendancePercentage == .0 -> { attendancePercentage == null || attendancePercentage == .0 -> {
root.context.getThemeAttrColor(R.attr.colorOnSurface) root.context.getThemeAttrColor(R.attr.colorOnSurface)
} }
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorPrimary) root.context.getThemeAttrColor(R.attr.colorPrimary)
} }
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorTimetableChange) root.context.getThemeAttrColor(R.attr.colorTimetableChange)
} }
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface) else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
} }
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) { val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
@ -336,24 +356,28 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
tomorrowTimetable.isNotEmpty() -> { tomorrowTimetable.isNotEmpty() -> {
dateToNavigate = tomorrowDate dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding) updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader) updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader) updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
else -> { else -> {
dateToNavigate = currentDate dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding) updateLessonView(item, emptyList(), binding)
@ -461,6 +485,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText = firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_moment) context.getString(R.string.dashboard_timetable_first_lesson_title_moment)
} }
minutesToStartLesson < 240 -> { minutesToStartLesson < 240 -> {
firstTitleAndValueTextColor = firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface) context.getThemeAttrColor(R.attr.colorOnSurface)
@ -468,6 +493,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText = firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_soon) context.getString(R.string.dashboard_timetable_first_lesson_title_soon)
} }
else -> { else -> {
firstTitleAndValueTextColor = firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface) context.getThemeAttrColor(R.attr.colorOnSurface)

View File

@ -13,9 +13,10 @@ class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding, private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit, private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit, private val onAdminMessageClickListener: (String?) -> Unit,
private val onPanicButtonClickListener: () -> Unit,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?) { fun bind(item: AdminMessage?, showPanicButton: Boolean = false) {
item ?: return item ?: return
val context = binding.root.context val context = binding.root.context
@ -48,10 +49,14 @@ class AdminMessageViewHolder(
dashboardAdminMessageItemClose.setOnClickListener { dashboardAdminMessageItemClose.setOnClickListener {
onAdminMessageDismissClickListener(item) onAdminMessageDismissClickListener(item)
} }
dashboardPanicSection.root.isVisible = showPanicButton
dashboardPanicSection.dashboardPanicButton.setOnClickListener {
onPanicButtonClickListener()
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) dashboardAdminMessage.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url -> item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) } dashboardAdminMessage.setOnClickListener { onAdminMessageClickListener(url) }
} }
} }
} }

View File

@ -6,6 +6,6 @@ enum class GradeAverageMode(val value: String) {
BOTH_SEMESTERS("both_semesters"); BOTH_SEMESTERS("both_semesters");
companion object { companion object {
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE_SEMESTER fun getByValue(value: String) = entries.firstOrNull { it.value == value } ?: ONE_SEMESTER
} }
} }

View File

@ -118,5 +118,6 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
inAppUpdateHelper.onResume() inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
} }
} }

View File

@ -1,12 +1,15 @@
package io.github.wulkanowy.ui.modules.login package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class LoginPresenter @Inject constructor( class LoginPresenter @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository
) : BasePresenter<LoginView>(errorHandler, studentRepository) { ) : BasePresenter<LoginView>(errorHandler, studentRepository) {
@ -16,4 +19,11 @@ class LoginPresenter @Inject constructor(
view.initView() view.initView()
Timber.i("Login view was initialized") Timber.i("Login view was initialized")
} }
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
} }

View File

@ -238,6 +238,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding = binding.loginFormMessage, binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected, onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(message) ).bind(message)
binding.loginFormMessage.root.isVisible = message != null binding.loginFormMessage.root.isVisible = message != null
} }

View File

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

View File

@ -2,16 +2,23 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.pojos.RegisterStudent import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.SchoolsRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
@ -33,6 +40,8 @@ class LoginStudentSelectPresenter @Inject constructor(
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
private val appInfo: AppInfo, private val appInfo: AppInfo,
private val preferencesRepository: PreferencesRepository,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null private var lastError: Throwable? = null
@ -65,6 +74,7 @@ class LoginStudentSelectPresenter @Inject constructor(
this.loginData = loginData this.loginData = loginData
this.registerUser = registerUser this.registerUser = registerUser
loadData() loadData()
loadAdminMessage()
} }
private fun loadData() { private fun loadData() {
@ -88,7 +98,20 @@ class LoginStudentSelectPresenter @Inject constructor(
refreshItems() refreshItems()
} }
} }
}.launch() }.launch("load_data")
}
private fun loadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(),
type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch("load_admin_message")
} }
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> { private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
@ -341,4 +364,14 @@ class LoginStudentSelectPresenter @Inject constructor(
) )
} }
} }
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -138,6 +138,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
inAppUpdateHelper.onResume() inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -29,6 +30,7 @@ class MainPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val wulkanowyRepository: WulkanowyRepository,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
private val json: Json, private val json: Json,
@ -199,4 +201,11 @@ class MainPresenter @Inject constructor(
.onFailure { errorHandler.dispatch(it) } .onFailure { errorHandler.dispatch(it) }
} }
} }
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
} }

View File

@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
@ -132,6 +133,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
) )
messageTabErrorRetry.setOnClickListener { presenter.onRetry() } messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
messageTabPanicSection.dashboardPanicButton.setOnClickListener { presenter.onPanicButtonClicked() }
} }
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle -> setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
@ -283,6 +285,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
) )
} }
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun hideKeyboard() { override fun hideKeyboard() {
activity?.hideSoftInput() activity?.hideSoftInput()
} }

View File

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch import me.xdrop.fuzzywuzzy.FuzzySearch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.pow import kotlin.math.pow
@ -429,4 +430,20 @@ class MessageTabPresenter @Inject constructor(
+ dateRatio.toDouble().pow(2) * 2 + dateRatio.toDouble().pow(2) * 2
).toInt() ).toInt()
} }
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}-wiadomosciplus.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
} }

View File

@ -50,4 +50,6 @@ interface MessageTabView : BaseView {
fun showRecyclerBottomPadding(show: Boolean) fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List<Mailbox>) fun showMailboxChooser(mailboxes: List<Mailbox>)
fun openPanicWebView(url: String)
} }

View File

@ -12,6 +12,8 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
var onClickListener: (moreItem: MoreItem) -> Unit = {} var onClickListener: (moreItem: MoreItem) -> Unit = {}
var onLongClickListener: (moreItem: MoreItem) -> Unit = {}
override fun getItemCount() = items.size override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
@ -27,6 +29,10 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
moreItemImage.setImageResource(item.icon) moreItemImage.setImageResource(item.icon)
root.setOnClickListener { onClickListener(item) } root.setOnClickListener { onClickListener(item) }
root.setOnLongClickListener {
onLongClickListener(item)
true
}
} }
} }

View File

@ -5,6 +5,7 @@ import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentMoreBinding import io.github.wulkanowy.databinding.FragmentMoreBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
@ -23,6 +24,9 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
@Inject @Inject
lateinit var moreAdapter: MoreAdapter lateinit var moreAdapter: MoreAdapter
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object { companion object {
fun newInstance() = MoreFragment() fun newInstance() = MoreFragment()
} }
@ -73,4 +77,9 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()
} }
override fun restartApp() {
startActivity(MainActivity.getStartIntent(requireContext()))
requireActivity().finishAffinity()
}
} }

View File

@ -12,4 +12,6 @@ interface MoreView : BaseView {
fun popView(depth: Int) fun popView(depth: Int)
fun openView(destination: Destination) fun openView(destination: Destination)
fun restartApp()
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.note
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.repositories.NoteRepository import io.github.wulkanowy.data.repositories.NoteRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -17,6 +18,7 @@ class NotePresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val noteRepository: NoteRepository, private val noteRepository: NoteRepository,
private val semesterRepository: SemesterRepository, private val semesterRepository: SemesterRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<NoteView>(errorHandler, studentRepository) { ) : BasePresenter<NoteView>(errorHandler, studentRepository) {
@ -48,6 +50,19 @@ class NotePresenter @Inject constructor(
} }
private fun loadData(forceRefresh: Boolean = false) { private fun loadData(forceRefresh: Boolean = false) {
if (!preferencesRepository.showNotes) {
view?.run {
enableSwipe(false)
showEmpty(false)
showContent(false)
showErrorView(false)
showProgress(false)
showEmpty(true)
}
return
}
flatResourceFlow { flatResourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)

View File

@ -0,0 +1,99 @@
package io.github.wulkanowy.ui.modules.panicmode
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.addCallback
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.databinding.FragmentPanicModeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@AndroidEntryPoint
class PanicModeFragment : BaseFragment<FragmentPanicModeBinding>(R.layout.fragment_panic_mode),
MainView.TitledView {
@Inject
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
override val titleStringId: Int get() = R.string.panic_mode_title
companion object {
private const val PANIC_URL = "panic_mode_url"
fun newInstance(url: String?): PanicModeFragment {
return PanicModeFragment().apply {
arguments = bundleOf(PANIC_URL to url)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentPanicModeBinding.bind(view)
binding.panicModeRefresh.setOnClickListener {
binding.panicModeWebview.loadUrl(
binding.panicModeWebview.url ?: arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeBack.setOnClickListener { binding.panicModeWebview.goBack() }
binding.panicModeHome.setOnClickListener {
binding.panicModeWebview.loadUrl(
arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeForward.setOnClickListener { binding.panicModeWebview.goForward() }
binding.panicModeShare.setOnClickListener {
requireContext().openInternetBrowser(
binding.panicModeWebview.url.toString(),
)
}
val onBackPressedCallback = requireActivity().onBackPressedDispatcher
.addCallback(viewLifecycleOwner) {
binding.panicModeWebview.goBack()
}
with(binding.panicModeWebview) {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.createBase().userAgent
}
webViewClient = object : WebViewClient() {
override fun doUpdateVisitedHistory(
view: WebView?,
url: String?,
isReload: Boolean
) {
binding.panicModeBack.isEnabled = binding.panicModeWebview.canGoBack()
binding.panicModeForward.isEnabled = binding.panicModeWebview.canGoForward()
onBackPressedCallback.isEnabled = binding.panicModeWebview.canGoBack()
}
}
loadUrl(arguments?.getString(PANIC_URL).orEmpty())
}
}
override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}
}

View File

@ -1,13 +1,22 @@
package io.github.wulkanowy.ui.modules.settings package io.github.wulkanowy.ui.modules.settings
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView { @AndroidEntryPoint
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, SettingsView {
companion object { companion object {
@ -16,11 +25,26 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin
override val titleStringId get() = R.string.settings_title override val titleStringId get() = R.string.settings_title
@Inject
lateinit var preferencesRepository: PreferencesRepository
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey) setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
val prefScreen: PreferenceScreen? = findPreference("settings_preferences")
val prefDeveloper: Preference? = findPreference("mod_settings")
if (!preferencesRepository.developerMode && prefScreen != null && prefDeveloper != null) {
prefScreen.removePreference(prefDeveloper)
}
Timber.i("Settings view was initialized") Timber.i("Settings view was initialized")
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
}
override fun showError(text: String, error: Throwable) {} override fun showError(text: String, error: Throwable) {}
override fun showMessage(text: String) {} override fun showMessage(text: String) {}

View File

@ -3,7 +3,9 @@ package io.github.wulkanowy.ui.modules.settings.appearance
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.os.bundleOf
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import com.yariksoffice.lingver.Lingver import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -29,13 +31,31 @@ class AppearanceFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.pref_settings_appearance_title override val titleStringId get() = R.string.pref_settings_appearance_title
companion object {
fun withFocusedPreference(key: String) = AppearanceFragment().apply {
arguments = bundleOf(FOCUSED_KEY to key)
}
private const val FOCUSED_KEY = "focusedKey"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this) presenter.onAttachView(this)
arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) }
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey) setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
val attendanceTargetPref =
findPreference<SeekBarPreference>(requireContext().getString(R.string.pref_key_attendance_target))!!
attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj ->
val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5
attendanceTargetPref.value =
newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max)
false
}
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {

View File

@ -0,0 +1,147 @@
package io.github.wulkanowy.ui.modules.settings.mod_settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.preference.EditTextPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class ModSettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, ModSettingsView {
@Inject
lateinit var presenter: ModSettingsPresenter
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
override val titleStringId get() = R.string.pref_mod_settings_title
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_mod_settings, rootKey)
val attendancePercentagePreference: EditTextPreference? = findPreference("attendance_percentage")
attendancePercentagePreference?.setOnBindEditTextListener { editText ->
editText.inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL
editText.filters = arrayOf(
android.text.InputFilter { source, _, _, dest, _, _ ->
if (source == "." && dest.isEmpty()) {
return@InputFilter "0."
}
val input = dest.toString() + source.toString()
if (input == "100.00") {
return@InputFilter null
}
val inputVal = input.toFloatOrNull()
if (inputVal != null && inputVal >= 0 && inputVal <= 100) {
null
} else {
""
}
}
)
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
(activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl)
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showAuthDialog() {
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun showAttendanceSettings(items: List<DashboardItem.HiddenAttendanceTile>) {
val entries = requireContext().resources.getStringArray(R.array.mod_settings_attendance_entries)
val values = requireContext().resources.getStringArray(R.array.mod_settings_attendance_values)
val selectedItemsState = values.map { value -> items.any { it.name == value } }
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.attendance_title)
.setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> }
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val selectedState = (dialog as AlertDialog).listView.checkedItemPositions
val selectedValues = values
.filterIndexed { index, _ -> selectedState[index] }
.map { DashboardItem.HiddenAttendanceTile.valueOf(it) }
Timber.i("Selected attendance to hide: $selectedValues")
presenter.onAttendanceSettingsSelected(selectedValues)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun restartApp() {
startActivity(MainActivity.getStartIntent(requireContext()))
requireActivity().finishAffinity()
}
}

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