1
0

Compare commits

...

119 Commits
1.0.0 ... 1.1.6

Author SHA1 Message Date
b60c59216d Merge branch 'hotfix/messages-send' 2021-05-21 14:08:00 +02:00
d20f3180cf Version 1.1.6 2021-05-21 14:03:13 +02:00
17761af9d6 Bump sdk 2021-05-21 12:35:57 +02:00
067817bace Bump huawei-publish-gradle-plugin from 1.2.2 to 1.2.4 (#1314)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
(cherry picked from commit 022a4d1ea2)
2021-05-21 12:30:36 +02:00
800a31f160 Merge branch 'hotfix/all-year-average' 2021-04-18 20:40:02 +02:00
8b83b37b09 Version 1.1.5 2021-04-18 20:38:18 +02:00
43e95cfdc6 Fix all year average 2021-04-18 19:36:02 +02:00
ae2a697e01 Merge pull request #1295 from wulkanowy/rbo/update-workflow
Update github action workflow
2021-04-18 19:34:32 +02:00
3f5fbbc71b Merge branch 'hotfix/fix-recovery-visibility' 2021-04-01 22:40:20 +02:00
206b40ce1b Change version in changelog 2021-04-01 22:38:35 +02:00
43c56b5534 Version 1.1.4 2021-04-01 22:36:51 +02:00
a1076539dc Fix content visibility in login recovery 2021-04-01 22:30:09 +02:00
3c48264539 Merge branch 'release/1.1.3' 2021-03-28 20:13:37 +02:00
539cf2207b Version 1.1.3 2021-03-28 20:13:32 +02:00
f0e897713c New Crowdin updates (#1233) 2021-03-28 19:52:38 +02:00
a448092008 Allow special login format in login form (#1258) 2021-03-28 19:46:56 +02:00
f5b46707ff Fixing README.md (#1255) 2021-03-28 15:56:44 +02:00
e6247d4428 Fix clearing no existing dialog fragment (#1242) 2021-03-28 08:18:07 +02:00
d0869b235a Use db student id to distinguish reporting units and recipients (#1254) 2021-03-27 13:43:25 +01:00
464900d95b Bump annotation from 1.1.0 to 1.2.0 (#1245) 2021-03-25 10:12:05 +00:00
368274239e Bump agconnect-crash from 1.5.0.300 to 1.5.1.200 (#1247) 2021-03-25 10:11:19 +00:00
ee33197494 Bump room from 2.3.0-beta03 to 2.3.0-rc01 (#1246) 2021-03-25 10:09:21 +00:00
d3ea743707 Bump kotlin_version from 1.4.31 to 1.4.32 (#1253) 2021-03-25 10:07:43 +00:00
976d4b8ce2 Bump fragment-ktx from 1.3.1 to 1.3.2 (#1248) 2021-03-25 11:05:14 +01:00
b77fc0d32a Bump activity-ktx from 1.2.1 to 1.2.2 (#1249) 2021-03-25 11:04:37 +01:00
bd3716609e Bump hianalytics from 5.2.0.300 to 5.2.0.301 (#1250) 2021-03-25 11:04:04 +01:00
5b87cc9009 Bump agcp from 1.5.0.300 to 1.5.1.200 (#1251) 2021-03-25 11:03:37 +01:00
4f7be8d2cb Bump lifecycle-livedata-ktx from 2.3.0 to 2.3.1 (#1252) 2021-03-25 11:02:39 +01:00
8733e7782f Fix colorPrimary and class name in widget account manager (#1241) 2021-03-21 22:37:34 +01:00
e03b0dfa01 Fix missing avatars in widgets (#1238) 2021-03-21 12:04:55 +01:00
efafd2094a Add dialog with info about dropping support for android 4 (#1221) 2021-03-20 14:01:17 +01:00
1560335749 Fix very rare crash in login recovery (#1231) 2021-03-20 13:27:47 +01:00
5bee155f1e Remove listenablefuture from dependencies (#1237) 2021-03-20 12:10:53 +00:00
c9dc9a323f Bump gradle from 4.1.2 to 4.1.3 (#1234) 2021-03-19 15:02:28 +00:00
87e7e00705 Remove firebase inappmessage dependency (#1235) 2021-03-19 15:57:12 +01:00
57681b35ea Bump mockk from 1.10.6 to 1.11.0 (#1229) 2021-03-17 22:58:17 +00:00
8fb09d7b7d Merge branch 'release/1.1.2' into develop 2021-03-16 12:59:00 +01:00
168f750863 Merge branch 'release/1.1.2' 2021-03-16 12:58:56 +01:00
3e1acbd3bf Version 1.1.2 2021-03-16 12:58:52 +01:00
21ef2adcf6 Disable optimization in r8 config and fix crash in grade fragment (#1226) 2021-03-16 11:49:17 +00:00
3f6159e976 Fix show error details button in additional lessons (#1225) 2021-03-16 12:47:51 +01:00
555b5ec112 Merge branch 'release/1.1.1' into develop 2021-03-16 00:43:39 +01:00
60a9bcae46 Merge branch 'release/1.1.1' 2021-03-16 00:43:33 +01:00
eeb1341c1f Version 1.1.1 2021-03-16 00:43:28 +01:00
c77b50d51b New Crowdin updates (#1204) 2021-03-16 00:30:28 +01:00
8644ce32d5 Fix semester switch when student have only one semester (#1215) 2021-03-15 23:58:50 +01:00
94506aca52 Add github actions config to deploy apk to App Center (#1220) 2021-03-15 18:18:08 +01:00
eee4e1f4b5 Fix empty view in attendance (#1217) 2021-03-15 00:33:40 +01:00
c1942d012f Maybe fix fragment commits after activity state is saved (#1216) 2021-03-13 20:15:12 +01:00
fe846b463a Update material chips input (#1214) 2021-03-13 20:14:36 +01:00
0ea2e68249 Fix and clean proguard/r8 file (#1213) 2021-03-13 20:14:10 +01:00
be0445b227 Change the absence request confirmation message string (#1212) 2021-03-13 20:13:57 +01:00
48249f3093 Update kotlin coroutines (#1211) 2021-03-13 20:13:48 +01:00
8d7110735d Ignore no current student during avatar loading (#1210) 2021-03-13 20:13:37 +01:00
94957850c3 Bump fragment-ktx from 1.3.0 to 1.3.1 (#1205) 2021-03-12 21:50:59 +00:00
fa2cfc8427 Bump room from 2.3.0-beta02 to 2.3.0-beta03 (#1207) 2021-03-12 21:33:30 +00:00
3d467c43ba Use new kotlin compiler backend (#1202) 2021-03-12 22:07:27 +01:00
b76032044d Bump activity-ktx from 1.2.0 to 1.2.1 (#1206) 2021-03-12 21:06:33 +00:00
495b84204c Bump work_hilt from 1.0.0-alpha03 to 1.0.0-beta01 (#1208) 2021-03-12 21:06:27 +00:00
ea4b299de6 Bump firebase-bom from 26.6.0 to 26.7.0 (#1209) 2021-03-12 12:39:01 +00:00
acb5e2afd4 Replace dash mark with no data string in SchoolFragment (#1203) 2021-03-09 17:49:24 +01:00
50863d6ac2 Merge branch 'release/1.1.0' into develop 2021-03-07 21:58:30 +01:00
e15eb03299 Merge branch 'release/1.1.0' 2021-03-07 21:58:23 +01:00
be48791d51 Version 1.1.0 2021-03-07 21:58:14 +01:00
c0e1a5b401 New Crowdin updates (#1167) 2021-03-07 20:48:29 +00:00
cb1b467a21 Add studzinskik to contributors (#1198) 2021-03-07 20:31:14 +00:00
f14346ff32 Fix duplicate items after running automatic and manual sync at the same time (#1197) 2021-03-07 20:47:18 +01:00
af8108a649 Add lucky number history (#1184) 2021-03-07 20:17:03 +01:00
5743928126 Fix notification and status bars colors (#1196) 2021-03-07 14:04:37 +00:00
388d37bf9c Fix date picker theme (#1194) 2021-03-07 13:41:54 +01:00
d572fc737f Fix typo (#1193) 2021-03-07 00:07:57 +01:00
47b0f1b527 Settings revamp (#1160) 2021-03-06 17:18:42 +00:00
1afa7ecf3c Fix OOM in grade statistics (#1192) 2021-03-06 16:50:06 +01:00
9139febbdf Migrate database migrations from androidTest to roboelectric (#1191) 2021-03-06 14:34:07 +00:00
2dd0b56333 Bump agconnect-crash from 1.5.0.200 to 1.5.0.300 (#1190) 2021-03-06 12:08:43 +00:00
3b970209a5 Fix grade fragment subtitle (#1185) 2021-03-06 13:01:41 +01:00
6f590eb194 Fix empty license dialogs (#1182) 2021-03-06 13:01:17 +01:00
b20b8fb243 Bump firebase-bom from 26.5.0 to 26.6.0 (#1189) 2021-03-06 11:23:16 +00:00
7e4a212951 Bump agcp from 1.5.0.200 to 1.5.0.300 (#1187) 2021-03-06 10:55:37 +00:00
6dfeed3a26 Fix API host key capitalisation for powiat krasnostawski (#1181) 2021-03-04 20:32:17 +00:00
06a27199ee Add early validation for special mail domains (#1000) (#1176) 2021-03-03 22:37:58 +01:00
76039e5eb9 Remove hardcoded strings (#1177) 2021-03-03 20:18:03 +01:00
6a4aaff8d6 Fix no system theme option in Czech and Slovak (#1178) 2021-03-03 19:16:46 +00:00
9e2985864a Add avatars (#1146) 2021-03-02 23:34:25 +00:00
412057b512 Remove deprecations (#1170) 2021-03-02 17:28:47 +01:00
963caadced Bump firebase-crashlytics-gradle from 2.5.0 to 2.5.1 (#1171) 2021-03-01 22:19:31 +00:00
8388a8a5fc Fix calling of binding outside lifecycle (#1169) 2021-03-01 21:57:47 +00:00
a801c8f8be Add exception ignoring to UpdateHelper when PendingIntent is duplicated (#1168) 2021-03-01 21:35:49 +00:00
9d8ad73e63 Bump hilt_version from 2.32-alpha to 2.33-beta (#1163)
* Bump hilt_version from 2.32-alpha to 2.33-beta

Bumps `hilt_version` from 2.32-alpha to 2.33-beta.

Updates `hilt-android-gradle-plugin` from 2.32-alpha to 2.33-beta
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/commits)

Updates `hilt-android` from 2.32-alpha to 2.33-beta
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/commits)

Updates `hilt-android-compiler` from 2.32-alpha to 2.33-beta
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* Add inject before super.onCreate() in activity

* Fix format

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Rafał Borcz <RafalBO99@outlook.com>
2021-02-27 22:49:50 +01:00
182f6c8a81 Bump kotlin_version from 1.4.30 to 1.4.31 (#1166) 2021-02-26 19:21:02 +00:00
1e4a3536cf Bump desugar_jdk_libs from 1.1.1 to 1.1.5 (#1164) 2021-02-26 18:50:58 +00:00
169a314664 Bump about_libraries from 8.8.2 to 8.8.3 (#1165) 2021-02-26 18:49:43 +00:00
904eed648b Bump hianalytics from 5.1.0.301 to 5.2.0.300 (#1161) 2021-02-24 23:00:00 +00:00
3bb94adece Bump room from 2.3.0-beta01 to 2.3.0-beta02 (#1158) 2021-02-22 23:07:37 +00:00
105b70fcad Add Daxxxis to contributors (#1156) 2021-02-18 21:25:46 +01:00
b4bf7c7589 Add Slovak to dialog (#1154) 2021-02-18 00:05:40 +01:00
ff425d6d2b Change background color of navigation and notification bars (#1120) 2021-02-18 00:05:27 +01:00
4fceb854b3 Bump activity-ktx from 1.1.0 to 1.2.0 (#1134) 2021-02-17 12:08:10 +00:00
3adac154b4 Bump firebase-crashlytics-gradle from 2.4.1 to 2.5.0 (#1147) 2021-02-17 11:55:27 +00:00
a320cf8f7c Bump fragment-ktx from 1.2.5 to 1.3.0 (#1148) 2021-02-17 11:39:26 +00:00
1ed0884dfd Bump hilt_version from 2.31.2-alpha to 2.32-alpha (#1133) 2021-02-17 11:38:51 +00:00
021e9726c6 Bump lifecycle-livedata-ktx from 2.2.0 to 2.3.0 (#1136) 2021-02-17 11:38:33 +00:00
3c438757e3 Bump firebase-bom from 26.4.0 to 26.5.0 (#1151) 2021-02-17 11:31:05 +00:00
99c4a65df5 Merge tag '1.0.1' into develop
Version 1.0.1
2021-02-16 12:46:54 +01:00
2c442fc87b Merge branch 'release/1.0.1' 2021-02-16 12:44:34 +01:00
44ba0d76de Version 1.0.1 2021-02-16 12:44:24 +01:00
17aa77ad41 New Crowdin updates (#1152) 2021-02-16 12:12:35 +01:00
c18302b812 Bump junit from 4.13.1 to 4.13.2 (#1150) 2021-02-14 12:14:28 +00:00
de8e9bde49 Bump mockk from 1.10.5 to 1.10.6 (#1149) 2021-02-14 12:13:21 +00:00
000cbd11a2 Fix EOF error in messages (#1145) 2021-02-13 20:39:52 +01:00
5e3b89636f Add checking vulcan average from both semesters (#1140) 2021-02-13 12:33:53 +01:00
dd085a14fa Revert "Bump fragment-ktx from 1.2.5 to 1.3.0 (#1138)" (#1144)
This reverts commit cac76857c4.
2021-02-13 10:58:01 +00:00
bfe558d887 Update README (#1143) 2021-02-12 19:14:45 +01:00
4841a0439d New Crowdin updates (#1131) 2021-02-12 19:07:36 +01:00
cac76857c4 Bump fragment-ktx from 1.2.5 to 1.3.0 (#1138) 2021-02-12 18:05:33 +00:00
6b8b7c9143 Bump material from 1.3.0-rc01 to 1.3.0 (#1137) 2021-02-12 10:12:35 +01:00
53584026dc Bump about_libraries from 8.7.0 to 8.8.2 (#1135) 2021-02-12 09:11:52 +00:00
7900ad913f Bump kotlin_version from 1.4.21 to 1.4.30 (#1139) 2021-02-11 12:20:45 +00:00
8ec844a8fe Merge branch 'release/1.0.0' into develop 2021-02-07 23:48:43 +01:00
213 changed files with 9594 additions and 1837 deletions

74
.github/workflows/deploy-store.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: Deploy to app stores
on:
release:
types: [ created ]
jobs:
deploy-google-play:
name: Deploy to google play
runs-on: ubuntu-latest
timeout-minutes: 10
environment: google-play
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
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/key.p12.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play
env:
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace;
deploy-app-gallery:
name: Deploy to AppGallery
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-gallery
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
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/key.p12.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_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew assembleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

144
.github/workflows/deploy-test.yml vendored Normal file
View File

@ -0,0 +1,144 @@
name: Deploy to app tests
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@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
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@v2
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@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
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@v2
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,48 +1,21 @@
name: Test and deploy
name: Tests
on:
push:
branches: [ develop ]
branches: [ master, develop ]
tags: [ '*' ]
pull_request:
branches: [ develop ]
workflow_dispatch:
branches: [ master, develop ]
jobs:
build:
name: Pre-build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Build
run: ./gradlew --build-cache compileFdroidDebugUnitTestKotlin preFdroidDebugAndroidTestBuild dexBuilderFdroidDebugAndroidTest packageFdroidDebug packageFdroidDebugAndroidTest
- name: Prepare build cache
run: tar -cf prebuild.tar .build-cache .gradle app/build
- uses: actions/upload-artifact@v2
with:
name: prebuild.tar
path: prebuild.tar
unit-tests:
name: Unit tests
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [ build ]
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v1
with:
java-version: 11
@ -52,11 +25,6 @@ jobs:
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- uses: actions/download-artifact@v2
with:
name: prebuild.tar
- name: Extract build cache
run: tar -xf prebuild.tar
- name: Unit tests
run: |
./gradlew --build-cache -Pcoverage testFdroidDebugUnitTest --stacktrace
@ -64,79 +32,3 @@ jobs:
- uses: codecov/codecov-action@v1
with:
flags: unit
instrumentation-tests:
name: Instrumentation tests
runs-on: macOS-latest
timeout-minutes: 15
needs: [ build ]
strategy:
fail-fast: true
matrix:
api-level: [21, 29]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- uses: actions/download-artifact@v2
with:
name: prebuild.tar
- name: Extract build cache
run: tar -xf prebuild.tar
- name: Instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86
script: |
./gradlew --build-cache -Pcoverage connectedFdroidDebugAndroidTest --stacktrace
./gradlew --build-cache -Pcoverage jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v1
with:
flags: instrumented,api-${{ matrix.api-level }}
deploy-google-play:
name: Deploy to google play
runs-on: ubuntu-latest
timeout-minutes: 10
environment: google-play
needs: [ build, unit-tests, instrumentation-tests ]
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- uses: actions/download-artifact@v2
with:
name: prebuild.tar
- name: Extract build cache
run: tar -xf prebuild.tar
- 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/key.p12.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play
env:
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace;

1
.gitignore vendored
View File

@ -117,3 +117,4 @@ Thumbs.db
app/src/release/agconnect-services.json
app/src/release/agconnect-credentials.json

View File

@ -12,7 +12,7 @@ Unofficial android VULCAN UONET+ register client for both students and their par
## Features
* logging in using the email and password OR using token and pin
* logging in using the email and password
* functions from the register website:
* grades
* grade statistics
@ -25,15 +25,19 @@ Unofficial android VULCAN UONET+ register client for both students and their par
* 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
* no ads
## Download
You can download the current beta version from the Google Play, F-Droid or Huawei AppGallery store
You can download the current version from the Google Play, F-Droid or Huawei AppGallery store
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
@ -60,6 +64,9 @@ You can also download a [development version](https://wulkanowy.github.io/#downl
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

@ -12,7 +12,7 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
## Funkcje
* logowanie za pomocą e-maila i hasła LUB tokena i pinu
* logowanie za pomocą e-maila i hasła
* funkcje ze strony internetowej dziennika:
* oceny
* statystyki ocen
@ -25,15 +25,19 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
* 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 offilne
* tryb offline
* brak reklam
## Pobierz
Aktualną wersję beta możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery
Aktualną wersję możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Pobierz z Google Play"
@ -61,6 +65,9 @@ Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#downloa
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)

BIN
app/bitrise.jks.gpg Normal file

Binary file not shown.

View File

@ -4,7 +4,10 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.github.triplet.play'
apply plugin: 'ru.cian.huawei-publish'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.huawei.agconnect'
apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
@ -18,8 +21,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 30
versionCode 84
versionName "1.0.0"
versionCode 92
versionName "1.1.6"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -41,7 +44,8 @@ android {
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
// https://github.com/robolectric/robolectric/issues/3928#issuecomment-395309991
debug.assets.srcDirs += files("$projectDir/schemas".toString())
}
signingConfigs {
@ -103,6 +107,10 @@ android {
disable 'HardwareIds'
}
testOptions.unitTests {
includeAndroidResources = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
@ -110,6 +118,7 @@ android {
}
kotlinOptions {
useIR = true
jvmTarget = "1.8"
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
}
@ -128,33 +137,43 @@ play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'alpha'
updatePriority = 3
track = 'production'
updatePriority = 5
}
huaweiPublish {
instances {
hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "apk"
deployType = "draft"
}
}
}
ext {
work_manager = "2.5.0"
work_hilt = "1.0.0-alpha03"
room = "2.3.0-beta01"
work_hilt = "1.0.0-beta01"
room = "2.3.0-rc01"
chucker = "3.4.0"
mockk = "1.10.5"
mockk = "1.11.0"
moshi = "1.11.0"
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.0.0"
implementation "io.github.wulkanowy:sdk:1.1.6"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.appcompat:appcompat-resources:1.2.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.3.2"
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.multidex:multidex:2.0.1"
implementation "androidx.preference:preference-ktx:1.1.1"
@ -163,14 +182,15 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.3.0-rc01"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.google.android.material:material:1.3.0"
implementation "com.github.wulkanowy:material-chips-input:2.2.0"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.mikhaellopez:circularimageview:4.2.0'
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room"
@ -197,35 +217,36 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation platform('com.google.firebase:firebase-bom:26.4.0')
playImplementation platform('com.google.firebase:firebase-bom:26.7.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx"
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
hmsImplementation 'com.huawei.hms:hianalytics:5.1.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.5.0.200'
hmsImplementation 'com.huawei.hms:hianalytics:5.2.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.5.1.200'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation "com.amitshekhar.android:debug-db:1.0.6"
testImplementation "junit:junit:4.13.1"
testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation "androidx.test:runner:1.3.0"
testImplementation "androidx.test.ext:junit:1.1.2"
testImplementation "androidx.test:core:1.3.0"
testImplementation "androidx.room:room-testing:$room"
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"
androidTestImplementation "androidx.test:core:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.huawei.agconnect'

View File

@ -1,33 +1,21 @@
# Optimizations
-optimizationpasses 5
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontskipnonpubliclibraryclassmembers
-dontpreverify
# General
-dontobfuscate
-allowaccessmodification
-repackageclasses ''
-verbose
#Keep all wulkanowy files
#Config for wulkanowy
-keep class io.github.wulkanowy.** {*;}
#Config for anallitycs
-keepattributes *Annotation*
#Config for firebase crashlitycs
-keepattributes SourceFile,LineNumberTable
-keep class com.crashlytics.** {*;}
-keep public class * extends java.lang.Exception
-dontwarn com.crashlytics.**
#Config for OkHttp
#Config for Okio and OkHttp
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-dontwarn org.codehaus.mojo.animal_sniffer.*
-dontwarn okhttp3.internal.platform.ConscryptPlatform
-dontwarn javax.annotation.**
#Config for MPAndroidChart
@ -36,10 +24,3 @@
#Config for Material Components
-keep class com.google.android.material.tabs.** { *; }
#Config for About Libraries
-keep class .R
-keep class **.R$* {
<fields>;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -56,7 +56,7 @@
android:name=".ui.modules.login.LoginActivity"
android:configChanges="orientation|screenSize"
android:label="@string/login_title"
android:theme="@style/WulkanowyTheme.NoActionBar"
android:theme="@style/WulkanowyTheme.Login"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.modules.main.MainActivity"
@ -68,7 +68,7 @@
android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize"
android:label="@string/send_message_title"
android:theme="@style/WulkanowyTheme.NoActionBar"
android:theme="@style/WulkanowyTheme.MessageSend"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"

View File

@ -38,5 +38,13 @@
{
"displayName": "MRmlik12",
"githubUsername": "MRmlik12"
},
{
"displayName": "Damian Czupryn",
"githubUsername": "Daxxxis"
},
{
"displayName": "Kamil Studziński",
"githubUsername": "studzinskik"
}
]

View File

@ -1,11 +1,13 @@
package io.github.wulkanowy
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
import android.webkit.WebView
import androidx.fragment.app.FragmentManager
import androidx.hilt.work.HiltWorkerFactory
import androidx.multidex.MultiDex
import androidx.work.Configuration
@ -46,9 +48,10 @@ class WulkanowyApp : Application(), Configuration.Provider {
MultiDex.install(this)
}
@SuppressLint("UnsafeOptInUsageWarning")
override fun onCreate() {
super.onCreate()
FragmentManager.enableNewStateManager(false)
initializeAppLanguage()
themeManager.applyDefaultTheme()
initLogging()

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber
import javax.inject.Singleton
@ -60,7 +61,8 @@ internal class RepositoryModule {
fun provideDatabase(
@ApplicationContext context: Context,
sharedPrefProvider: SharedPrefProvider,
) = AppDatabase.newInstance(context, sharedPrefProvider)
appInfo: AppInfo
) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo)
@Singleton
@Provides

View File

@ -6,7 +6,6 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
@ -85,12 +84,15 @@ import io.github.wulkanowy.data.db.migrations.Migration30
import io.github.wulkanowy.data.db.migrations.Migration31
import io.github.wulkanowy.data.db.migrations.Migration32
import io.github.wulkanowy.data.db.migrations.Migration33
import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration35
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Singleton
@Singleton
@ -130,53 +132,55 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 33
const val VERSION_SCHEMA = 35
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
Migration2(),
Migration3(),
Migration4(),
Migration5(),
Migration6(),
Migration7(),
Migration8(),
Migration9(),
Migration10(),
Migration11(),
Migration12(),
Migration13(),
Migration14(),
Migration15(),
Migration16(),
Migration17(),
Migration18(),
Migration19(sharedPrefProvider),
Migration20(),
Migration21(),
Migration22(),
Migration23(),
Migration24(),
Migration25(),
Migration26(),
Migration27(),
Migration28(),
Migration29(),
Migration30(),
Migration31(),
Migration32(),
Migration33()
)
}
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
Migration3(),
Migration4(),
Migration5(),
Migration6(),
Migration7(),
Migration8(),
Migration9(),
Migration10(),
Migration11(),
Migration12(),
Migration13(),
Migration14(),
Migration15(),
Migration16(),
Migration17(),
Migration18(),
Migration19(sharedPrefProvider),
Migration20(),
Migration21(),
Migration22(),
Migration23(),
Migration24(),
Migration25(),
Migration26(),
Migration27(),
Migration28(),
Migration29(),
Migration30(),
Migration31(),
Migration32(),
Migration33(),
Migration34(),
Migration35(appInfo)
)
fun newInstance(context: Context, sharedPrefProvider: SharedPrefProvider): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE)
.fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1)
.fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(*getMigrations(sharedPrefProvider))
.build()
}
fun newInstance(
context: Context,
sharedPrefProvider: SharedPrefProvider,
appInfo: AppInfo
) = Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE)
.fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1)
.fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(*getMigrations(sharedPrefProvider, appInfo))
.build()
}
abstract val studentDao: StudentDao

View File

@ -13,4 +13,7 @@ interface LuckyNumberDao : BaseDao<LuckyNumber> {
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date = :date")
fun load(studentId: Int, date: LocalDate): Flow<LuckyNumber?>
@Query("SELECT * FROM LuckyNumbers WHERE student_id = :studentId AND date >= :start AND date <= :end")
fun getAll(studentId: Int, start: LocalDate, end: LocalDate): Flow<List<LuckyNumber>>
}

View File

@ -9,6 +9,6 @@ import javax.inject.Singleton
@Dao
interface RecipientDao : BaseDao<Recipient> {
@Query("SELECT * FROM Recipients WHERE student_id = :userLoginId AND unit_id = :unitId AND role = :role")
suspend fun loadAll(userLoginId: Int, unitId: Int, role: Int): List<Recipient>
@Query("SELECT * FROM Recipients WHERE student_id = :studentId AND unit_id = :unitId AND role = :role")
suspend fun loadAll(studentId: Int, unitId: Int, role: Int): List<Recipient>
}

View File

@ -8,7 +8,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton
@ -23,13 +23,13 @@ interface StudentDao {
suspend fun delete(student: Student)
@Update(entity = Student::class)
suspend fun update(studentNick: StudentNick)
suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id")
suspend fun loadById(id: Int): Student?
suspend fun loadById(id: Long): Student?
@Query("SELECT * FROM Students")
suspend fun loadAll(): List<Student>

View File

@ -10,7 +10,7 @@ import java.time.LocalDateTime
data class Message(
@ColumnInfo(name = "student_id")
val studentId: Int,
val studentId: Long,
@ColumnInfo(name = "real_id")
val realId: Int,

View File

@ -9,7 +9,7 @@ import java.io.Serializable
data class Recipient(
@ColumnInfo(name = "student_id")
val userLoginId: Int,
val studentId: Int,
@ColumnInfo(name = "real_id")
val realId: String,

View File

@ -81,4 +81,7 @@ data class Student(
var id: Long = 0
var nick = ""
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}

View File

@ -1,13 +1,17 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity
data class StudentNick(
data class StudentNickAndAvatar(
val nick: String
val nick: String,
@ColumnInfo(name = "avatar_color")
var avatarColor: Long
) : Serializable {

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration34 : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DELETE FROM ReportingUnits")
database.execSQL("DELETE FROM Recipients")
}
}

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.data.db.migrations
import androidx.core.database.getLongOrNull
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.utils.AppInfo
class Migration35(private val appInfo: AppInfo) : Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0")
val studentsCursor = database.query("SELECT * FROM Students")
while (studentsCursor.moveToNext()) {
val studentId = studentsCursor.getLongOrNull(0)
database.execSQL(
"""UPDATE Students
SET avatar_color = ${appInfo.defaultColorsForAvatar.random()}
WHERE id = $studentId"""
)
}
}
}

View File

@ -5,10 +5,9 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject as SdkGradeStatisticsSubject
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester
import io.github.wulkanowy.sdk.pojo.GradePointsStatistics as SdkGradePointsStatistics
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSemester as SdkGradeStatisticsSemester
import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject as SdkGradeStatisticsSubject
@JvmName("mapToEntitiesSubject")
fun List<SdkGradeStatisticsSubject>.mapToEntities(semester: Semester) = map {
@ -51,7 +50,7 @@ fun List<SdkGradePointsStatistics>.mapToEntities(semester: Semester) = map {
fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.classAmounts.isEmpty() }.map {
GradeStatisticsItem(
type = ViewType.PARTIAL,
type = GradeStatisticsItem.DataType.PARTIAL,
average = it.classAverage,
partial = it,
points = null,
@ -61,7 +60,7 @@ fun List<GradePartialStatistics>.mapPartialToStatisticItems() = filterNot { it.c
fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it.amounts.isEmpty() }.map {
GradeStatisticsItem(
type = ViewType.SEMESTER,
type = GradeStatisticsItem.DataType.SEMESTER,
partial = null,
points = null,
average = "",
@ -71,7 +70,7 @@ fun List<GradeSemesterStatistics>.mapSemesterToStatisticItems() = filterNot { it
fun List<GradePointsStatistics>.mapPointsToStatisticsItems() = map {
GradeStatisticsItem(
type = ViewType.POINTS,
type = GradeStatisticsItem.DataType.POINTS,
partial = null,
semester = null,
average = "",

View File

@ -4,14 +4,14 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import java.time.LocalDateTime
import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(student: Student) = map {
Message(
studentId = student.id.toInt(),
studentId = student.id,
realId = it.id ?: 0,
messageId = it.messageId ?: 0,
sender = it.sender?.name.orEmpty(),

View File

@ -5,7 +5,7 @@ import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkRecipient>.mapToEntities(userLoginId: Int) = map {
Recipient(
userLoginId = userLoginId,
studentId = userLoginId,
realId = it.id,
realName = it.name,
name = it.shortName,

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.sdk.pojo.ReportingUnit as SdkReportingUnit
fun List<SdkReportingUnit>.mapToEntities(student: Student) = map {
ReportingUnit(
studentId = student.studentId,
studentId = student.id.toInt(),
unitId = it.id,
roles = it.roles,
senderId = it.senderId,

View File

@ -5,7 +5,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import java.time.LocalDateTime
import io.github.wulkanowy.sdk.pojo.Student as SdkStudent
fun List<SdkStudent>.mapToEntities(password: String = "") = map {
fun List<SdkStudent>.mapToEntities(password: String = "", colors: List<Long>) = map {
StudentWithSemesters(
student = Student(
email = it.email,
@ -28,8 +28,10 @@ fun List<SdkStudent>.mapToEntities(password: String = "") = map {
mobileBaseUrl = it.mobileBaseUrl,
privateKey = it.privateKey,
certificateKey = it.certificateKey,
loginMode = it.loginMode.name
),
loginMode = it.loginMode.name,
).apply {
avatarColor = colors.random()
},
semesters = it.semesters.mapToEntities(it.studentId)
)
}

View File

@ -3,11 +3,10 @@ package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.GradePartialStatistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
data class GradeStatisticsItem(
val type: ViewType,
val type: DataType,
val average: String,
@ -16,4 +15,11 @@ data class GradeStatisticsItem(
val semester: GradeSemesterStatistics?,
val points: GradePointsStatistics?
)
) {
enum class DataType {
SEMESTER,
PARTIAL,
POINTS,
}
}

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@ -27,9 +28,12 @@ class AttendanceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance"
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
fetch = {

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -20,9 +21,12 @@ class AttendanceSummaryRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance_summary"
fun getAttendanceSummary(student: Student, semester: Semester, subjectId: Int, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = {

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class CompletedLessonsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "completed"
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
fetch = {

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -20,9 +21,12 @@ class ConferenceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "conference"
fun getConferences(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { conferenceDb.loadAll(semester.diaryId, student.studentId) },
fetch = {

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.startExamsDay
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class ExamRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "exam"
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { examDb.loadAll(semester.diaryId, semester.studentId, start.startExamsDay, start.endExamsDay) },
fetch = {

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDateTime
import javax.inject.Inject
import javax.inject.Singleton
@ -28,14 +29,20 @@ class GradeRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "grade"
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
shouldFetch = { (details, summaries) -> details.isEmpty() || summaries.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = {
gradeDb.loadAll(semester.semesterId, semester.studentId).combine(gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)) { details, summaries ->
details to summaries
}
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries }
},
fetch = {
val (details, summary) = sdk.init(student)
@ -92,19 +99,27 @@ class GradeRepository @Inject constructor(
}
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isRead } }
return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isRead }
}
}
fun getNotNotifiedGrades(semester: Semester): Flow<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { grade -> !grade.isNotified } }
return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isNotified }
}
}
fun getNotNotifiedPredictedGrades(semester: Semester): Flow<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } }
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified }
}
}
fun getNotNotifiedFinalGrades(semester: Semester): Flow<List<GradeSummary>> {
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } }
return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified }
}
}
suspend fun updateGrade(grade: Grade) {

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@ -30,11 +31,16 @@ class GradeStatisticsRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val partialMutex = Mutex()
private val semesterMutex = Mutex()
private val pointsMutex = Mutex()
private val partialCacheKey = "grade_stats_partial"
private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points"
fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = partialMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) },
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
@ -71,6 +77,7 @@ class GradeStatisticsRepository @Inject constructor(
)
fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = semesterMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) },
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
@ -112,6 +119,7 @@ class GradeStatisticsRepository @Inject constructor(
)
fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = pointsMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) },
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -24,9 +25,12 @@ class HomeworkRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "homework"
fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) },
fetch = {

View File

@ -9,6 +9,8 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDate.now
import javax.inject.Inject
import javax.inject.Singleton
@ -19,7 +21,10 @@ class LuckyNumberRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) },
fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) },
@ -33,6 +38,9 @@ class LuckyNumberRepository @Inject constructor(
}
)
fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) =
luckyNumberDb.getAll(student.studentId, start, end)
suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map {
if (it?.isNotified == false) it else null
}.first()

View File

@ -20,6 +20,7 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -33,10 +34,13 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message"
@Suppress("UNUSED_PARAMETER")
fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) },

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class MobileDeviceRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "devices"
fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) },
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = {

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -23,9 +24,12 @@ class NoteRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "note"
fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { noteDb.loadAll(student.studentId) },
fetch = {

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
@ -18,26 +19,43 @@ class PreferencesRepository @Inject constructor(
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
val isShowPresent: Boolean
get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present)
get() = getBoolean(
R.string.pref_key_attendance_present,
R.bool.pref_default_attendance_present
)
val gradeAverageMode: GradeAverageMode
get() = GradeAverageMode.getByValue(getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode))
get() = GradeAverageMode.getByValue(
getString(
R.string.pref_key_grade_average_mode,
R.string.pref_default_grade_average_mode
)
)
val gradeAverageForceCalc: Boolean
get() = getBoolean(R.string.pref_key_grade_average_force_calc, R.bool.pref_default_grade_average_force_calc)
get() = getBoolean(
R.string.pref_key_grade_average_force_calc,
R.bool.pref_default_grade_average_force_calc
)
val isGradeExpandable: Boolean
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
val showAllSubjectsOnStatisticsList: Boolean
get() = getBoolean(R.string.pref_key_grade_statistics_list, R.bool.pref_default_grade_statistics_list)
get() = getBoolean(
R.string.pref_key_grade_statistics_list,
R.bool.pref_default_grade_statistics_list
)
val appThemeKey = context.getString(R.string.pref_key_app_theme)
val appTheme: String
get() = getString(appThemeKey, R.string.pref_default_app_theme)
val gradeColorTheme: String
get() = getString(R.string.pref_key_grade_color_scheme, R.string.pref_default_grade_color_scheme)
get() = getString(
R.string.pref_key_grade_color_scheme,
R.string.pref_default_grade_color_scheme
)
val appLanguageKey = context.getString(R.string.pref_key_app_language)
val appLanguage
@ -55,50 +73,90 @@ class PreferencesRepository @Inject constructor(
val isServicesOnlyWifi: Boolean
get() = getBoolean(servicesOnlyWifiKey, R.bool.pref_default_services_wifi_only)
val notificationsEnableKey = context.getString(R.string.pref_key_notifications_enable)
val isNotificationsEnable: Boolean
get() = getBoolean(R.string.pref_key_notifications_enable, R.bool.pref_default_notifications_enable)
get() = getBoolean(notificationsEnableKey, R.bool.pref_default_notifications_enable)
val isUpcomingLessonsNotificationsEnableKey = context.getString(R.string.pref_key_notifications_upcoming_lessons_enable)
val isUpcomingLessonsNotificationsEnableKey =
context.getString(R.string.pref_key_notifications_upcoming_lessons_enable)
val isUpcomingLessonsNotificationsEnable: Boolean
get() = getBoolean(isUpcomingLessonsNotificationsEnableKey, R.bool.pref_default_notification_upcoming_lessons_enable)
get() = getBoolean(
isUpcomingLessonsNotificationsEnableKey,
R.bool.pref_default_notification_upcoming_lessons_enable
)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
val gradePlusModifier: Double
get() = getString(R.string.pref_key_grade_modifier_plus, R.string.pref_default_grade_modifier_plus).toDouble()
get() = getString(
R.string.pref_key_grade_modifier_plus,
R.string.pref_default_grade_modifier_plus
).toDouble()
val gradeMinusModifier: Double
get() = getString(R.string.pref_key_grade_modifier_minus, R.string.pref_default_grade_modifier_minus).toDouble()
get() = getString(
R.string.pref_key_grade_modifier_minus,
R.string.pref_default_grade_modifier_minus
).toDouble()
val fillMessageContent: Boolean
get() = getBoolean(R.string.pref_key_fill_message_content, R.bool.pref_default_fill_message_content)
get() = getBoolean(
R.string.pref_key_fill_message_content,
R.bool.pref_default_fill_message_content
)
val showGroupsInPlan: Boolean
get() = getBoolean(R.string.pref_key_timetable_show_groups, R.bool.pref_default_timetable_show_groups)
get() = getBoolean(
R.string.pref_key_timetable_show_groups,
R.bool.pref_default_timetable_show_groups
)
val showWholeClassPlan: String
get() = getString(R.string.pref_key_timetable_show_whole_class, R.string.pref_default_timetable_show_whole_class)
get() = getString(
R.string.pref_key_timetable_show_whole_class,
R.string.pref_default_timetable_show_whole_class
)
val gradeSortingMode: GradeSortingMode
get() = GradeSortingMode.getByValue(getString(R.string.pref_key_grade_sorting_mode, R.string.pref_default_grade_sorting_mode))
get() = GradeSortingMode.getByValue(
getString(
R.string.pref_key_grade_sorting_mode,
R.string.pref_default_grade_sorting_mode
)
)
val showTimetableTimers: Boolean
get() = getBoolean(R.string.pref_key_timetable_show_timers, R.bool.pref_default_timetable_show_timers)
get() = getBoolean(
R.string.pref_key_timetable_show_timers,
R.bool.pref_default_timetable_show_timers
)
var isHomeworkFullscreen: Boolean
get() = getBoolean(R.string.pref_key_homework_fullscreen, R.bool.pref_default_homework_fullscreen)
get() = getBoolean(
R.string.pref_key_homework_fullscreen,
R.bool.pref_default_homework_fullscreen
)
set(value) = sharedPref.edit().putBoolean("homework_fullscreen", value).apply()
val showSubjectsWithoutGrades: Boolean
get() = getBoolean(R.string.pref_key_subjects_without_grades, R.bool.pref_default_subjects_without_grades)
get() = getBoolean(
R.string.pref_key_subjects_without_grades,
R.bool.pref_default_subjects_without_grades
)
var isKitkatDialogDisabled: Boolean
get() = sharedPref.getBoolean("kitkat_dialog_disabled", false)
set(value) = sharedPref.edit { putBoolean("kitkat_dialog_disabled", value) }
private fun getString(id: Int, default: Int) = getString(context.getString(id), default)
private fun getString(id: String, default: Int) = sharedPref.getString(id, context.getString(default)) ?: context.getString(default)
private fun getString(id: String, default: Int) =
sharedPref.getString(id, context.getString(default)) ?: context.getString(default)
private fun getBoolean(id: Int, default: Int) = getBoolean(context.getString(id), default)
private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default))
private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default))
}

View File

@ -19,22 +19,22 @@ class RecipientRepository @Inject constructor(
) {
suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.senderId)
val old = recipientDb.loadAll(unit.senderId, unit.unitId, role)
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role)
recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old)
}
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> {
return recipientDb.loadAll(unit.senderId, unit.unitId, role).ifEmpty {
return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty {
refreshRecipients(student, unit, role)
recipientDb.loadAll(unit.senderId, unit.unitId, role)
recipientDb.loadAll(unit.studentId, unit.unitId, role)
}
}
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> {
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.userLoginId)
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId)
}
}

View File

@ -18,25 +18,25 @@ class ReportingUnitRepository @Inject constructor(
suspend fun refreshReportingUnits(student: Student) {
val new = sdk.init(student).getReportingUnits().mapToEntities(student)
val old = reportingUnitDb.load(student.studentId)
val old = reportingUnitDb.load(student.id.toInt())
reportingUnitDb.deleteAll(old.uniqueSubtract(new))
reportingUnitDb.insertAll(new.uniqueSubtract(old))
}
suspend fun getReportingUnits(student: Student): List<ReportingUnit> {
return reportingUnitDb.load(student.studentId).ifEmpty {
return reportingUnitDb.load(student.id.toInt()).ifEmpty {
refreshReportingUnits(student)
reportingUnitDb.load(student.studentId)
reportingUnitDb.load(student.id.toInt())
}
}
suspend fun getReportingUnit(student: Student, unitId: Int): ReportingUnit? {
return reportingUnitDb.loadOne(student.studentId, unitId) ?: run {
return reportingUnitDb.loadOne(student.id.toInt(), unitId) ?: run {
refreshReportingUnits(student)
return reportingUnitDb.loadOne(student.studentId, unitId)
return reportingUnitDb.loadOne(student.id.toInt(), unitId)
}
}
}

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -16,8 +17,11 @@ class SchoolRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -16,8 +17,11 @@ class StudentInfoRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {

View File

@ -5,11 +5,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt
@ -23,7 +24,8 @@ class StudentRepository @Inject constructor(
private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk
private val sdk: Sdk,
private val appInfo: AppInfo
) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -35,7 +37,8 @@ class StudentRepository @Inject constructor(
symbol: String,
token: String
): List<StudentWithSemesters> =
sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities()
sdk.getStudentsFromMobileApi(token, pin, symbol, "")
.mapToEntities(colors = appInfo.defaultColorsForAvatar)
suspend fun getStudentsScrapper(
email: String,
@ -44,7 +47,7 @@ class StudentRepository @Inject constructor(
symbol: String
): List<StudentWithSemesters> =
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getStudentsHybrid(
email: String,
@ -52,46 +55,58 @@ class StudentRepository @Inject constructor(
scrapperBaseUrl: String,
symbol: String
): List<StudentWithSemesters> =
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password)
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getSavedStudents(decryptPass: Boolean = true) =
withContext(dispatchers.backgroundThread) {
studentDb.loadStudentsWithSemesters().map {
studentDb.loadStudentsWithSemesters()
.map {
it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = decrypt(student.password)
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
}
}
}
suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) {
studentDb.loadById(id)?.apply {
if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student {
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
} ?: throw NoCurrentStudentException()
return student
}
suspend fun getCurrentStudent(decryptPass: Boolean = true) =
withContext(dispatchers.backgroundThread) {
studentDb.loadCurrent()?.apply {
if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
} ?: throw NoCurrentStudentException()
}
return student
}
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters })
val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student }
.map {
it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
password = withContext(dispatchers.backgroundThread) {
encrypt(password, context)
}
}
}
}
return withContext(dispatchers.backgroundThread) {
studentDb.insertAll(studentsWithSemesters.map { it.student }.map {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
it.copy(password = encrypt(it.password, context))
} else it
})
}
semesterDb.insertSemesters(semesters)
return studentDb.insertAll(students)
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
@ -103,5 +118,6 @@ class StudentRepository @Inject constructor(
suspend fun logoutStudent(student: Student) = studentDb.delete(student)
suspend fun updateStudentNick(studentNick: StudentNick) = studentDb.update(studentNick)
suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) =
studentDb.update(studentNickAndAvatar)
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -17,7 +18,10 @@ class SubjectRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh },
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = {

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -17,7 +18,10 @@ class TeacherRepository @Inject constructor(
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh },
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = {

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@ -31,9 +32,12 @@ class TimetableRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "timetable"
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional) -> timetable.isEmpty() || (additional.isEmpty() && refreshAdditional) || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = {
timetableDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)

View File

@ -37,6 +37,7 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
abstract var presenter: T
override fun onCreate(savedInstanceState: Bundle?) {
inject()
themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
@ -44,7 +45,9 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
if (SDK_INT >= LOLLIPOP) {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)))
setTaskDescription(
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
)
}
}
@ -84,4 +87,9 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
invalidateOptionsMenu()
presenter.onDetachView()
}
//https://github.com/google/dagger/releases/tag/dagger-2.33
protected open fun inject() {
throw UnsupportedOperationException()
}
}

View File

@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
//TODO Use ViewPager2
class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) :
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

View File

@ -42,10 +42,8 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
companion object {
private const val ARGUMENT_KEY = "Data"
fun newInstance(error: Throwable): ErrorDialog {
return ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
}
fun newInstance(error: Throwable) = ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
}
}
@ -57,12 +55,14 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return DialogErrorBinding.inflate(inflater).apply { binding = this }.root
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogErrorBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val stringWriter = StringWriter().apply {
error.printStackTrace(PrintWriter(this))

View File

@ -8,6 +8,9 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import javax.inject.Inject
import javax.inject.Singleton
@ -17,7 +20,13 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
fun applyActivityTheme(activity: AppCompatActivity) {
if (isThemeApplicable(activity)) {
applyDefaultTheme()
if (preferencesRepository.appTheme == "black") activity.setTheme(R.style.WulkanowyTheme_Black)
if (preferencesRepository.appTheme == "black") {
when (activity) {
is MainActivity -> activity.setTheme(R.style.WulkanowyTheme_Black)
is LoginActivity -> activity.setTheme(R.style.WulkanowyTheme_Login_Black)
is SendMessageActivity -> activity.setTheme(R.style.WulkanowyTheme_MessageSend_Black)
}
}
}
}
@ -33,8 +42,13 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
}
private fun isThemeApplicable(activity: AppCompatActivity): Boolean {
return activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES)
.activities.singleOrNull { it.name == activity::class.java.canonicalName }?.theme
.let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar }
return activity.packageManager
.getPackageInfo(activity.packageName, GET_ACTIVITIES)
.activities.singleOrNull { it.name == activity::class.java.canonicalName }
?.theme.let {
it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar
|| it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
}
}
}

View File

@ -1,20 +1,25 @@
package io.github.wulkanowy.ui.base
import android.annotation.SuppressLint
import android.graphics.PorterDuff
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.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() {
class WidgetConfigureAdapter @Inject constructor() :
RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() {
var items = emptyList<Pair<Student, Boolean>>()
var items = emptyList<StudentWithSemesters>()
var selectedId = -1L
var onClickListener: (Student) -> Unit = {}
@ -26,17 +31,33 @@ class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<Widget
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val (student, isCurrent) = items[position]
val (student, semesters) = items[position]
val semester = semesters.maxByOrNull { it.semesterId }
val context = holder.binding.root.context
val checkBackgroundColor = context.getThemeAttrColor(R.attr.colorSurface)
val avatar = context.createNameInitialsDrawable(student.nickOrName, student.avatarColor)
val isDuplicatedStudent = items.filter {
val studentToCompare = it.student
studentToCompare.studentId == student.studentId
&& studentToCompare.schoolSymbol == student.schoolSymbol
&& studentToCompare.symbol == student.symbol
}.size > 1
with(holder.binding) {
accountItemName.text = "${student.nickOrName} ${student.className}"
accountItemName.text = "${student.nickOrName} ${semester?.diaryName.orEmpty()}"
accountItemSchool.text = student.schoolName
accountItemImage.setImageDrawable(avatar)
with(accountItemImage) {
val colorImage = if (isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
with(accountItemAccountType) {
setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
isVisible = isDuplicatedStudent
}
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN)
with(accountItemCheck) {
isVisible = student.id == selectedId
borderColor = checkBackgroundColor
circleColor = checkBackgroundColor
}
root.setOnClickListener { onClickListener(student) }

View File

@ -4,9 +4,9 @@ import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import dagger.hilt.android.AndroidEntryPoint
@ -26,14 +26,9 @@ class LicenseFragment : BaseFragment<FragmentLicenseBinding>(R.layout.fragment_l
@Inject
lateinit var licenseAdapter: LicenseAdapter
private val libs by lazy { Libs(requireContext()) }
override val titleStringId get() = R.string.license_title
override val appLibraries: ArrayList<Library>?
get() = context?.let {
libs.prepareLibraries(it, emptyArray(), emptyArray(), autoDetect = true, checkCachedDetection = true, sort = true)
}
override val appLibraries by lazy { Libs(requireContext()).libraries }
companion object {
fun newInstance() = LicenseFragment()
@ -63,7 +58,7 @@ class LicenseFragment : BaseFragment<FragmentLicenseBinding>(R.layout.fragment_l
override fun openLicense(licenseHtml: String) {
context?.let {
AlertDialog.Builder(it).apply {
MaterialAlertDialogBuilder(it).apply {
setTitle(R.string.license_dialog_title)
setMessage(licenseHtml.parseAsHtml())
setPositiveButton(android.R.string.ok) { _, _ -> }

View File

@ -5,7 +5,7 @@ import io.github.wulkanowy.ui.base.BaseView
interface LicenseView : BaseView {
val appLibraries: ArrayList<Library>?
val appLibraries: List<Library>
fun initView()

View File

@ -1,16 +1,17 @@
package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.HeaderAccountBinding
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
@ -72,9 +73,12 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
binding: ItemAccountBinding,
studentWithSemesters: StudentWithSemesters
) {
val student = studentWithSemesters.student
val semesters = studentWithSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId }
val context = binding.root.context
val (student, semesters) = studentWithSemesters
val semester = semesters.maxByOrNull { it.semesterId }
val avatar = context.createNameInitialsDrawable(student.nickOrName, student.avatarColor)
val checkBackgroundColor =
context.getThemeAttrColor(if (isAccountQuickDialogMode) R.attr.colorBackgroundFloating else R.attr.colorSurface)
val isDuplicatedStudent = items.filter {
if (it.value !is StudentWithSemesters) return@filter false
val studentToCompare = it.value.student
@ -85,17 +89,19 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
}.size > 1 && isAccountQuickDialogMode
with(binding) {
accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}"
accountItemName.text = "${student.nickOrName} ${semester?.diaryName.orEmpty()}"
accountItemSchool.text = studentWithSemesters.student.schoolName
accountItemAccountType.setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
accountItemAccountType.visibility = if (isDuplicatedStudent) VISIBLE else GONE
accountItemImage.setImageDrawable(avatar)
with(accountItemImage) {
val colorImage =
if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
with(accountItemAccountType) {
setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
isVisible = isDuplicatedStudent
}
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN)
with(accountItemCheck) {
isVisible = student.isCurrent
borderColor = checkBackgroundColor
circleColor = checkBackgroundColor
}
root.setOnClickListener { onClickListener(studentWithSemesters) }

View File

@ -34,25 +34,20 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override val titleStringId = R.string.account_title
override var subtitleString = ""
override val isViewEmpty get() = accountAdapter.items.isEmpty()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
binding.accountErrorRetry.setOnClickListener { presenter.onRetry() }
binding.accountErrorDetails.setOnClickListener { presenter.onDetailsClick() }
binding.accountRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
@ -60,9 +55,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
accountAdapter.onClickListener = presenter::onItemSelected
with(binding) {
accountAdd.setOnClickListener { presenter.onAddSelected() }
}
binding.accountAdd.setOnClickListener { presenter.onAddSelected() }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -84,28 +77,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) {
(activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance(
studentWithSemesters
)
AccountDetailsFragment.newInstance(studentWithSemesters)
)
}
override fun showErrorView(show: Boolean) {
binding.accountError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.accountErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.accountProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
with(binding) {
accountRecycler.visibility = if (show) View.VISIBLE else View.GONE
accountAdd.visibility = if (show) View.VISIBLE else View.GONE
}
}
}

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -16,28 +15,13 @@ class AccountPresenter @Inject constructor(
studentRepository: StudentRepository,
) : BasePresenter<AccountView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: AccountView) {
super.onAttachView(view)
view.initView()
Timber.i("Account view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
fun onAddSelected() {
Timber.i("Select add account")
view?.openLoginView()
@ -47,6 +31,24 @@ class AccountPresenter @Inject constructor(
view?.openAccountDetailsView(studentWithSemesters)
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading account data started")
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.launch("load")
}
private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> {
return items.groupBy {
Account("${it.student.userName} (${it.student.email})", it.student.isParent)
@ -60,45 +62,4 @@ class AccountPresenter @Inject constructor(
}
.flatten()
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading account data started")
view?.run {
showProgress(true)
showContent(false)
}
}
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
view?.run {
showContent(true)
showErrorView(false)
}
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.showProgress(false) }
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showContent(false)
showProgress(false)
} else showError(message, error)
}
}
}

View File

@ -5,8 +5,6 @@ import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<AccountItem<*>>)
@ -14,13 +12,4 @@ interface AccountView : BaseView {
fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
}

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.get
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
@ -18,6 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
@ -31,8 +33,6 @@ class AccountDetailsFragment :
override val titleStringId = R.string.account_details_title
override var subtitleString = ""
companion object {
private const val ARGUMENT_KEY = "Data"
@ -88,8 +88,15 @@ class AccountDetailsFragment :
override fun showAccountData(student: Student) {
with(binding) {
accountDetailsCheck.isVisible = student.isCurrent
accountDetailsName.text = student.nickOrName
accountDetailsSchool.text = student.schoolName
accountDetailsAvatar.setImageDrawable(
requireContext().createNameInitialsDrawable(
student.nickOrName,
student.avatarColor
)
)
}
}

View File

@ -21,7 +21,7 @@ class AccountDetailsPresenter @Inject constructor(
private val syncManager: SyncManager
) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) {
private lateinit var studentWithSemesters: StudentWithSemesters
private var studentWithSemesters: StudentWithSemesters? = null
private lateinit var lastError: Throwable
@ -69,10 +69,10 @@ class AccountDetailsPresenter @Inject constructor(
}
Status.SUCCESS -> {
Timber.i("Loading account details view result: Success")
studentWithSemesters = it.data!!
studentWithSemesters = it.data
view?.run {
showAccountData(studentWithSemesters.student)
enableSelectStudentButton(!studentWithSemesters.student.isCurrent)
showAccountData(studentWithSemesters!!.student)
enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent)
showContent(true)
showErrorView(false)
}
@ -88,17 +88,23 @@ class AccountDetailsPresenter @Inject constructor(
}
fun onAccountEditSelected() {
view?.showAccountEditDetailsDialog(studentWithSemesters.student)
studentWithSemesters?.let {
view?.showAccountEditDetailsDialog(it.student)
}
}
fun onStudentInfoSelected(infoType: StudentInfoView.Type) {
view?.openStudentInfoView(infoType, studentWithSemesters)
studentWithSemesters?.let {
view?.openStudentInfoView(infoType, it)
}
}
fun onStudentSelect() {
Timber.i("Select student ${studentWithSemesters.student.id}")
if (studentWithSemesters == null) return
flowWithResource { studentRepository.switchStudent(studentWithSemesters) }
Timber.i("Select student ${studentWithSemesters!!.student.id}")
flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
@ -122,8 +128,10 @@ class AccountDetailsPresenter @Inject constructor(
}
fun onLogoutConfirm() {
if (studentWithSemesters == null) return
flowWithResource {
val studentToLogout = studentWithSemesters.student
val studentToLogout = studentWithSemesters!!.student
studentRepository.logoutStudent(studentToLogout)
val students = studentRepository.getSavedStudents(false)
@ -143,7 +151,7 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters.student.isCurrent -> {
studentWithSemesters!!.student.isCurrent -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}

View File

@ -0,0 +1,90 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.databinding.ItemAccountEditColorBinding
import javax.inject.Inject
class AccountEditColorAdapter @Inject constructor() :
RecyclerView.Adapter<AccountEditColorAdapter.ViewHolder>() {
var items = listOf<Int>()
var selectedColor = 0
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAccountEditColorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("RestrictedApi", "NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
accountEditItemColor.setImageDrawable(GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(item)
})
accountEditItemColorContainer.foreground = item.createForegroundDrawable()
accountEditCheck.isVisible = selectedColor == item
root.setOnClickListener {
val oldSelectedPosition = items.indexOf(selectedColor)
selectedColor = item
notifyItemChanged(oldSelectedPosition)
notifyItemChanged(position)
}
}
}
private fun Int.createForegroundDrawable(): Drawable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(Color.BLACK)
}
RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
} else {
val foreground = StateListDrawable().apply {
alpha = 80
setEnterFadeDuration(250)
setExitFadeDuration(250)
}
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(this@createForegroundDrawable.rippleColor)
}
foreground.apply {
addState(intArrayOf(android.R.attr.state_pressed), mask)
addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT))
}
}
private inline val Int.rippleColor: Int
get() {
val hsv = FloatArray(3)
Color.colorToHSV(this, hsv)
hsv[2] = hsv[2] * 0.5f
return Color.HSVToColor(hsv)
}
class ViewHolder(val binding: ItemAccountEditColorBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding
@ -16,6 +17,9 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
@Inject
lateinit var presenter: AccountEditPresenter
@Inject
lateinit var accountEditColorAdapter: AccountEditColorAdapter
companion object {
private const val ARGUMENT_KEY = "student_with_semesters"
@ -48,8 +52,30 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
with(binding) {
accountEditDetailsCancel.setOnClickListener { dismiss() }
accountEditDetailsSave.setOnClickListener {
presenter.changeStudentNick(binding.accountEditDetailsNickText.text.toString())
presenter.changeStudentNickAndAvatar(
binding.accountEditDetailsNickText.text.toString(),
accountEditColorAdapter.selectedColor
)
}
with(binding.accountEditColors) {
layoutManager = GridLayoutManager(context, 4)
adapter = accountEditColorAdapter
}
}
}
override fun updateSelectedColorData(color: Int) {
with(accountEditColorAdapter) {
selectedColor = color
notifyDataSetChanged()
}
}
override fun updateColorsData(colors: List<Int>) {
with(accountEditColorAdapter) {
items = colors
notifyDataSetChanged()
}
}

View File

@ -2,10 +2,11 @@ package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
@ -13,12 +14,15 @@ import timber.log.Timber
import javax.inject.Inject
class AccountEditPresenter @Inject constructor(
private val appInfo: AppInfo,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AccountEditView>(errorHandler, studentRepository) {
lateinit var student: Student
private val colors = appInfo.defaultColorsForAvatar.map { it.toInt() }
fun onAttachView(view: AccountEditView, student: Student) {
super.onAttachView(view)
this.student = student
@ -28,27 +32,49 @@ class AccountEditPresenter @Inject constructor(
showCurrentNick(student.nick.trim())
}
Timber.i("Account edit dialog view was initialized")
loadData()
view.updateColorsData(colors)
}
fun changeStudentNick(nick: String) {
private fun loadData() {
flowWithResource {
studentRepository.getStudentById(student.id, false).avatarColor
}.onEach { resource ->
when (resource.status) {
Status.LOADING -> Timber.i("Attempt to load student")
Status.SUCCESS -> {
view?.updateSelectedColorData(resource.data?.toInt()!!)
Timber.i("Attempt to load student: Success")
}
Status.ERROR -> {
Timber.i("Attempt to load student: An exception occurred")
errorHandler.dispatch(resource.error!!)
}
}
}.launch("load_data")
}
fun changeStudentNickAndAvatar(nick: String, avatarColor: Int) {
flowWithResource {
val studentNick =
StudentNick(nick = nick.trim()).apply { id = student.id }
studentRepository.updateStudentNick(studentNick)
StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong())
.apply { id = student.id }
studentRepository.updateStudentNickAndAvatar(studentNick)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student nick")
Status.LOADING -> Timber.i("Attempt to change a student nick and avatar")
Status.SUCCESS -> {
Timber.i("Change a student nick result: Success")
Timber.i("Change a student nick and avatar result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
Timber.i("Change a student nick and avatar result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.popView() }
.launch()
.launch("update_student")
}
}

View File

@ -11,4 +11,8 @@ interface AccountEditView : BaseView {
fun recreateMainView()
fun showCurrentNick(nick: String)
fun updateSelectedColorData(color: Int)
fun updateColorsData(colors: List<Int>)
}

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.DialogAccountQuickBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.account.AccountAdapter
@ -24,7 +25,15 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
lateinit var presenter: AccountQuickPresenter
companion object {
fun newInstance() = AccountQuickDialog()
private const val STUDENTS_ARGUMENT_KEY = "students"
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
AccountQuickDialog().apply {
arguments = Bundle().apply {
putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray())
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -38,8 +47,12 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
savedInstanceState: Bundle?
) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(this)
val studentsWithSemesters =
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList()
presenter.onAttachView(this, studentsWithSemesters)
}
override fun initView() {

View File

@ -17,11 +17,15 @@ class AccountQuickPresenter @Inject constructor(
studentRepository: StudentRepository
) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) {
override fun onAttachView(view: AccountQuickView) {
private lateinit var studentsWithSemesters: List<StudentWithSemesters>
fun onAttachView(view: AccountQuickView, studentsWithSemesters: List<StudentWithSemesters>) {
super.onAttachView(view)
this.studentsWithSemesters = studentsWithSemesters
view.initView()
Timber.i("Account quick dialog view was initialized")
loadData()
view.updateData(createAccountItems(studentsWithSemesters))
}
fun onManagerSelected() {
@ -57,22 +61,6 @@ class AccountQuickPresenter @Inject constructor(
.launch("switch")
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading account data started")
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.launch()
}
private fun createAccountItems(items: List<StudentWithSemesters>) = items.map {
AccountItem(it, AccountItem.ViewType.ITEM)
}

View File

@ -18,12 +18,11 @@ class AttendanceDialog : DialogFragment() {
private lateinit var attendance: Attendance
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Attendance): AttendanceDialog {
return AttendanceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
fun newInstance(exam: Attendance) = AttendanceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
}
@ -35,12 +34,14 @@ class AttendanceDialog : DialogFragment() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogAttendanceBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
attendanceDialogSubject.text = attendance.subject

View File

@ -192,7 +192,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
}
override fun showContent(show: Boolean) {
binding. attendanceRecycler.visibility = if (show) VISIBLE else GONE
binding.attendanceRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showRefresh(show: Boolean) {

View File

@ -190,35 +190,48 @@ class AttendancePresenter @Inject constructor(
flowWithResourceIn {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
attendanceRepository.getAttendance(student, semester, currentDate, currentDate, forceRefresh)
attendanceRepository.getAttendance(
student,
semester,
currentDate,
currentDate,
forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> {
view?.showExcuseButton(false)
if (!it.data.isNullOrEmpty()) {
val filteredAttendance = if (prefRepository.isShowPresent) {
it.data
} else {
it.data.filter { item -> !item.presence }
}
view?.run {
enableSwipe(true)
showRefresh(true)
showProgress(false)
showContent(true)
updateData(it.data.let { items ->
if (prefRepository.isShowPresent) items
else items.filter { item -> !item.presence }
}.sortedBy { item -> item.number })
showEmpty(filteredAttendance.isEmpty())
showContent(filteredAttendance.isNotEmpty())
updateData(filteredAttendance.sortedBy { item -> item.number })
}
}
}
Status.SUCCESS -> {
Timber.i("Loading attendance result: Success")
val filteredAttendance = if (prefRepository.isShowPresent) {
it.data.orEmpty()
} else {
it.data?.filter { item -> !item.presence }.orEmpty()
}
view?.apply {
updateData(it.data!!.let { items ->
if (prefRepository.isShowPresent) items
else items.filter { item -> !item.presence }
}.sortedBy { item -> item.number })
showEmpty(it.data.isEmpty())
updateData(filteredAttendance.sortedBy { item -> item.number })
showEmpty(filteredAttendance.isEmpty())
showErrorView(false)
showContent(it.data.isNotEmpty())
showExcuseButton(it.data.any { item -> item.excusable })
showContent(filteredAttendance.isNotEmpty())
showExcuseButton(filteredAttendance.any { item -> item.excusable })
}
analytics.logEvent(
"load_data",

View File

@ -17,12 +17,11 @@ class ExamDialog : DialogFragment() {
private lateinit var exam: Exam
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Exam): ExamDialog {
return ExamDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
fun newInstance(exam: Exam) = ExamDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
}
@ -34,12 +33,14 @@ class ExamDialog : DialogFragment() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogExamBinding.inflate(inflater).apply { binding = this }.root
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogExamBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
examDialogSubjectValue.text = exam.subject

View File

@ -68,7 +68,7 @@ class GradeAverageProvider @Inject constructor(
forceRefresh: Boolean,
averageMode: GradeAverageMode
): Flow<Resource<List<GradeSubject>>> {
val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester =
semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
@ -78,15 +78,18 @@ class GradeAverageProvider @Inject constructor(
if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects
val firstSemesterGradeSubjects =
getGradeSubjects(student, firstSemester, forceRefresh)
val firstSemesterGradeSubjects = getGradeSubjects(student, firstSemester, forceRefresh)
return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject ->
if (firstSemesterGradeSubject.status == Status.ERROR) {
return@combine firstSemesterGradeSubject
}
val isAnyAverage = secondSemesterGradeSubject.data.orEmpty().any { it.average != .0 }
val isAnyVulcanAverageInFirstSemester =
firstSemesterGradeSubject.data.orEmpty().any { it.isVulcanAverage }
val isAnyVulcanAverageInSecondSemester =
secondSemesterGradeSubject.data.orEmpty().any { it.isVulcanAverage }
val updatedData = secondSemesterGradeSubject.data?.map { secondSemesterSubject ->
val firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty()
.singleOrNull { it.subject == secondSemesterSubject.subject }
@ -94,16 +97,16 @@ class GradeAverageProvider @Inject constructor(
val updatedAverage = if (averageMode == ALL_YEAR) {
calculateAllYearAverage(
student = student,
isAnyAverage = isAnyAverage,
gradeAverageForceCalc = gradeAverageForceCalc,
isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
isGradeAverageForceCalc = isGradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject
)
} else {
calculateBothSemestersAverage(
student = student,
isAnyAverage = isAnyAverage,
gradeAverageForceCalc = gradeAverageForceCalc,
isAnyVulcanAverage = isAnyVulcanAverageInFirstSemester || isAnyVulcanAverageInSecondSemester,
isGradeAverageForceCalc = isGradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject
)
@ -116,11 +119,11 @@ class GradeAverageProvider @Inject constructor(
private fun calculateAllYearAverage(
student: Student,
isAnyAverage: Boolean,
gradeAverageForceCalc: Boolean,
isAnyVulcanAverage: Boolean,
isGradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject?
) = if (!isAnyAverage || gradeAverageForceCalc) {
) = if (!isAnyVulcanAverage || isGradeAverageForceCalc) {
val updatedSecondSemesterGrades =
secondSemesterSubject.grades.updateModifiers(student)
val updatedFirstSemesterGrades =
@ -133,20 +136,23 @@ class GradeAverageProvider @Inject constructor(
private fun calculateBothSemestersAverage(
student: Student,
isAnyAverage: Boolean,
gradeAverageForceCalc: Boolean,
isAnyVulcanAverage: Boolean,
isGradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject?
) = if (!isAnyAverage || gradeAverageForceCalc) {
val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage()
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage() ?: secondSemesterAverage
): Double {
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
(secondSemesterAverage + firstSemesterAverage) / divider
} else {
(secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / 2
return if (!isAnyVulcanAverage || isGradeAverageForceCalc) {
val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage()
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage() ?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider
} else {
(secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / divider
}
}
private fun getGradeSubjects(
@ -154,7 +160,7 @@ class GradeAverageProvider @Inject constructor(
semester: Semester,
forceRefresh: Boolean
): Flow<Resource<List<GradeSubject>>> {
val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
val isGradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh)
.map { res ->
@ -163,20 +169,21 @@ class GradeAverageProvider @Inject constructor(
val allGrades = details.orEmpty().groupBy { it.subject }
val items = summaries?.emulateEmptySummaries(
student,
semester,
allGrades.toList(),
isAnyAverage
student = student,
semester = semester,
grades = allGrades.toList(),
calcAverage = isAnyAverage
)?.map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeSubject(
subject = summary.subject,
average = if (!isAnyAverage || gradeAverageForceCalc) {
average = if (!isAnyAverage || isGradeAverageForceCalc) {
grades.updateModifiers(student).calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
grades = grades,
isVulcanAverage = isAnyAverage
)
}

View File

@ -10,6 +10,7 @@ import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.databinding.FragmentGradeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
@ -63,11 +64,13 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun initView() {
with(pagerAdapter) {
containerId = binding.gradeViewPager.id
addFragmentsWithTitle(mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
))
addFragmentsWithTitle(
mapOf(
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
)
)
}
with(binding.gradeViewPager) {
@ -119,11 +122,9 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
semesterSwitchMenu?.isVisible = show
}
override fun showSemesterDialog(selectedIndex: Int) {
val choices = arrayOf(
getString(R.string.grade_semester, 1),
getString(R.string.grade_semester, 2)
)
override fun showSemesterDialog(selectedIndex: Int, semesters: List<Semester>) {
val choices = semesters.map { getString(R.string.grade_semester, it.semesterName) }
.toTypedArray()
AlertDialog.Builder(requireContext())
.setSingleChoiceItems(choices, selectedIndex) { dialog, which ->
@ -137,7 +138,10 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun setCurrentSemesterName(semester: Int, schoolYear: Int) {
subtitleString = getString(R.string.grade_subtitle, semester, schoolYear, schoolYear + 1)
(activity as MainView).setViewSubTitle(subtitleString)
if (isVisible) {
(activity as MainView?)?.setViewSubTitle(subtitleString)
}
}
fun onChildRefresh() {
@ -149,7 +153,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentLoadData(semesterId, forceRefresh)
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
}
override fun notifyChildParentReselected(index: Int) {

View File

@ -49,7 +49,9 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) view?.showSemesterDialog(selectedIndex - 1)
if (semesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
}
return true
}
@ -137,11 +139,17 @@ class GradePresenter @Inject constructor(
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
semesters.first { it.semesterName == selectedIndex }.semesterId.also {
if (forceRefresh || loadedSemesterId[index] != it) {
Timber.i("Load grade child view index: $index")
view?.notifyChildLoadData(index, it, forceRefresh)
}
val newSelectedSemesterId = try {
semesters.first { it.semesterName == selectedIndex }.semesterId
} catch (e: NoSuchElementException) {
Timber.e(e, "Selected semester no exists")
return
}
if (forceRefresh || loadedSemesterId[index] != newSelectedSemesterId) {
Timber.i("Load grade child view index: $index")
view?.notifyChildLoadData(index, newSelectedSemesterId, forceRefresh)
}
}

View File

@ -8,5 +8,6 @@ data class GradeSubject(
val average: Double,
val points: String,
val summary: GradeSummary,
val grades: List<Grade>
val grades: List<Grade>,
val isVulcanAverage: Boolean
)

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.ui.base.BaseView
interface GradeView : BaseView {
@ -18,7 +19,7 @@ interface GradeView : BaseView {
fun showSemesterSwitch(show: Boolean)
fun showSemesterDialog(selectedIndex: Int)
fun showSemesterDialog(selectedIndex: Int, semesters: List<Semester>)
fun setCurrentSemesterName(semester: Int, schoolYear: Int)

View File

@ -24,17 +24,18 @@ class GradeDetailsDialog : DialogFragment() {
private lateinit var colorScheme: String
companion object {
private const val ARGUMENT_KEY = "Item"
private const val COLOR_SCHEME_KEY = "Scheme"
fun newInstance(grade: Grade, colorScheme: String): GradeDetailsDialog {
return GradeDetailsDialog().apply {
fun newInstance(grade: Grade, colorScheme: String) =
GradeDetailsDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, grade)
putString(COLOR_SCHEME_KEY, colorScheme)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -46,12 +47,14 @@ class GradeDetailsDialog : DialogFragment() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogGradeBinding.inflate(inflater).apply { binding = this }.root
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogGradeBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
gradeDialogSubject.text = grade.subject

View File

@ -22,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.databinding.ItemGradeStatisticsBarBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding
import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@ -29,12 +30,16 @@ import javax.inject.Inject
class GradeStatisticsAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var currentDataType = GradeStatisticsItem.DataType.PARTIAL
var items = emptyList<GradeStatisticsItem>()
var theme: String = "vulcan"
var showAllSubjectsOnList: Boolean = false
var onDataTypeChangeListener: () -> Unit = {}
private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five,
@ -62,37 +67,90 @@ class GradeStatisticsAdapter @Inject constructor() :
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
)
override fun getItemCount() = if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1)
override fun getItemCount() =
(if (showAllSubjectsOnList) items.size else (if (items.isEmpty()) 0 else 1)) + 1
override fun getItemViewType(position: Int) = items[position].type.id
override fun getItemViewType(position: Int) =
if (position == 0) {
ViewType.HEADER.id
} else {
when (items[position - 1].type) {
GradeStatisticsItem.DataType.PARTIAL -> ViewType.PARTIAL.id
GradeStatisticsItem.DataType.POINTS -> ViewType.POINTS.id
GradeStatisticsItem.DataType.SEMESTER -> ViewType.SEMESTER.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.PARTIAL.id -> PartialViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false))
ViewType.SEMESTER.id -> SemesterViewHolder(ItemGradeStatisticsPieBinding.inflate(inflater, parent, false))
ViewType.POINTS.id -> PointsViewHolder(ItemGradeStatisticsBarBinding.inflate(inflater, parent, false))
ViewType.PARTIAL.id -> PartialViewHolder(
ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)
)
ViewType.SEMESTER.id -> SemesterViewHolder(
ItemGradeStatisticsPieBinding.inflate(inflater, parent, false)
)
ViewType.POINTS.id -> PointsViewHolder(
ItemGradeStatisticsBarBinding.inflate(inflater, parent, false)
)
ViewType.HEADER.id -> HeaderViewHolder(
ItemGradeStatisticsHeaderBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val index = position - 1
when (holder) {
is PartialViewHolder -> bindPartialChart(holder.binding, items[position].partial!!)
is SemesterViewHolder -> bindSemesterChart(holder.binding, items[position].semester!!)
is PointsViewHolder -> bindBarChart(holder.binding, items[position].points!!)
is PartialViewHolder -> bindPartialChart(holder.binding, items[index].partial!!)
is SemesterViewHolder -> bindSemesterChart(holder.binding, items[index].semester!!)
is PointsViewHolder -> bindBarChart(holder.binding, items[index].points!!)
is HeaderViewHolder -> bindHeader(holder.binding)
}
}
private fun bindPartialChart(binding: ItemGradeStatisticsPieBinding, partials: GradePartialStatistics) {
private fun bindHeader(binding: ItemGradeStatisticsHeaderBinding) {
binding.gradeStatisticsTypeSwitch.check(
when (currentDataType) {
GradeStatisticsItem.DataType.PARTIAL -> R.id.gradeStatisticsTypePartial
GradeStatisticsItem.DataType.SEMESTER -> R.id.gradeStatisticsTypeSemester
GradeStatisticsItem.DataType.POINTS -> R.id.gradeStatisticsTypePoints
}
)
binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
currentDataType = when (checkedId) {
R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL
R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER
R.id.gradeStatisticsTypePoints -> GradeStatisticsItem.DataType.POINTS
else -> GradeStatisticsItem.DataType.PARTIAL
}
onDataTypeChangeListener()
}
}
private fun bindPartialChart(
binding: ItemGradeStatisticsPieBinding,
partials: GradePartialStatistics
) {
bindPieChart(binding, partials.subject, partials.classAverage, partials.classAmounts)
}
private fun bindSemesterChart(binding: ItemGradeStatisticsPieBinding, semester: GradeSemesterStatistics) {
private fun bindSemesterChart(
binding: ItemGradeStatisticsPieBinding,
semester: GradeSemesterStatistics
) {
bindPieChart(binding, semester.subject, semester.average, semester.amounts)
}
private fun bindPieChart(binding: ItemGradeStatisticsPieBinding, subject: String, average: String, amounts: List<Int>) {
private fun bindPieChart(
binding: ItemGradeStatisticsPieBinding,
subject: String,
average: String,
amounts: List<Int>
) {
with(binding.gradeStatisticsPieTitle) {
text = subject
visibility = if (items.size == 1 || !showAllSubjectsOnList) GONE else VISIBLE
@ -114,7 +172,8 @@ class GradeStatisticsAdapter @Inject constructor() :
valueTextSize = 12f
sliceSpace = 1f
valueTextColor = Color.WHITE
val grades = amounts.mapIndexed { grade, amount -> (grade + 1) to amount }.filterNot { it.second == 0 }
val grades = amounts.mapIndexed { grade, amount -> (grade + 1) to amount }
.filterNot { it.second == 0 }
setColors(grades.reversed().map { (grade, _) ->
gradeColors.single { color -> color.first == grade }.second
}.toIntArray(), binding.root.context)
@ -126,7 +185,11 @@ class GradeStatisticsAdapter @Inject constructor() :
data = PieData(dataset).apply {
setValueFormatter(object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt())
return resources.getQuantityString(
R.plurals.grade_number_item,
value.toInt(),
value.toInt()
)
}
})
}
@ -143,11 +206,14 @@ class GradeStatisticsAdapter @Inject constructor() :
val numberOfGradesString = amounts.fold(0) { acc, it -> acc + it }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
val averageString = binding.root.context.getString(R.string.grade_statistics_average, average)
val averageString =
binding.root.context.getString(R.string.grade_statistics_average, average)
minAngleForSlices = 25f
description.isEnabled = false
centerText = numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() }.orEmpty()
centerText =
numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() }
.orEmpty()
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
@ -155,16 +221,21 @@ class GradeStatisticsAdapter @Inject constructor() :
}
}
private fun bindBarChart(binding: ItemGradeStatisticsBarBinding, points: GradePointsStatistics) {
private fun bindBarChart(
binding: ItemGradeStatisticsBarBinding,
points: GradePointsStatistics
) {
with(binding.gradeStatisticsBarTitle) {
text = points.subject
visibility = if (items.size == 1) GONE else VISIBLE
}
val dataset = BarDataSet(listOf(
BarEntry(1f, points.others.toFloat()),
BarEntry(2f, points.student.toFloat())
), binding.root.context.getString(R.string.grade_statistics_legend))
val dataset = BarDataSet(
listOf(
BarEntry(1f, points.others.toFloat()),
BarEntry(2f, points.student.toFloat())
), binding.root.context.getString(R.string.grade_statistics_legend)
)
with(dataset) {
valueTextSize = 12f
@ -189,7 +260,8 @@ class GradeStatisticsAdapter @Inject constructor() :
form = Legend.LegendForm.SQUARE
},
LegendEntry().apply {
label = binding.root.context.getString(R.string.grade_statistics_average_student)
label =
binding.root.context.getString(R.string.grade_statistics_average_student)
formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE
}
@ -226,4 +298,7 @@ class GradeStatisticsAdapter @Inject constructor() :
private class PointsViewHolder(val binding: ItemGradeStatisticsBarBinding) :
RecyclerView.ViewHolder(binding.root)
private class HeaderViewHolder(val binding: ItemGradeStatisticsHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -38,27 +38,28 @@ class GradeStatisticsFragment :
override val isViewEmpty get() = statisticsAdapter.items.isEmpty()
override val currentType
get() = when (binding.gradeStatisticsTypeSwitch.checkedRadioButtonId) {
R.id.gradeStatisticsTypeSemester -> ViewType.SEMESTER
R.id.gradeStatisticsTypePartial -> ViewType.PARTIAL
else -> ViewType.POINTS
}
override val currentType get() = statisticsAdapter.currentDataType
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeStatisticsBinding.bind(view)
messageContainer = binding.gradeStatisticsSwipe
presenter.onAttachView(this, savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? ViewType)
presenter.onAttachView(
this,
savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType
)
}
override fun initView() {
statisticsAdapter.onDataTypeChangeListener = presenter::onTypeChange
with(binding.gradeStatisticsRecycler) {
layoutManager = LinearLayoutManager(requireContext())
adapter = statisticsAdapter
}
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
subjectsAdapter =
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
subjectsAdapter.setDropDownViewResource(R.layout.item_attendance_summary_subject)
with(binding.gradeStatisticsSubjects) {
@ -71,7 +72,9 @@ class GradeStatisticsFragment :
gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)
)
gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -85,11 +88,15 @@ class GradeStatisticsFragment :
}
}
override fun updateData(items: List<GradeStatisticsItem>, theme: String, showAllSubjectsOnStatisticsList: Boolean) {
override fun updateData(
newItems: List<GradeStatisticsItem>,
newTheme: String,
showAllSubjectsOnStatisticsList: Boolean
) {
with(statisticsAdapter) {
this.showAllSubjectsOnList = showAllSubjectsOnStatisticsList
this.theme = theme
this.items = items
showAllSubjectsOnList = showAllSubjectsOnStatisticsList
theme = newTheme
items = newItems
notifyDataSetChanged()
}
}
@ -103,11 +110,7 @@ class GradeStatisticsFragment :
}
override fun resetView() {
binding.gradeStatisticsScroll.scrollTo(0, 0)
}
override fun showContent(show: Boolean) {
binding.gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE
binding.gradeStatisticsRecycler.scrollToPosition(0)
}
override fun showEmpty(show: Boolean) {
@ -154,11 +157,6 @@ class GradeStatisticsFragment :
(parentFragment as? GradeFragment)?.onChildRefresh()
}
override fun onResume() {
super.onResume()
binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, _ -> presenter.onTypeChange() }
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable(SAVED_CHART_TYPE, presenter.currentType)

View File

@ -35,12 +35,12 @@ class GradeStatisticsPresenter @Inject constructor(
private lateinit var lastError: Throwable
var currentType: ViewType = ViewType.PARTIAL
var currentType: GradeStatisticsItem.DataType = GradeStatisticsItem.DataType.PARTIAL
private set
fun onAttachView(view: GradeStatisticsView, type: ViewType?) {
fun onAttachView(view: GradeStatisticsView, type: GradeStatisticsItem.DataType?) {
super.onAttachView(view)
currentType = type ?: ViewType.PARTIAL
currentType = type ?: GradeStatisticsItem.DataType.PARTIAL
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
}
@ -59,11 +59,11 @@ class GradeStatisticsPresenter @Inject constructor(
}
fun onParentViewChangeSemester() {
clearDataInView()
view?.run {
showProgress(true)
enableSwipe(false)
showRefresh(false)
showContent(false)
showErrorView(false)
showEmpty(false)
clearView()
@ -90,8 +90,8 @@ class GradeStatisticsPresenter @Inject constructor(
fun onSubjectSelected(name: String?) {
Timber.i("Select grade stats subject $name")
clearDataInView()
view?.run {
showContent(false)
showProgress(true)
enableSwipe(false)
showEmpty(false)
@ -104,11 +104,11 @@ class GradeStatisticsPresenter @Inject constructor(
}
fun onTypeChange() {
val type = view?.currentType ?: ViewType.POINTS
val type = view?.currentType ?: GradeStatisticsItem.DataType.POINTS
Timber.i("Select grade stats semester: $type")
cancelJobs("load")
clearDataInView()
view?.run {
showContent(false)
showProgress(true)
enableSwipe(false)
showEmpty(false)
@ -143,10 +143,16 @@ class GradeStatisticsPresenter @Inject constructor(
}.launch("subjects")
}
private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) {
private fun loadDataByType(
semesterId: Int,
subjectName: String,
type: GradeStatisticsItem.DataType,
forceRefresh: Boolean = false
) {
Timber.i("Loading grade stats data started")
currentSubjectName = if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName
currentSubjectName =
if (preferencesRepository.showAllSubjectsOnStatisticsList) "Wszystkie" else subjectName
currentType = type
flowWithResourceIn {
@ -156,9 +162,30 @@ class GradeStatisticsPresenter @Inject constructor(
with(gradeStatisticsRepository) {
when (type) {
ViewType.PARTIAL -> getGradesPartialStatistics(student, semester, currentSubjectName, forceRefresh)
ViewType.SEMESTER -> getGradesSemesterStatistics(student, semester, currentSubjectName, forceRefresh)
ViewType.POINTS -> getGradesPointsStatistics(student, semester, currentSubjectName, forceRefresh)
GradeStatisticsItem.DataType.PARTIAL -> {
getGradesPartialStatistics(
student = student,
semester = semester,
subjectName = currentSubjectName,
forceRefresh = forceRefresh
)
}
GradeStatisticsItem.DataType.SEMESTER -> {
getGradesSemesterStatistics(
student = student,
semester = semester,
subjectName = currentSubjectName,
forceRefresh = forceRefresh
)
}
GradeStatisticsItem.DataType.POINTS -> {
getGradesPointsStatistics(
student = student,
semester = semester,
subjectName = currentSubjectName,
forceRefresh = forceRefresh
)
}
}
}
}.onEach {
@ -168,12 +195,15 @@ class GradeStatisticsPresenter @Inject constructor(
if (!isNoContent) {
view?.run {
showEmpty(isNoContent)
showContent(!isNoContent)
showErrorView(false)
enableSwipe(true)
showRefresh(true)
showProgress(false)
updateData(it.data!!, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
updateData(
if (isNoContent) emptyList() else it.data!!,
preferencesRepository.gradeColorTheme,
preferencesRepository.showAllSubjectsOnStatisticsList
)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
}
}
@ -183,9 +213,12 @@ class GradeStatisticsPresenter @Inject constructor(
view?.run {
val isNoContent = checkIsNoContent(it.data!!, type)
showEmpty(isNoContent)
showContent(!isNoContent)
showErrorView(false)
updateData(it.data, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
updateData(
if (isNoContent) emptyList() else it.data,
preferencesRepository.gradeColorTheme,
preferencesRepository.showAllSubjectsOnStatisticsList
)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
}
analytics.logEvent(
@ -209,16 +242,31 @@ class GradeStatisticsPresenter @Inject constructor(
}.launch("load")
}
private fun checkIsNoContent(items: List<GradeStatisticsItem>, type: ViewType): Boolean {
private fun checkIsNoContent(
items: List<GradeStatisticsItem>,
type: GradeStatisticsItem.DataType
): Boolean {
return items.isEmpty() || when (type) {
ViewType.SEMESTER -> items.firstOrNull()?.semester?.amounts.orEmpty().sum() == 0
ViewType.PARTIAL -> items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0
ViewType.POINTS -> items.firstOrNull()?.points?.let { points ->
points.student == .0 && points.others == .0
} ?: false
GradeStatisticsItem.DataType.SEMESTER -> {
items.firstOrNull()?.semester?.amounts.orEmpty().sum() == 0
}
GradeStatisticsItem.DataType.PARTIAL -> {
items.firstOrNull()?.partial?.classAmounts.orEmpty().sum() == 0
}
GradeStatisticsItem.DataType.POINTS -> {
items.firstOrNull()?.points?.let { points -> points.student == .0 && points.others == .0 } ?: false
}
}
}
private fun clearDataInView() {
view?.updateData(
emptyList(),
preferencesRepository.gradeColorTheme,
preferencesRepository.showAllSubjectsOnStatisticsList
)
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {

View File

@ -7,13 +7,17 @@ interface GradeStatisticsView : BaseView {
val isViewEmpty: Boolean
val currentType: ViewType
val currentType: GradeStatisticsItem.DataType
fun initView()
fun updateSubjects(data: ArrayList<String>)
fun updateData(items: List<GradeStatisticsItem>, theme: String, showAllSubjectsOnStatisticsList: Boolean)
fun updateData(
newItems: List<GradeStatisticsItem>,
newTheme: String,
showAllSubjectsOnStatisticsList: Boolean
)
fun showSubjects(show: Boolean)
@ -25,8 +29,6 @@ interface GradeStatisticsView : BaseView {
fun resetView()
fun showContent(show: Boolean)
fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean)

View File

@ -3,5 +3,6 @@ package io.github.wulkanowy.ui.modules.grade.statistics
enum class ViewType(val id: Int) {
SEMESTER(1),
PARTIAL(2),
POINTS(3)
POINTS(3),
HEADER(4)
}

View File

@ -28,12 +28,11 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
private lateinit var homework: Homework
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework): HomeworkDetailsDialog {
return HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
}
fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
}
}
@ -45,19 +44,22 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogHomeworkBinding.inflate(inflater).apply { binding = this }.root
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
@SuppressLint("SetTextI18n")
override fun initView() {
with(binding) {
homeworkDialogRead.text = view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
homeworkDialogRead.text =
view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
homeworkDialogRead.setOnClickListener { presenter.toggleDone(homework) }
homeworkDialogClose.setOnClickListener { dismiss() }
}
@ -87,7 +89,8 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
}
override fun updateMarkAsDoneLabel(isDone: Boolean) {
binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
binding.homeworkDialogRead.text =
view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
}
override fun onDestroyView() {

View File

@ -52,6 +52,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
updateHelper.onResume(this)
}
//https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
updateHelper.onActivityResult(requestCode, resultCode)
@ -65,13 +67,15 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
with(loginAdapter) {
containerId = binding.loginViewpager.id
addFragments(listOf(
LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance()
))
addFragments(
listOf(
LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance()
)
)
}
with(binding.loginViewpager) {
@ -99,14 +103,20 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
override fun notifyInitSymbolFragment(loginData: Triple<String, String, String>) {
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(loginData)
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(
loginData
)
}
override fun notifyInitStudentSelectFragment(studentsWithSemesters: List<StudentWithSemesters>) {
(loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment)?.onParentInitStudentSelectFragment(studentsWithSemesters)
(loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment)
?.onParentInitStudentSelectFragment(studentsWithSemesters)
}
fun onFormFragmentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>, loginData: Triple<String, String, String>) {
fun onFormFragmentAccountLogged(
studentsWithSemesters: List<StudentWithSemesters>,
loginData: Triple<String, String, String>
) {
presenter.onFormViewAccountLogged(studentsWithSemesters, loginData)
}

View File

@ -150,6 +150,13 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
}
override fun setErrorEmailInvalid(domain: String) {
with(binding.loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_invalid_custom_email,domain)
}
}
override fun clearUsernameError() {
binding.loginFormUsernameLayout.error = null
}

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.net.URL
import javax.inject.Inject
class LoginFormPresenter @Inject constructor(
@ -87,7 +88,14 @@ class LoginFormPresenter @Inject constructor(
if (!validateCredentials(email, password, host)) return
flowWithResource { studentRepository.getStudentsScrapper(email, password, host, symbol) }.onEach {
flowWithResource {
studentRepository.getStudentsScrapper(
email,
password,
host,
symbol
)
}.onEach {
when (it.status) {
Status.LOADING -> view?.run {
Timber.i("Login started")
@ -150,11 +158,18 @@ class LoginFormPresenter @Inject constructor(
view?.setErrorLoginRequired()
isCorrect = false
}
if ("@" !in login && "email" in host) {
view?.setErrorEmailRequired()
isCorrect = false
}
if ("@" in login && "||" !in login && "login" !in host && "email" !in host) {
val emailHost = login.substringAfter("@")
val emailDomain = URL(host).host
if (emailHost != emailDomain) {
view?.setErrorEmailInvalid(domain = emailDomain)
isCorrect = false
}
}
}
if (password.isEmpty()) {

View File

@ -39,6 +39,8 @@ interface LoginFormView : BaseView {
fun setErrorPassIncorrect()
fun setErrorEmailInvalid(domain: String)
fun clearUsernameError()
fun clearPassError()

View File

@ -9,6 +9,7 @@ import android.view.View.VISIBLE
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -42,10 +43,12 @@ class LoginRecoverFragment :
private lateinit var hostSymbols: Array<String>
override val recoverHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString())).orEmpty()
get() = hostValues.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString()))
.orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString())).orEmpty()
get() = hostSymbols.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString()))
.orEmpty()
override val recoverNameValue: String
get() = bindingLocal.loginRecoverName.text.toString().trim()
@ -82,7 +85,9 @@ class LoginRecoverFragment :
with(bindingLocal.loginRecoverHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setAdapter(
LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)
)
setOnClickListener { if (bindingLocal.loginRecoverFormContainer.visibility == GONE) dismissDropDown() }
}
}
@ -127,6 +132,7 @@ class LoginRecoverFragment :
override fun showErrorView(show: Boolean) {
bindingLocal.loginRecoverError.visibility = if (show) VISIBLE else GONE
bindingLocal.loginRecoverErrorDetails.isVisible = true
}
override fun setErrorDetails(message: String) {
@ -166,7 +172,7 @@ class LoginRecoverFragment :
with(bindingLocal.loginRecoverWebView) {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
private var recoverWebViewSuccess: Boolean = true
private var recoverWebViewSuccess = true
override fun onPageFinished(view: WebView?, url: String?) {
if (recoverWebViewSuccess) {
@ -175,10 +181,16 @@ class LoginRecoverFragment :
} else {
showProgress(false)
showErrorView(true)
bindingLocal.loginRecoverErrorDetails.isVisible = false
}
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String,
failingUrl: String
) {
recoverWebViewSuccess = false
}
}

View File

@ -9,6 +9,8 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.FragmentLuckyNumberBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.luckynumber.history.LuckyNumberHistoryFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@ -42,6 +44,7 @@ class LuckyNumberFragment :
luckyNumberSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
luckyNumberSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
luckyNumberSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
luckyNumberHistoryButton.setOnClickListener { openLuckyNumberHistory() }
luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() }
luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -79,6 +82,10 @@ class LuckyNumberFragment :
binding.luckyNumberContent.visibility = if (show) VISIBLE else GONE
}
override fun openLuckyNumberHistory() {
(activity as? MainActivity)?.pushView(LuckyNumberHistoryFragment.newInstance())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -24,4 +24,6 @@ interface LuckyNumberView : BaseView {
fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean)
fun openLuckyNumberHistory()
}

View File

@ -0,0 +1,36 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.ItemLuckyNumberHistoryBinding
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import java.util.Locale
import javax.inject.Inject
class LuckyNumberHistoryAdapter @Inject constructor() :
RecyclerView.Adapter<LuckyNumberHistoryAdapter.ItemViewHolder>() {
var items = emptyList<LuckyNumber>()
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemLuckyNumberHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("DefaultLocale")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
luckyNumberHistoryWeekName.text = item.date.weekDayName.capitalize()
luckyNumberHistoryDate.text = item.date.toFormattedString()
luckyNumberHistory.text = item.luckyNumber.toString()
}
}
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -0,0 +1,134 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.recyclerview.widget.LinearLayoutManager
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.FragmentLuckyNumberHistoryBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate
import javax.inject.Inject
@AndroidEntryPoint
class LuckyNumberHistoryFragment :
BaseFragment<FragmentLuckyNumberHistoryBinding>(R.layout.fragment_lucky_number_history), LuckyNumberHistoryView,
MainView.TitledView {
@Inject
lateinit var presenter: LuckyNumberHistoryPresenter
@Inject
lateinit var luckyNumberHistoryAdapter: LuckyNumberHistoryAdapter
companion object {
fun newInstance() = LuckyNumberHistoryFragment()
}
override val titleStringId: Int
get() = R.string.lucky_number_history_title
override val isViewEmpty get() = luckyNumberHistoryAdapter.items.isEmpty()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLuckyNumberHistoryBinding.bind(view)
messageContainer = binding.luckyNumberHistoryRecycler
presenter.onAttachView(this)
}
override fun initView() {
with(binding.luckyNumberHistoryRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = luckyNumberHistoryAdapter
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
luckyNumberHistoryNavDate.setOnClickListener { presenter.onPickDate() }
luckyNumberHistoryErrorRetry.setOnClickListener { presenter.onRetry() }
luckyNumberHistoryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() }
luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f))
}
}
override fun updateData(data: List<LuckyNumber>) {
with(luckyNumberHistoryAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun clearData() {
with(luckyNumberHistoryAdapter) {
items = emptyList()
notifyDataSetChanged()
}
}
override fun showEmpty(show: Boolean) {
binding.luckyNumberHistoryEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
binding.luckyNumberHistoryError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
binding.luckyNumberHistoryErrorMessage.text = message
}
override fun updateNavigationWeek(date: String) {
binding.luckyNumberHistoryNavDate.text = date
}
override fun showProgress(show: Boolean) {
binding.luckyNumberHistoryProgress.visibility = if (show) VISIBLE else GONE
}
override fun showPreButton(show: Boolean) {
binding.luckyNumberHistoryPreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE
}
override fun showNextButton(show: Boolean) {
binding.luckyNumberHistoryNextButton.visibility = if (show) VISIBLE else View.INVISIBLE
}
override fun showDatePickerDialog(currentDate: LocalDate) {
val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
presenter.onDateSet(year, month + 1, dayOfMonth)
}
val datePickerDialog = DatePickerDialog.newInstance(dateSetListener,
currentDate.year, currentDate.monthValue - 1, currentDate.dayOfMonth)
with(datePickerDialog) {
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@LuckyNumberHistoryFragment.parentFragmentManager, null)
}
}
override fun showContent(show: Boolean) {
binding.luckyNumberHistoryRecycler.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,151 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject
class LuckyNumberHistoryPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val luckyNumberRepository: LuckyNumberRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<LuckyNumberHistoryView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
var currentDate: LocalDate = LocalDate.now().previousOrSameSchoolDay
override fun onAttachView(view: LuckyNumberHistoryView) {
super.onAttachView(view)
view.run {
initView()
reloadNavigation()
showContent(false)
}
Timber.i("Lucky number history view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
private fun loadData() {
flowWithResource {
val student = studentRepository.getCurrentStudent()
luckyNumberRepository.getLuckyNumberHistory(student, currentDate.monday, currentDate.sunday)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading lucky number history started")
Status.SUCCESS -> {
if (!it.data?.first().isNullOrEmpty()) {
Timber.i("Loading lucky number result: Success")
view?.apply {
updateData(it.data!!.first())
showContent(true)
showEmpty(false)
showErrorView(false)
showProgress(false)
}
analytics.logEvent(
"load_items",
"type" to "lucky_number_history",
"numbers" to it.data
)
} else {
Timber.i("Loading lucky number history result: No lucky numbers found")
view?.run {
showContent(false)
showEmpty(true)
showErrorView(false)
}
}
}
Status.ERROR -> {
Timber.i("Loading lucky number history result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.run {
showProgress(false)
}
}.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
private fun reloadView(date: LocalDate) {
currentDate = date
Timber.i("Reload lucky number history view with the date ${currentDate.toFormattedString()}")
view?.apply {
showProgress(true)
showContent(false)
showEmpty(false)
showErrorView(false)
clearData()
reloadNavigation()
}
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun reloadNavigation() {
view?.apply {
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.sunday.toFormattedString("dd.MM"))
}
}
fun onDateSet(year: Int, month: Int, day: Int) {
reloadView(LocalDate.of(year, month, day))
loadData()
}
fun onPickDate() {
view?.showDatePickerDialog(currentDate)
}
fun onPreviousWeek() {
reloadView(currentDate.minusDays(7))
loadData()
}
fun onNextWeek() {
reloadView(currentDate.plusDays(7))
loadData()
}
}

View File

@ -0,0 +1,36 @@
package io.github.wulkanowy.ui.modules.luckynumber.history
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.ui.base.BaseView
import java.time.LocalDate
interface LuckyNumberHistoryView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<LuckyNumber>)
fun clearData()
fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun updateNavigationWeek(date: String)
fun showProgress(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)
fun showDatePickerDialog(currentDate: LocalDate)
fun showContent(show: Boolean)
fun onDestroyView()
}

View File

@ -11,7 +11,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.WidgetConfigureAdapter
@ -38,7 +38,9 @@ class LuckyNumberWidgetConfigureActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
setContentView(ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root)
setContentView(
ActivityWidgetConfigureBinding.inflate(layoutInflater).apply { binding = this }.root
)
intent.extras.let {
presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID))
@ -70,8 +72,9 @@ class LuckyNumberWidgetConfigureActivity :
.show()
}
override fun updateData(data: List<Pair<Student, Boolean>>) {
override fun updateData(data: List<StudentWithSemesters>, selectedStudentId: Long) {
with(configureAdapter) {
selectedId = selectedStudentId
items = data
notifyDataSetChanged()
}

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