forked from github/wulkanowy-mirror
Compare commits
209 Commits
Author | SHA1 | Date | |
---|---|---|---|
4316d4ba10 | |||
5e78b01415 | |||
![]() |
528a947ffd | ||
![]() |
ca9caeca66 | ||
![]() |
e971eb7821 | ||
7266d1fcd3 | |||
e048fe4c35 | |||
49541e9a7d | |||
72c93594f6 | |||
b249eab1eb | |||
![]() |
c7afa262d9 | ||
![]() |
b622c09e56 | ||
![]() |
87fb1916d8 | ||
![]() |
5ca9ac3978 | ||
![]() |
c76ace40eb | ||
![]() |
4a585fc56e | ||
![]() |
f3afe7fdb7 | ||
![]() |
859c6ef154 | ||
![]() |
24abe47332 | ||
![]() |
52c1878f6b | ||
![]() |
4060240368 | ||
![]() |
f69816fbac | ||
![]() |
1a41e9e3ee | ||
![]() |
1c0df6c145 | ||
![]() |
2b61e883c5 | ||
d29d95bfae | |||
65a0457675 | |||
b25c61a485 | |||
e33350e153 | |||
8109459ee2 | |||
![]() |
31a7ae6d15 | ||
![]() |
4b3b4a21fa | ||
![]() |
9a6b17c9d9 | ||
![]() |
729e0f547b | ||
![]() |
faa8d34e79 | ||
e553d9cdcf | |||
![]() |
b6f5ac91ad | ||
![]() |
d1d0caa1e3 | ||
eed3a93196 | |||
![]() |
c40779f48f | ||
![]() |
594d2dbec5 | ||
f6b8c33121 | |||
7b728d910e | |||
7302bb3336 | |||
5886ed2bce | |||
3fdd92ea35 | |||
708e25c622 | |||
36d9e02de7 | |||
ebe6bf1cc0 | |||
4bd95237c6 | |||
cb553b1a33 | |||
f5a95421a1 | |||
f2857bdece | |||
3c1497394c | |||
![]() |
26596e8254 | ||
![]() |
6e472d6c5c | ||
9f6ae0e6b5 | |||
![]() |
6f4826249c | ||
01fa007c89 | |||
3f7db44ea7 | |||
c426701b27 | |||
2d8db2b071 | |||
01f6f402b4 | |||
e058134740 | |||
0aefab9463 | |||
748e9f62f3 | |||
c40e371843 | |||
484996120e | |||
2de3362e0b | |||
470827fc42 | |||
6a524deca0 | |||
f9fcfe6d1a | |||
02f8b45ad5 | |||
6ccb2e9b5c | |||
cbf405fb16 | |||
![]() |
2f3e1b6aae | ||
![]() |
567d868f76 | ||
![]() |
12030efee2 | ||
![]() |
04c382643d | ||
58f7c84cd5 | |||
a2e641523a | |||
3b2eed487a | |||
bc999f1b9c | |||
![]() |
fc140ad9c1 | ||
![]() |
78a2cc89e9 | ||
![]() |
dbfe5c8918 | ||
![]() |
c697ca7ad1 | ||
![]() |
1b00f4e518 | ||
![]() |
fb77bf882f | ||
![]() |
466ebbef3a | ||
![]() |
458a4c8164 | ||
![]() |
0363c0854f | ||
![]() |
233ddc955b | ||
![]() |
065c711f91 | ||
![]() |
49655c11c9 | ||
![]() |
38fd4eda22 | ||
![]() |
b71630246a | ||
![]() |
fd2eac1f08 | ||
![]() |
1545ff65d3 | ||
![]() |
ff32c82851 | ||
![]() |
d531a94594 | ||
![]() |
71ab9586ac | ||
![]() |
e1a19be06c | ||
![]() |
6f2168d641 | ||
![]() |
cde2121b60 | ||
d7fec4806e | |||
4fba76d327 | |||
![]() |
a0bc37e826 | ||
![]() |
ad5381ce34 | ||
![]() |
dbc7587741 | ||
![]() |
bc3aa7b8dc | ||
![]() |
6bf6a9da11 | ||
![]() |
ab175bdd9a | ||
![]() |
2816d7217a | ||
![]() |
2fa868173b | ||
![]() |
622c75bb42 | ||
![]() |
2121125283 | ||
![]() |
c72a117e34 | ||
![]() |
860095e862 | ||
![]() |
ff9be43291 | ||
![]() |
a487378daf | ||
![]() |
895f5cbb76 | ||
![]() |
8b9b1460ab | ||
![]() |
7edd3df074 | ||
![]() |
16c51f7b07 | ||
![]() |
7a3a97447f | ||
![]() |
b500d8e204 | ||
![]() |
c34a369286 | ||
![]() |
b9f3ab2e56 | ||
![]() |
6b59973624 | ||
![]() |
d18485293d | ||
![]() |
596e8df4fc | ||
![]() |
f13ce6e2b4 | ||
![]() |
8c10606b61 | ||
![]() |
7fda4276d6 | ||
![]() |
7993366bfc | ||
ecae5d96c4 | |||
![]() |
ddd49575d3 | ||
![]() |
5b0748a254 | ||
![]() |
47ff549d4b | ||
![]() |
a1203b360e | ||
![]() |
c80c041dd0 | ||
667b1b14d2 | |||
3f2a02f242 | |||
![]() |
29196c62fa | ||
e5412086d5 | |||
![]() |
ce114e1824 | ||
![]() |
b0d221473c | ||
![]() |
ee2803b0d3 | ||
![]() |
47817c47f8 | ||
![]() |
92bad9d112 | ||
![]() |
d17614fa64 | ||
![]() |
d068371fb1 | ||
![]() |
af71023bfb | ||
![]() |
b5adc27efa | ||
![]() |
de0b9cf221 | ||
![]() |
425d28d563 | ||
![]() |
31744e7e77 | ||
![]() |
925ad9616d | ||
![]() |
878e7b4247 | ||
![]() |
e347045ac8 | ||
![]() |
8eb65196cb | ||
![]() |
f99df4ad46 | ||
![]() |
27eb0588d7 | ||
![]() |
34d34a050a | ||
![]() |
961bc24f27 | ||
![]() |
8a90b61b97 | ||
![]() |
6a8f6f9496 | ||
![]() |
afb5ae741c | ||
![]() |
95e41b5570 | ||
![]() |
eb6fdd900e | ||
![]() |
88def5eff8 | ||
![]() |
0e99c81eb8 | ||
![]() |
38c00ddab5 | ||
![]() |
c72cc39920 | ||
![]() |
4ef9fb1f28 | ||
![]() |
5dd5697f65 | ||
![]() |
a7c2009e49 | ||
![]() |
a71ef4a4b2 | ||
![]() |
30413086fc | ||
![]() |
98ddf97855 | ||
![]() |
8f5a210ec7 | ||
![]() |
ce09b07cfd | ||
![]() |
0264682d0d | ||
![]() |
c395abb7e4 | ||
![]() |
dfd5e10fbf | ||
![]() |
130f6164c4 | ||
![]() |
b2191ab21b | ||
![]() |
eb08a2cddb | ||
![]() |
eb63e5eb04 | ||
![]() |
7a8ab6fb52 | ||
![]() |
0eadf36222 | ||
![]() |
85e3bb4d17 | ||
![]() |
84969e6f9b | ||
![]() |
d39998000b | ||
![]() |
b6cbd0523b | ||
![]() |
4d74c252c8 | ||
![]() |
c1687f5856 | ||
![]() |
f7b25139c0 | ||
![]() |
6e751c83f6 | ||
![]() |
43a01e4e04 | ||
![]() |
ee906d02ae | ||
![]() |
83cf39a28c | ||
![]() |
3c5c50f84d | ||
![]() |
d0d3b11662 | ||
![]() |
d3ff6682ca | ||
![]() |
a571dabb71 | ||
![]() |
5c27edbe3c | ||
![]() |
598edaadb7 |
@ -162,7 +162,7 @@ jobs:
|
||||
openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks
|
||||
- run:
|
||||
name: Publish release
|
||||
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex
|
||||
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PdisablePreDex
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
@ -1,7 +1,7 @@
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
insert_final_newline=true
|
||||
insert_final_newline=Advanced
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
|
18
.gitea/release.yml
Normal file
18
.gitea/release.yml
Normal file
@ -0,0 +1,18 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- "release ignore"
|
||||
categories:
|
||||
- title: breaking changes
|
||||
labels:
|
||||
- major
|
||||
- title: new features
|
||||
labels:
|
||||
- minor
|
||||
- fr:approved
|
||||
- title: translation updates
|
||||
labels:
|
||||
- translation
|
||||
- title: features
|
||||
labels:
|
||||
- "*"
|
84
.gitea/workflows/build_android.yml
Normal file
84
.gitea/workflows/build_android.yml
Normal file
@ -0,0 +1,84 @@
|
||||
name: Generate APK
|
||||
|
||||
env:
|
||||
main_project_module: app
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set current date as env variable
|
||||
run: echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set repository name as env variable
|
||||
run: echo "repository_name=$(echo '${{ gitea.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Set up Go environment
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Get hash of Gradle files
|
||||
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: get-hash
|
||||
with:
|
||||
patterns: |-
|
||||
**/*.gradle*
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ steps.get-hash.outputs.hash }}
|
||||
|
||||
- name: Get app version
|
||||
id: get_version
|
||||
run: echo "VERSION_NAME=$(grep -m1 "versionName" app/build.gradle | awk '{print $2}' | tr -d \'\'\"\')" >> $GITHUB_ENV
|
||||
|
||||
- name: Change wrapper permissions
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Build debug APK
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3 # not v4 because of GHES
|
||||
with:
|
||||
name: wulkanowy_mod_debug_builds
|
||||
path: |
|
||||
app/build/outputs/**/*-debug.apk
|
||||
|
||||
- name: Create release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch'
|
||||
with:
|
||||
files: |
|
||||
app/build/outputs/**/*-debug.apk
|
||||
|
||||
name: Release ${{ env.VERSION_NAME }} (${{ env.date_today }})
|
||||
tag_name: v${{ env.VERSION_NAME }}
|
79
.github/workflows/deploy-store.yml
vendored
79
.github/workflows/deploy-store.yml
vendored
@ -1,79 +0,0 @@
|
||||
name: Deploy release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
|
||||
deploy-google-play:
|
||||
name: Google Play
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: google-play
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Decrypt keys
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
|
||||
run: |
|
||||
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg
|
||||
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
|
||||
- name: Upload apk to google play
|
||||
env:
|
||||
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
|
||||
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
|
||||
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
|
||||
ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}
|
||||
ADMOB_PROJECT_ID: ${{ secrets.ADMOB_PROJECT_ID }}
|
||||
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }}
|
||||
DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }}
|
||||
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
|
||||
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
|
||||
|
||||
deploy-app-gallery:
|
||||
name: AppGallery
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: app-gallery
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Decrypt keys
|
||||
env:
|
||||
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
|
||||
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
|
||||
run: |
|
||||
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg
|
||||
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
|
||||
- name: Prepare credentials
|
||||
env:
|
||||
AGC_CREDENTIALS: ${{ secrets.AGC_CREDENTIALS }}
|
||||
run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json
|
||||
- name: Build and publish HMS version
|
||||
env:
|
||||
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
|
||||
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
|
||||
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
|
||||
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
|
||||
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace
|
146
.github/workflows/deploy-test.yml
vendored
146
.github/workflows/deploy-test.yml
vendored
@ -1,146 +0,0 @@
|
||||
name: Deploy DEV
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches: [ develop ]
|
||||
branches: [ '!*' ]
|
||||
pull_request_target:
|
||||
# branches: [ develop ]
|
||||
branches: [ '!*' ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
deploy-appcenter:
|
||||
name: App Center
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: app-center
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Set run number with offset
|
||||
env:
|
||||
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
|
||||
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
|
||||
- name: Prepare build configuration
|
||||
run: |
|
||||
sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle
|
||||
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json
|
||||
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json
|
||||
sed -i -e '/versionNameSuffix/d' app/build.gradle
|
||||
- name: Add signing config
|
||||
run: |
|
||||
cat >> app/build.gradle <<EOF
|
||||
android.signingConfigs.debug {
|
||||
storeFile file("bitrise.jks")
|
||||
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
|
||||
keyAlias System.getenv("BITRISE_KEY_ALIAS")
|
||||
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
|
||||
}
|
||||
EOF
|
||||
- name: Decrypt keys
|
||||
env:
|
||||
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
|
||||
run: |
|
||||
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
|
||||
- name: Bump version
|
||||
uses: chkfung/android-version-actions@v1.1
|
||||
with:
|
||||
gradlePath: app/build.gradle
|
||||
versionCode: ${{ env.RUN_NUMBER }}
|
||||
versionName: ${{ env.RUN_NUMBER }}-${{ github.head_ref }}
|
||||
- name: Build apk
|
||||
env:
|
||||
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
|
||||
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
|
||||
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
|
||||
run: ./gradlew assembleFdroidDebug --stacktrace
|
||||
- name: Upload apk to github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk
|
||||
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
|
||||
- name: Deploy to app center
|
||||
uses: wzieba/AppCenter-Github-Action@v1
|
||||
with:
|
||||
appName: wulkanowy/wulkanowy
|
||||
token: ${{ secrets.APP_CENTER_TOKEN }}
|
||||
group: Testers
|
||||
file: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
|
||||
notifyTesters: true
|
||||
debug: true
|
||||
|
||||
deploy-app-distribution:
|
||||
name: App Distribution
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: app-distribution
|
||||
if: github.event_name != 'pull_request_target'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Set run number with offset
|
||||
env:
|
||||
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
|
||||
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
|
||||
- name: Add signing config
|
||||
run: |
|
||||
cat >> app/build.gradle <<EOF
|
||||
android.signingConfigs.debug {
|
||||
storeFile file("bitrise.jks")
|
||||
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
|
||||
keyAlias System.getenv("BITRISE_KEY_ALIAS")
|
||||
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
|
||||
}
|
||||
EOF
|
||||
- name: Decrypt keys
|
||||
env:
|
||||
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
|
||||
BITRISE_SERVICES_ENCRYPT_KEY: ${{ secrets.BITRISE_SERVICES_ENCRYPT_KEY }}
|
||||
run: |
|
||||
gpg --yes --batch --passphrase=$BITRISE_SERVICES_ENCRYPT_KEY ./app/src/debug/google-services.json.gpg
|
||||
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
|
||||
- name: Bump version
|
||||
uses: chkfung/android-version-actions@v1.1
|
||||
with:
|
||||
gradlePath: app/build.gradle
|
||||
versionCode: ${{ env.RUN_NUMBER }}
|
||||
versionName: ${{ env.RUN_NUMBER }}
|
||||
- name: Build apk
|
||||
env:
|
||||
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
|
||||
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
|
||||
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
|
||||
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
|
||||
- name: Upload apk to github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk
|
||||
path: app/build/outputs/apk/play/debug/app-play-debug.apk
|
||||
- name: Deploy to app distribution
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
token: ${{ secrets.FIREBASE_TOKEN }}
|
||||
groups: discord
|
||||
file: app/build/outputs/apk/play/debug/app-play-debug.apk
|
90
.github/workflows/test.yml
vendored
90
.github/workflows/test.yml
vendored
@ -1,90 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'hotfix/**'
|
||||
tags: [ '*' ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
tests-fdroid:
|
||||
name: F-Droid
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: fkirc/skip-duplicate-actions@master
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Unit tests
|
||||
run: |
|
||||
./gradlew testFdroidDebugUnitTest --stacktrace
|
||||
./gradlew jacocoTestReport --stacktrace
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: unit
|
||||
|
||||
tests-play:
|
||||
name: Play
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: fkirc/skip-duplicate-actions@master
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Unit tests
|
||||
run: |
|
||||
./gradlew testPlayDebugUnitTest --stacktrace
|
||||
./gradlew jacocoTestReport --stacktrace
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: unit
|
||||
|
||||
tests-hms:
|
||||
name: HMS
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: fkirc/skip-duplicate-actions@master
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
|
||||
- name: Unit tests
|
||||
run: |
|
||||
./gradlew testHmsDebugUnitTest --stacktrace
|
||||
./gradlew jacocoTestReport --stacktrace
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: unit
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -117,12 +117,14 @@ Thumbs.db
|
||||
*.ear
|
||||
|
||||
### AndroidStudio Patch ###
|
||||
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
.idea/jarRepositories.xml
|
||||
|
||||
### Services config files
|
||||
agconnect-services.json
|
||||
agconnect-credentials.json
|
||||
google-services.json
|
||||
!app/google-services.json
|
||||
|
||||
app/src/release/agconnect-services.json
|
||||
app/src/release/agconnect-credentials.json
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/kotlinc.xml
|
||||
|
||||
.idea/appInsightsSettings.xml
|
||||
|
@ -61,7 +61,7 @@ script:
|
||||
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;
|
||||
./gradlew publishPlayRelease -PenableFirebase --stacktrace;
|
||||
./gradlew publishPlayRelease --stacktrace;
|
||||
fi
|
||||
|
||||
after_success:
|
||||
|
90
README.cs.md
90
README.cs.md
@ -1,73 +1,33 @@
|
||||
Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
|
||||
|
||||
# Wulkanowy
|
||||
# Wulkanowy MOD
|
||||
|
||||
[](https://github.com/wulkanowy/wulkanowy/actions)
|
||||
[](https://codecov.io/gh/wulkanowy/wulkanowy)
|
||||
[](https://discord.gg/vccAQBr)
|
||||
[](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[](https://github.com/wulkanowy/wulkanowy/releases)
|
||||
[](https://translate.wulkanowy.net.pl)
|
||||
## Funkce:
|
||||
* skrýt známky
|
||||
* Skrýt jednotlivé záznamy o docházce.
|
||||
* Skrýt komentáře.
|
||||
* falešná docházka %
|
||||
|
||||
Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče
|
||||
Chcete-li se dostat na skrytý panel:
|
||||
1. Přejděte na kartu „Další“.
|
||||
2. Přejděte na panel „Nastavení“.
|
||||
3. Přejděte na panel „O aplikaci“.
|
||||
4. Klikněte 5x na logo aplikace
|
||||
5. Přejděte na domovskou obrazovku
|
||||
6. Přejděte do nastavení
|
||||
7. Zadejte „tajná nastavení“
|
||||
|
||||
# Instalace
|
||||
|
||||
## Funkce
|
||||
| Název souboru | Přizpůsobeno |
|
||||
| ---------------- | ----------------- |
|
||||
| `*-fdroid-*.apk` | F-Droid |
|
||||
| `*-hms-*.apk` | Huawei AppGallery |
|
||||
| `*-play-*.apk` | Play Store |
|
||||
|
||||
* přihlášení pomocí emailu a hesla
|
||||
* funkce z webové stránky deníku:
|
||||
* známky
|
||||
* statistiky známek
|
||||
* frekvence
|
||||
* procento frekvence
|
||||
* zkoušky
|
||||
* plán lekce
|
||||
* dokončené lekce
|
||||
* zprávy
|
||||
* domácí úkoly
|
||||
* poznámky
|
||||
* šťastné číslo
|
||||
* další lekce
|
||||
* školní setkání
|
||||
* informace o žáku a škole
|
||||
* výpočet průměru nezávisle na preferencích školy
|
||||
* upozornění, např. o nových známkách
|
||||
* podpora více účtů s možností přejmenování žáků
|
||||
* tmavý a černý (AMOLED) motiv
|
||||
* offline režim
|
||||
* volitelné reklamy na podporu projektu
|
||||
Stáhněte si vybranou verzi z [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
|
||||
Doporučujeme stáhnout nejnovější dostupnou verzi.
|
||||
|
||||
## Stáhnout
|
||||
# O projektu Wulkanowy
|
||||
|
||||
Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGallery
|
||||
|
||||
[<img src="https://play.google.com/intl/cs-CZ/badges/images/generic/cs_badge_web_generic.png"
|
||||
alt="Nyní na Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Stáhnout s F-Droid"
|
||||
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[<img src="https://i.imgur.com/baTGiDP.png"
|
||||
alt="Objevuj v AppGallery"
|
||||
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
|
||||
|
||||
Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání
|
||||
|
||||
## Postaveno s pomocí
|
||||
|
||||
|
||||
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
|
||||
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
|
||||
* [Hilt](https://dagger.dev/hilt/)
|
||||
* [Room](https://developer.android.com/topic/libraries/architecture/room)
|
||||
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
|
||||
|
||||
## Spolupráce
|
||||
|
||||
Přispějte do projektu vytvořením PR nebo odesláním issue na GitHub.
|
||||
|
||||
Pro zájemce o překlad aplikace do různých jazyků poskytujeme Crowdin:
|
||||
https://crowdin.com/project/wulkanowy2
|
||||
|
||||
## Licence
|
||||
|
||||
Tento projekt je licencován pod licencí Apache License 2.0 - podrobnosti v souboru [LICENSE](LICENSE)
|
||||
Chcete si přečíst více o projektu Wulkanowy? [Klikněte sem](https://github.com/wulkanowy/wulkanowy)
|
90
README.de.md
90
README.de.md
@ -1,73 +1,33 @@
|
||||
[Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
|
||||
|
||||
# Wulkanowy
|
||||
# Wulkanowy MOD
|
||||
|
||||
[](https://github.com/wulkanowy/wulkanowy/actions)
|
||||
[](https://codecov.io/gh/wulkanowy/wulkanowy)
|
||||
[](https://discord.gg/vccAQBr)
|
||||
[](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[](https://github.com/wulkanowy/wulkanowy/releases)
|
||||
[](https://translate.wulkanowy.net.pl)
|
||||
## Funktionen:
|
||||
* Noten ausblenden
|
||||
* Individuelle Anwesenheitslisten ausblenden.
|
||||
* Kommentare ausblenden.
|
||||
* Anwesenheit fälschen %
|
||||
|
||||
Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern
|
||||
So gelangen Sie zum ausgeblendeten Bereich:
|
||||
1. Gehen Sie zur Registerkarte „Mehr“.
|
||||
2. Gehen Sie zum Bereich „Einstellungen“.
|
||||
3. Gehen Sie zum Bereich „Über die Anwendung“.
|
||||
4. Klicken Sie fünfmal auf das Anwendungslogo
|
||||
5. Gehen Sie zum Startbildschirm
|
||||
6. Gehen Sie zu den Einstellungen
|
||||
7. Geben Sie „Geheime Einstellungen“ ein
|
||||
|
||||
# Installation
|
||||
|
||||
## Merkmale
|
||||
| Dateiname | Angepasst an |
|
||||
| ---------------- | ----------------- |
|
||||
| `*-fdroid-*.apk` | F-Droid |
|
||||
| `*-hms-*.apk` | Huawei AppGallery |
|
||||
| `*-play-*.apk` | Play Store |
|
||||
|
||||
* Einloggen mit E-Mail und Passwort
|
||||
* Funktionen von der Registerwebsite:
|
||||
* Noten
|
||||
* Notenstatistik
|
||||
* Anwesenheit
|
||||
* Prozentsatz der Anwesenheit
|
||||
* Prüfungen
|
||||
* Stundenplan
|
||||
* abgeschlossene Unterrichtsstunden
|
||||
* Nachrichten
|
||||
* Hausaufgaben
|
||||
* Anmerkungen
|
||||
* Glückszahl
|
||||
* Zusätzliche Lektionen
|
||||
* Schulkonferenzen
|
||||
* Schüler- und Schulinformationen
|
||||
* Berechnung des Durchschnitts unabhängig von den Präferenzen der Schule
|
||||
* Benachrichtigungen, z. B. über eine neue Note
|
||||
* Unterstützung für mehrere Konten mit der Möglichkeit, den Namen des Schülers zu ändern
|
||||
* dunkles und schwarzes (AMOLED) Thema
|
||||
* Offline-Modus
|
||||
* optionale Werbungen, die es uns ermöglichen das Projekt zu unterstützen
|
||||
Laden Sie die ausgewählte Version von [hier](https://git.sador.me/sadorowo/wulkanowy-mod/releases) herunter.
|
||||
Wir empfehlen, die neueste verfügbare Version herunterzuladen.
|
||||
|
||||
## Herunterladen
|
||||
# Über das Wulkanowy-Projekt
|
||||
|
||||
Die aktuelle Version können Sie von der Google Play, F-Droid oder Huawei AppGallery store herunterladen
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[<img src="appgallery_badge.png"
|
||||
alt="Explore it on AppGallery"
|
||||
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
|
||||
|
||||
Sie können auch eine [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) die beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden
|
||||
|
||||
## Gebaut mit
|
||||
|
||||
|
||||
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
|
||||
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
|
||||
* [Hilt](https://dagger.dev/hilt/)
|
||||
* [Room](https://developer.android.com/topic/libraries/architecture/room)
|
||||
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
|
||||
|
||||
## Beitragen
|
||||
|
||||
Bitte tragen Sie zum Projekt bei, indem Sie entweder eine PR erstellen oder ein Issue auf GitHub einreichen.
|
||||
|
||||
Für Personen, die daran interessiert sind, die Anwendung in verschiedene Sprachen zu übersetzen, bieten wir Crowdin
|
||||
https://crowdin.com/project/wulkanowy2
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt ist unter der Apache License 2.0 lizenziert - siehe die [LIZENZ](LICENSE) Datei für Details
|
||||
Möchten Sie mehr über das Wulkanowy-Projekt lesen? [Hier klicken](https://github.com/wulkanowy/wulkanowy)
|
90
README.en.md
90
README.en.md
@ -1,73 +1,33 @@
|
||||
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
|
||||
|
||||
# Wulkanowy
|
||||
# Wulkanowy MOD
|
||||
|
||||
[](https://github.com/wulkanowy/wulkanowy/actions)
|
||||
[](https://codecov.io/gh/wulkanowy/wulkanowy)
|
||||
[](https://discord.gg/vccAQBr)
|
||||
[](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[](https://github.com/wulkanowy/wulkanowy/releases)
|
||||
[](https://translate.wulkanowy.net.pl)
|
||||
## Functions:
|
||||
* hide grades
|
||||
* hide individual attendance entries
|
||||
* hide comments
|
||||
* fake attendance %.
|
||||
|
||||
Unofficial android VULCAN UONET+ register client for both students and their parents
|
||||
To get to the hidden panel:
|
||||
1. Go to the "More" tab
|
||||
2. Go to the "Settings" panel
|
||||
3. Go to the "About application" panel
|
||||
4. Click on the application logo 5 times
|
||||
5. Go to the home screen
|
||||
6. Go to settings
|
||||
7. Enter "secret settings"
|
||||
|
||||
# Installation
|
||||
|
||||
## Features
|
||||
| File name | Adapted to |
|
||||
| ---------------- | ----------------- |
|
||||
| `*-fdroid-*.apk` | F-Droid |
|
||||
| `*-hms-*.apk` | Huawei AppGallery |
|
||||
| `*-play-*.apk` | Play Store |
|
||||
|
||||
* logging in using the email and password
|
||||
* functions from the register website:
|
||||
* grades
|
||||
* grade statistics
|
||||
* attendance
|
||||
* percentage of attendance
|
||||
* exams
|
||||
* timetable
|
||||
* completed lessons
|
||||
* messages
|
||||
* homework
|
||||
* notes
|
||||
* lucky number
|
||||
* additional lessons
|
||||
* school conferences
|
||||
* student and school information
|
||||
* calculation of the average independently of school's preferences
|
||||
* notifications, e.g. about a new grade
|
||||
* support for multiple accounts with the ability to rename students
|
||||
* dark and black (AMOLED) theme
|
||||
* offline mode
|
||||
* optional ads which allow to support the project
|
||||
Download application from [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
|
||||
We recommend downloading the latest available version.
|
||||
|
||||
## Download
|
||||
# About the Wulkanowy project
|
||||
|
||||
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"
|
||||
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[<img src="appgallery_badge.png"
|
||||
alt="Explore it on AppGallery"
|
||||
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
|
||||
|
||||
You can also download a [development version](https://wulkanowy.github.io/#download) that includes new features being prepared for the next release
|
||||
|
||||
## Built With
|
||||
|
||||
|
||||
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
|
||||
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
|
||||
* [Hilt](https://dagger.dev/hilt/)
|
||||
* [Room](https://developer.android.com/topic/libraries/architecture/room)
|
||||
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please contribute to the project either by creating a PR or submitting an issue on GitHub.
|
||||
|
||||
For people interested in translating the application into different languages, we provide Crowdin
|
||||
https://crowdin.com/project/wulkanowy2
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details
|
||||
Want to read more about the Wulkanowy project? [Click here](https://github.com/wulkanowy/wulkanowy)
|
91
README.md
91
README.md
@ -1,74 +1,33 @@
|
||||
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md)
|
||||
|
||||
# Wulkanowy
|
||||
# Wulkanowy MOD
|
||||
|
||||
[](https://github.com/wulkanowy/wulkanowy/actions)
|
||||
[](https://codecov.io/gh/wulkanowy/wulkanowy)
|
||||
[](https://discord.gg/vccAQBr)
|
||||
[](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[](https://github.com/wulkanowy/wulkanowy/releases)
|
||||
[](https://translate.wulkanowy.net.pl)
|
||||
## Funkcje:
|
||||
* ukryj oceny
|
||||
* ukryj poszczególne wpisy frekwencji
|
||||
* ukryj uwagi
|
||||
* sfałszuj % frekwencji
|
||||
|
||||
Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
|
||||
Aby dostać się do ukrytego panelu:
|
||||
1. Przejdź do karty "Więcej"
|
||||
2. Przejdź do panelu "Ustawienia"
|
||||
3. Przejdź do panelu "O aplikacji"
|
||||
4. Kliknij 5 razy w logo aplikacji
|
||||
5. Przejdź na ekran główny
|
||||
6. Wejdź w ustawienia
|
||||
7. Wejdź w "sekretne ustawienia"
|
||||
|
||||
# Instalacja
|
||||
|
||||
## Funkcje
|
||||
| Nazwa pliku | Przystosowana do |
|
||||
| ---------------- | ----------------- |
|
||||
| `*-fdroid-*.apk` | F-Droid |
|
||||
| `*-hms-*.apk` | Huawei AppGallery |
|
||||
| `*-play-*.apk` | Sklep Play |
|
||||
|
||||
* logowanie za pomocą e-maila i hasła
|
||||
* funkcje ze strony internetowej dziennika:
|
||||
* oceny
|
||||
* statystyki ocen
|
||||
* frekwencja
|
||||
* procent frekwencji
|
||||
* sprawdziany
|
||||
* plan lekcji
|
||||
* lekcje zrealizowane
|
||||
* wiadomości
|
||||
* zadania domowe
|
||||
* uwagi
|
||||
* szczęśliwy numerek
|
||||
* dodatkowe lekcje
|
||||
* zebrania w szkole
|
||||
* informacje o uczniu i szkole
|
||||
* obliczanie średniej niezależnie od preferencji szkoły
|
||||
* powiadomienia np. o nowej ocenie
|
||||
* obsługa wielu kont wraz z możliwością zmiany nazwy ucznia
|
||||
* ciemny i czarny (AMOLED) motyw
|
||||
* tryb offline
|
||||
* opcjonalne reklamy umożliwiające wsparcie projektu
|
||||
Pobierz wybraną wersję z [wydań](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
|
||||
Zalecamy pobranie najnowszej dostępnej wersji.
|
||||
|
||||
## Pobierz
|
||||
# O projekcie Wulkanowy
|
||||
|
||||
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"
|
||||
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Pobierz z F-Droid"
|
||||
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[<img src="appgallery_badge.png"
|
||||
alt="Odkrywaj w AppGallery"
|
||||
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
|
||||
|
||||
|
||||
Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#download), która zawiera nowe funkcje przygotowywane do następnego wydania
|
||||
|
||||
|
||||
## Zbudowana za pomocą
|
||||
|
||||
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
|
||||
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
|
||||
* [Hilt](https://dagger.dev/hilt/)
|
||||
* [Room](https://developer.android.com/topic/libraries/architecture/room)
|
||||
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
|
||||
|
||||
## Współpraca
|
||||
|
||||
Wnieś swój wkład w projekt, tworząc PR lub wysyłając issue na GitHub.
|
||||
|
||||
Dla osób zainteresowanych tłumaczeniem aplikacji na różne języki udostępniamy Crowdina
|
||||
https://crowdin.com/project/wulkanowy2
|
||||
|
||||
## Licencja
|
||||
|
||||
Ten projekt udostępniany jest na licencji Apache License 2.0 - szczegóły w pliku [LICENSE](LICENSE)
|
||||
Chcesz poczytać więcej o projekcie Wulkanowy? [Kliknij tutaj](https://github.com/wulkanowy/wulkanowy)
|
90
README.sk.md
90
README.sk.md
@ -1,73 +1,33 @@
|
||||
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia
|
||||
|
||||
# Wulkanowy
|
||||
# Wulkanowy MOD
|
||||
|
||||
[](https://github.com/wulkanowy/wulkanowy/actions)
|
||||
[](https://codecov.io/gh/wulkanowy/wulkanowy)
|
||||
[](https://discord.gg/vccAQBr)
|
||||
[](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[](https://github.com/wulkanowy/wulkanowy/releases)
|
||||
[](https://translate.wulkanowy.net.pl)
|
||||
## Funkcie:
|
||||
* skryť známky
|
||||
* Skryť individuálne záznamy o dochádzke.
|
||||
* Skryť komentáre.
|
||||
* falošná dochádzka %
|
||||
|
||||
Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov
|
||||
Ak chcete prejsť na skrytý panel:
|
||||
1. Prejdite na kartu „Viac“.
|
||||
2. Prejdite na panel „Nastavenia“.
|
||||
3. Prejdite na panel „O aplikácii“.
|
||||
4. Kliknite 5-krát na logo aplikácie
|
||||
5. Prejdite na domovskú obrazovku
|
||||
6. Prejdite do nastavení
|
||||
7. Zadajte „tajné nastavenia“
|
||||
|
||||
# Inštalácia
|
||||
|
||||
## Funkcie
|
||||
| Názov súboru | Prispôsobené |
|
||||
| ---------------- | ----------------- |
|
||||
| `*-fdroid-*.apk` | F-Droid |
|
||||
| `*-hms-*.apk` | Huawei AppGallery |
|
||||
| `*-play-*.apk` | Play Store |
|
||||
|
||||
* prihlásenie pomocou emailu a hesla
|
||||
* funkcie z webovej stránky denníka:
|
||||
* známky
|
||||
* štatistiky známok
|
||||
* frekvencia
|
||||
* percento frekvencie
|
||||
* skúšky
|
||||
* plán lekcie
|
||||
* dokončené lekcie
|
||||
* správy
|
||||
* domáce úlohy
|
||||
* poznámky
|
||||
* šťastné číslo
|
||||
* ďalšie lekcie
|
||||
* školské stretnutie
|
||||
* informácie o žiakovi a škole
|
||||
* výpočet priemeru nezávisle od preferencií školy
|
||||
* upozornenia, napr. o nových známkach
|
||||
* podpora viacerých účtov s možnosťou premenovania žiakov
|
||||
* tmavý a čierny (AMOLED) motív
|
||||
* offline režim
|
||||
* voliteľné reklamy na podporu projektu
|
||||
Stiahnite si vybranú verziu z [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
|
||||
Odporúčame stiahnuť najnovšiu dostupnú verziu.
|
||||
|
||||
## Stiahnuť
|
||||
# O projekte Wulkanowy
|
||||
|
||||
Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGallery
|
||||
|
||||
[<img src="https://play.google.com/intl/sk/badges/images/generic/sk_badge_web_generic.png"
|
||||
alt="Nyní na Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Stiahnuť s F-Droid"
|
||||
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
|
||||
[<img src="https://i.imgur.com/sX8UyAw.png"
|
||||
alt="Objavíte v AppGallery"
|
||||
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
|
||||
|
||||
Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie
|
||||
|
||||
## Postavené s pomocou
|
||||
|
||||
|
||||
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
|
||||
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
|
||||
* [Hilt](https://dagger.dev/hilt/)
|
||||
* [Room](https://developer.android.com/topic/libraries/architecture/room)
|
||||
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
|
||||
|
||||
## Spolupráca
|
||||
|
||||
Prispejte do projektu vytvorením PR alebo odoslaním issue na GitHub.
|
||||
|
||||
Pre záujemcov o preklad aplikácie do rôznych jazykov poskytujeme Crowdin:
|
||||
https://crowdin.com/project/wulkanowy2
|
||||
|
||||
## Licencia
|
||||
|
||||
Tento projekt je licencovaný pod licenciou Apache License 2.0 - podrobnosti v súbore [LICENSE](LICENSE)
|
||||
Chcete si prečítať viac o projekte Wulkanowy? [Kliknite sem](https://github.com/wulkanowy/wulkanowy)
|
@ -27,15 +27,12 @@ android {
|
||||
testApplicationId "io.github.tests.wulkanowy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 157
|
||||
versionName "2.5.8"
|
||||
versionCode 173
|
||||
versionName "2.6.13"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
resValue "string", "app_name", "Wulkanowy"
|
||||
manifestPlaceholders = [
|
||||
firebase_enabled: project.hasProperty("enableFirebase"),
|
||||
admob_project_id: ""
|
||||
]
|
||||
manifestPlaceholders = [admob_project_id: ""]
|
||||
|
||||
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
|
||||
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
|
||||
@ -65,8 +62,8 @@ android {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
// signingConfig signingConfigs.release
|
||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
|
||||
}
|
||||
@ -76,7 +73,6 @@ android {
|
||||
resValue "string", "app_name", "Wulkanowy DEV"
|
||||
applicationIdSuffix ".dev"
|
||||
versionNameSuffix "-dev"
|
||||
ext.enableCrashlytics = project.hasProperty("enableFirebase")
|
||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
|
||||
}
|
||||
@ -164,8 +160,8 @@ play {
|
||||
defaultToAppBundles = false
|
||||
track = 'production'
|
||||
releaseStatus = ReleaseStatus.IN_PROGRESS
|
||||
userFraction = 0.99d
|
||||
updatePriority = 4
|
||||
userFraction = 0.1d
|
||||
updatePriority = 2
|
||||
enabled.set(false)
|
||||
}
|
||||
|
||||
@ -191,27 +187,29 @@ ext {
|
||||
room = "2.6.1"
|
||||
chucker = "4.0.0"
|
||||
mockk = "1.13.10"
|
||||
coroutines = "1.8.0"
|
||||
coroutines = "1.8.1"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'io.github.wulkanowy:sdk:2.5.8'
|
||||
implementation 'io.github.wulkanowy:sdk:2.6.11'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||
implementation "androidx.activity:activity-ktx:1.9.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.7.0"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
|
||||
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0-rc01"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
@ -237,7 +235,7 @@ dependencies {
|
||||
implementation 'com.github.ncapdevi:FragNav:3.3.0'
|
||||
implementation "com.github.YarikSOffice:lingver:1.3.0"
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
||||
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
|
||||
@ -250,9 +248,9 @@ dependencies {
|
||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
|
||||
playImplementation 'com.google.firebase:firebase-analytics'
|
||||
playImplementation 'com.google.firebase:firebase-messaging'
|
||||
playImplementation 'com.google.firebase:firebase-crashlytics:'
|
||||
@ -278,7 +276,7 @@ dependencies {
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.11.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.12.1'
|
||||
testImplementation "androidx.test:runner:1.5.2"
|
||||
testImplementation "androidx.test.ext:junit:1.1.5"
|
||||
testImplementation "androidx.test:core:1.5.0"
|
||||
|
@ -36,6 +36,37 @@
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1",
|
||||
"android_client_info": {
|
||||
"package_name": "io.github.wulkanowy"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": ""
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
@ -1,7 +1,8 @@
|
||||
#!/bin/bash -
|
||||
|
||||
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
|
||||
if [[ "${#content}" -gt 500 ]]; then
|
||||
content2=echo "$content" | dos2unix
|
||||
if [[ "${#content2}" -gt 500 ]]; then
|
||||
echo >&2 "Release notes content has reached the limit of 500 characters"
|
||||
exit 1
|
||||
fi
|
||||
|
@ -1,92 +0,0 @@
|
||||
{
|
||||
"agcgw": {
|
||||
"backurl": "connect-dre.hispace.hicloud.com",
|
||||
"url": "connect-dre.dbankcloud.cn",
|
||||
"websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
|
||||
"websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
|
||||
},
|
||||
"agcgw_all": {
|
||||
"CN": "connect-drcn.dbankcloud.cn",
|
||||
"CN_back": "connect-drcn.hispace.hicloud.com",
|
||||
"DE": "connect-dre.dbankcloud.cn",
|
||||
"DE_back": "connect-dre.hispace.hicloud.com",
|
||||
"RU": "connect-drru.hispace.dbankcloud.ru",
|
||||
"RU_back": "connect-drru.hispace.dbankcloud.cn",
|
||||
"SG": "connect-dra.dbankcloud.cn",
|
||||
"SG_back": "connect-dra.hispace.hicloud.com"
|
||||
},
|
||||
"websocketgw_all": {
|
||||
"CN": "connect-ws-drcn.hispace.dbankcloud.cn",
|
||||
"CN_back": "connect-ws-drcn.hispace.dbankcloud.com",
|
||||
"DE": "connect-ws-dre.hispace.dbankcloud.cn",
|
||||
"DE_back": "connect-ws-dre.hispace.dbankcloud.com",
|
||||
"RU": "connect-ws-drru.hispace.dbankcloud.ru",
|
||||
"RU_back": "connect-ws-drru.hispace.dbankcloud.cn",
|
||||
"SG": "connect-ws-dra.hispace.dbankcloud.cn",
|
||||
"SG_back": "connect-ws-dra.hispace.dbankcloud.com"
|
||||
},
|
||||
"client": {
|
||||
"cp_id": "890048000024105546",
|
||||
"product_id": "736430079244736562",
|
||||
"client_id": "514530959291319360",
|
||||
"client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B",
|
||||
"project_id": "736430079244736562",
|
||||
"app_id": "106552551",
|
||||
"api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS",
|
||||
"package_name": "io.github.wulkanowy.dev"
|
||||
},
|
||||
"oauth_client": {
|
||||
"client_id": "106552551",
|
||||
"client_type": 1
|
||||
},
|
||||
"app_info": {
|
||||
"app_id": "106552551",
|
||||
"package_name": "io.github.wulkanowy.dev"
|
||||
},
|
||||
"service": {
|
||||
"analytics": {
|
||||
"collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||
"collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||
"collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||
"resource_id": "p1",
|
||||
"channel_id": ""
|
||||
},
|
||||
"search":{
|
||||
"url":"https://search-dre.cloud.huawei.com"
|
||||
},
|
||||
"cloudstorage": {
|
||||
"storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
|
||||
"storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu",
|
||||
"storage_url_de": "https://ops-dre.agcstorage.link",
|
||||
"storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||
"storage_url_sg": "https://ops-dra.agcstorage.link",
|
||||
"storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn",
|
||||
"storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn"
|
||||
},
|
||||
"ml": {
|
||||
"mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
||||
}
|
||||
},
|
||||
"region": "DE",
|
||||
"configuration_version": "3.0",
|
||||
"appInfos": [
|
||||
{
|
||||
"package_name": "io.github.wulkanowy.dev",
|
||||
"client": {
|
||||
"app_id": "106552551"
|
||||
},
|
||||
"app_info": {
|
||||
"package_name": "io.github.wulkanowy.dev",
|
||||
"app_id": "106552551"
|
||||
},
|
||||
"oauth_client": {
|
||||
"client_type": 1,
|
||||
"client_id": "106552551"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -3,6 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@ -42,16 +44,16 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/WulkanowyTheme"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="DataExtractionRules,UnusedAttribute">
|
||||
<activity
|
||||
android:name=".ui.modules.splash.SplashActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/WulkanowyTheme.SplashScreen"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
tools:ignore="DiscouragedApi,LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@ -155,33 +157,9 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
|
||||
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:authorities="${applicationId}.firebaseinitprovider"
|
||||
android:enabled="${firebase_enabled}"
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass" />
|
||||
|
||||
<meta-data
|
||||
android:name="install_channel"
|
||||
android:value="${install_channel}" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_stat_all" />
|
||||
|
@ -13,8 +13,8 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.github.wulkanowy.data.api.AdminMessageService
|
||||
import io.github.wulkanowy.data.api.SchoolsService
|
||||
import io.github.wulkanowy.data.api.services.SchoolsService
|
||||
import io.github.wulkanowy.data.api.services.WulkanowyService
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.SharedPrefProvider
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
@ -71,7 +71,7 @@ internal class DataModule {
|
||||
okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
appInfo: AppInfo
|
||||
): AdminMessageService = Retrofit.Builder()
|
||||
): WulkanowyService = Retrofit.Builder()
|
||||
.baseUrl(appInfo.messagesBaseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
|
@ -1,11 +1,17 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -14,16 +20,39 @@ import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
sealed class Resource<T> {
|
||||
|
||||
open class Loading<T> : Resource<T>()
|
||||
sealed interface Resource<out T> {
|
||||
/**
|
||||
* The initial value of a resource flow. Indicates no data that is currently available to be shown,
|
||||
* however with the expectation that the state will transition to another one soon.
|
||||
*/
|
||||
open class Loading<T> : Resource<T>
|
||||
|
||||
/**
|
||||
* A semi-loading state with some data available to be displayed (usually cached data loaded from
|
||||
* the database). Still not the target state and it's expected to transition into another one soon.
|
||||
*/
|
||||
data class Intermediate<T>(val data: T) : Loading<T>()
|
||||
|
||||
data class Success<T>(val data: T) : Resource<T>()
|
||||
/**
|
||||
* The happy-path target state. Data can either be:
|
||||
* - loaded from the database - while it may seem like this case is already handled by the
|
||||
* Intermediate state, the difference here is semantic. Cached data is returned as Intermediate
|
||||
* when there's a API request in progress (or soon expected to be), however when there is no
|
||||
* intention of immediately querying the API, the cached data is returned as a Success.
|
||||
* - fetched from the API.
|
||||
*/
|
||||
data class Success<T>(val data: T) : Resource<T>
|
||||
|
||||
data class Error<T>(val error: Throwable) : Resource<T>()
|
||||
/**
|
||||
* Something bad happened and we were unable to get the requested data. This can be caused by
|
||||
* a database error, a network error, or really just any other error. Upon receiving this state
|
||||
* the UI can either: display a full screen error, or, when it has received any data previously,
|
||||
* display a snack bar informing of the problem.
|
||||
*/
|
||||
data class Error<T>(val error: Throwable) : Resource<T>
|
||||
}
|
||||
|
||||
val <T> Resource<T>.dataOrNull: T?
|
||||
@ -64,6 +93,22 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
|
||||
is Resource.Error -> Resource.Error(this.error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects another flow into this flow's resource data.
|
||||
*/
|
||||
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
|
||||
flow: Flow<T2>,
|
||||
crossinline block: suspend (T1, T2) -> R
|
||||
): Flow<Resource<R>> =
|
||||
combine(flow) { resource, inject ->
|
||||
when (resource) {
|
||||
is Resource.Success -> Resource.Success(block(resource.data, inject))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(resource.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
||||
val description = when (it) {
|
||||
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
||||
@ -74,8 +119,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
|
||||
Timber.i("$name: $description")
|
||||
}
|
||||
|
||||
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
|
||||
it.mapData(block)
|
||||
inline fun <T, U> Flow<Resource<T>>.mapResourceData(crossinline block: suspend (T) -> U) = map {
|
||||
when (it) {
|
||||
is Resource.Success -> Resource.Success(block(it.data))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
|
||||
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
|
||||
) = flatMapLatest {
|
||||
when (it) {
|
||||
is Resource.Success -> block(it.data)
|
||||
is Resource.Intermediate -> block(it.data).map { newRes ->
|
||||
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
|
||||
else newRes
|
||||
}
|
||||
|
||||
is Resource.Loading -> flowOf(Resource.Loading())
|
||||
is Resource.Error -> flowOf(Resource.Error(it.error))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
||||
@ -105,13 +171,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
|
||||
if (it is Resource.Error) {
|
||||
block(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
|
||||
if (it !is Resource.Loading) {
|
||||
block()
|
||||
}
|
||||
@ -121,70 +187,99 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
|
||||
|
||||
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
||||
|
||||
inline fun <ResultType, RequestType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (ResultType) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline filterResult: (ResultType) -> ResultType = { it }
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
|
||||
// use `debounceIntermediates` to alleviate this behavior.
|
||||
inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
|
||||
combine(flows) { items ->
|
||||
var isIntermediate = false
|
||||
val data = mutableListOf<T>()
|
||||
for (item in items) {
|
||||
when (item) {
|
||||
is Resource.Success -> data.add(item.data)
|
||||
is Resource.Intermediate -> {
|
||||
isIntermediate = true
|
||||
data.add(item.data)
|
||||
}
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val filteredResult = filterResult(data)
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
|
||||
emit(Resource.Intermediate(filteredResult))
|
||||
is Resource.Loading -> return@combine Resource.Loading()
|
||||
is Resource.Error -> continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
if (data.isEmpty()) {
|
||||
// All items have to be errors for this to happen, so just return the first one.
|
||||
// mapData is functionally useless and exists only to satisfy the type checker
|
||||
items.first().mapData { listOf(it) }
|
||||
} else if (isIntermediate) {
|
||||
Resource.Intermediate(data)
|
||||
} else {
|
||||
Resource.Success(data)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
|
||||
var wasIntermediate = false
|
||||
|
||||
emitAll(this@debounceIntermediates.debounce {
|
||||
if (it is Resource.Intermediate) {
|
||||
if (!wasIntermediate) {
|
||||
wasIntermediate = true
|
||||
Duration.ZERO
|
||||
} else {
|
||||
timeout
|
||||
}
|
||||
} else {
|
||||
wasIntermediate = false
|
||||
Duration.ZERO
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
inline fun <OutputType, ApiType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<OutputType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (OutputType) -> Boolean = { true },
|
||||
crossinline filterResult: (OutputType) -> OutputType = { it }
|
||||
) = networkBoundResource(
|
||||
mutex = mutex,
|
||||
isResultEmpty = isResultEmpty,
|
||||
query = query,
|
||||
fetch = fetch,
|
||||
saveFetchResult = saveFetchResult,
|
||||
shouldFetch = shouldFetch,
|
||||
mapResult = filterResult
|
||||
)
|
||||
|
||||
@JvmName("networkBoundResourceWithMap")
|
||||
inline fun <ResultType, RequestType, T> networkBoundResource(
|
||||
inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (T) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline mapResult: (ResultType) -> T,
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<DatabaseType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
|
||||
crossinline mapResult: (DatabaseType) -> OutputType,
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val mappedResult = mapResult(data)
|
||||
if (shouldFetch(data)) {
|
||||
emit(Resource.Intermediate(data))
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
|
||||
emit(Resource.Intermediate(mappedResult))
|
||||
}
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
val newData = fetch()
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
emit(Resource.Error(throwable))
|
||||
return@flow
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
emitAll(query().map { Resource.Success(it) })
|
||||
}
|
||||
.mapResourceData { mapResult(it) }
|
||||
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }
|
||||
|
@ -1,13 +1,21 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.javascriptengine.JavaScriptSandbox
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.data.db.dao.StudentDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
|
||||
import io.github.wulkanowy.data.repositories.WulkanowyRepository
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
|
||||
import io.github.wulkanowy.utils.RemoteConfigHelper
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
@ -16,18 +24,24 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class WulkanowySdkFactory @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val chuckerInterceptor: ChuckerInterceptor,
|
||||
private val remoteConfig: RemoteConfigHelper,
|
||||
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
|
||||
private val studentDb: StudentDao,
|
||||
private val wulkanowyRepository: WulkanowyRepository,
|
||||
) {
|
||||
|
||||
private val eduOneMutex = Mutex()
|
||||
private val migrationFailedStudentIds = mutableSetOf<Long>()
|
||||
private val sandbox: ListenableFuture<JavaScriptSandbox>? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
|
||||
JavaScriptSandbox.createConnectedInstanceAsync(context)
|
||||
else null
|
||||
|
||||
private val sdk = Sdk().apply {
|
||||
androidVersion = android.os.Build.VERSION.RELEASE
|
||||
buildTag = android.os.Build.MODEL
|
||||
androidVersion = Build.VERSION.RELEASE
|
||||
buildTag = Build.MODEL
|
||||
userAgentTemplate = remoteConfig.userAgentTemplate
|
||||
setSimpleHttpLogger { Timber.d(it) }
|
||||
setAdditionalCookieManager(webkitCookieManagerProxy)
|
||||
@ -36,14 +50,46 @@ class WulkanowySdkFactory @Inject constructor(
|
||||
addInterceptor(chuckerInterceptor, network = true)
|
||||
}
|
||||
|
||||
fun create() = sdk
|
||||
fun createBase() = sdk
|
||||
|
||||
suspend fun create(): Sdk {
|
||||
val mapping = wulkanowyRepository.getMapping()
|
||||
|
||||
return createBase().apply {
|
||||
if (mapping != null) {
|
||||
endpointsMapping = mapping.endpoints
|
||||
vTokenMapping = mapping.vTokens
|
||||
vHeaders = mapping.vHeaders
|
||||
vParamsEvaluation = createIsolate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createIsolate(): suspend () -> EvaluateHandler {
|
||||
return {
|
||||
val isolate = sandbox?.await()?.createIsolate()
|
||||
object : EvaluateHandler {
|
||||
override suspend fun evaluate(code: String): String? {
|
||||
return isolate?.evaluateJavaScriptAsync(code)?.await()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
isolate?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(student: Student, semester: Semester? = null): Sdk {
|
||||
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
|
||||
return buildSdk(student, semester, overrideIsEduOne)
|
||||
}
|
||||
|
||||
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
|
||||
private suspend fun buildSdk(
|
||||
student: Student,
|
||||
semester: Semester?,
|
||||
isStudentEduOne: Boolean
|
||||
): Sdk {
|
||||
return create().apply {
|
||||
email = student.email
|
||||
password = student.password
|
||||
|
@ -0,0 +1,20 @@
|
||||
package io.github.wulkanowy.data.api.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Mapping(
|
||||
|
||||
@SerialName("endpoints")
|
||||
val endpoints: Map<String, Map<String, Map<String, String>>>,
|
||||
|
||||
@SerialName("vTokens")
|
||||
val vTokens: Map<String, Map<String, Map<String, String>>>,
|
||||
|
||||
@SerialName("vTokenScheme")
|
||||
val vTokenScheme: Map<String, Map<String, String>> = emptyMap(),
|
||||
|
||||
@SerialName("vHeaders")
|
||||
val vHeaders: Map<String, Map<String, Map<String, String>>> = emptyMap(),
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package io.github.wulkanowy.data.api
|
||||
package io.github.wulkanowy.data.api.services
|
||||
|
||||
import io.github.wulkanowy.data.pojos.IntegrityRequest
|
||||
import io.github.wulkanowy.data.pojos.LoginEvent
|
@ -1,12 +1,16 @@
|
||||
package io.github.wulkanowy.data.api
|
||||
package io.github.wulkanowy.data.api.services
|
||||
|
||||
import io.github.wulkanowy.data.api.models.Mapping
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import retrofit2.http.GET
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
interface AdminMessageService {
|
||||
interface WulkanowyService {
|
||||
|
||||
@GET("/v1.json")
|
||||
suspend fun getAdminMessages(): List<AdminMessage>
|
||||
}
|
||||
|
||||
@GET("/mapping2.json")
|
||||
suspend fun getMapping(): Mapping
|
||||
}
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Singleton
|
||||
|
@ -12,4 +12,8 @@ interface GradeDao : BaseDao<Grade> {
|
||||
|
||||
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId")
|
||||
fun loadAll(semesterId: Int, studentId: Int): Flow<List<Grade>>
|
||||
|
||||
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId " +
|
||||
"AND entry NOT IN(:censoredEntries)")
|
||||
fun loadAllCensored(semesterId: Int, studentId: Int, censoredEntries: Array<String>): Flow<List<Grade>>
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
@Dao
|
||||
interface NoteDao : BaseDao<Note> {
|
||||
|
||||
@Query("SELECT * FROM Notes WHERE student_id = :studentId")
|
||||
fun loadAll(studentId: Int): Flow<List<Note>>
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@ -34,6 +36,8 @@ data class AdminMessage(
|
||||
|
||||
val priority: String,
|
||||
|
||||
@SerialName("messageTypes")
|
||||
@Serializable(with = SafeMessageTypeEnumListSerializer::class)
|
||||
@ColumnInfo(name = "types", defaultValue = "[]")
|
||||
val types: List<MessageType> = emptyList(),
|
||||
|
||||
|
@ -7,6 +7,6 @@ enum class AppTheme(val value: String) {
|
||||
BLACK("black");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().find { it.value == value } ?: LIGHT
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: LIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package io.github.wulkanowy.data.enums
|
||||
|
||||
enum class AttendanceCalculatorSortingMode(private val value: String) {
|
||||
ALPHABETIC("alphabetic"),
|
||||
ATTENDANCE("attendance_percentage"),
|
||||
LESSON_BALANCE("lesson_balance");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) =
|
||||
AttendanceCalculatorSortingMode.values()
|
||||
.find { it.value == value } ?: ALPHABETIC
|
||||
}
|
||||
}
|
@ -8,6 +8,6 @@ enum class GradeColorTheme(val value: String) : Serializable {
|
||||
GRADE_COLOR("grade_color");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().find { it.value == value } ?: VULCAN
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: VULCAN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,6 @@ enum class GradeExpandMode(val value: String) {
|
||||
ALWAYS_EXPANDED("always");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().find { it.value == value } ?: ONE
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: ONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,6 @@ enum class GradeSortingMode(val value: String) {
|
||||
AVERAGE("average");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().find { it.value == value } ?: ALPHABETIC
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: ALPHABETIC
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ enum class MessageType {
|
||||
GENERAL_MESSAGE,
|
||||
DASHBOARD_MESSAGE,
|
||||
LOGIN_MESSAGE,
|
||||
LOGIN_STUDENT_SELECT_MESSAGE,
|
||||
LOGIN_SYMBOL_MESSAGE,
|
||||
PASS_RESET_MESSAGE,
|
||||
ERROR_OVERRIDE,
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package io.github.wulkanowy.data.enums
|
||||
|
||||
enum class ShowAdditionalLessonsMode(val value: String) {
|
||||
NONE("none"),
|
||||
INLINE("inline"),
|
||||
BELOW("below");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: INLINE
|
||||
}
|
||||
}
|
@ -6,6 +6,6 @@ enum class TimetableMode(val value: String) {
|
||||
SMALL_OTHER_GROUP("small");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().find { it.value == value } ?: ONLY_CURRENT_GROUP
|
||||
fun getByValue(value: String) = entries.find { it.value == value } ?: ONLY_CURRENT_GROUP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
package io.github.wulkanowy.data.pojos
|
||||
|
||||
data class AttendanceData(
|
||||
val subjectName: String,
|
||||
val lessonBalance: Int,
|
||||
val presences: Int,
|
||||
val absences: Int,
|
||||
) {
|
||||
val total: Int
|
||||
get() = presences + absences
|
||||
|
||||
val presencePercentage: Double
|
||||
get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.api.AdminMessageService
|
||||
import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AdminMessageRepository @Inject constructor(
|
||||
private val adminMessageService: AdminMessageService,
|
||||
private val adminMessageDao: AdminMessageDao,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
|
||||
networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { false },
|
||||
query = { adminMessageDao.loadAll() },
|
||||
fetch = { adminMessageService.getAdminMessages() },
|
||||
shouldFetch = { true },
|
||||
saveFetchResult = { oldItems, newItems ->
|
||||
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
||||
},
|
||||
showSavedOnLoading = false,
|
||||
)
|
||||
}
|
@ -9,12 +9,15 @@ import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.pojo.Absent
|
||||
import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
@ -28,12 +31,71 @@ class AttendanceRepository @Inject constructor(
|
||||
private val timetableDb: TimetableDao,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val cacheKey = "attendance"
|
||||
|
||||
private fun filterAttendance(
|
||||
hiddenAttendanceTiles: List<DashboardItem.HiddenAttendanceTile>,
|
||||
attendanceItem: Attendance
|
||||
): Boolean {
|
||||
return when {
|
||||
attendanceItem.absence && attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.EXCUSED_ABSENCE
|
||||
) -> false
|
||||
|
||||
attendanceItem.absence && !attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.UNEXCUSED_ABSENCE
|
||||
) -> false
|
||||
|
||||
attendanceItem.lateness && attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.EXCUSED_LATENESS
|
||||
) -> false
|
||||
|
||||
attendanceItem.lateness && !attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.UNEXCUSED_LATENESS
|
||||
) -> false
|
||||
|
||||
attendanceItem.exemption && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.EXEMPTION) -> false
|
||||
attendanceItem.deleted && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.DELETED) -> false
|
||||
attendanceItem.presence && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.PRESENT) -> false
|
||||
|
||||
else -> !hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterAttendance(
|
||||
hiddenAttendanceTiles: List<DashboardItem.HiddenAttendanceTile>,
|
||||
attendanceItem: SdkAttendance
|
||||
): Boolean {
|
||||
return when {
|
||||
attendanceItem.absence && attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.EXCUSED_ABSENCE
|
||||
) -> false
|
||||
|
||||
attendanceItem.absence && !attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.UNEXCUSED_ABSENCE
|
||||
) -> false
|
||||
|
||||
attendanceItem.lateness && attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.EXCUSED_LATENESS
|
||||
) -> false
|
||||
|
||||
attendanceItem.lateness && !attendanceItem.excused && hiddenAttendanceTiles.contains(
|
||||
DashboardItem.HiddenAttendanceTile.UNEXCUSED_LATENESS
|
||||
) -> false
|
||||
|
||||
attendanceItem.exemption && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.EXEMPTION) -> false
|
||||
attendanceItem.deleted && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.DELETED) -> false
|
||||
attendanceItem.presence && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.PRESENT) -> false
|
||||
|
||||
else -> !hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttendance(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
@ -51,14 +113,24 @@ class AttendanceRepository @Inject constructor(
|
||||
it.isEmpty() || forceRefresh || isExpired
|
||||
},
|
||||
query = {
|
||||
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
|
||||
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
|
||||
|
||||
attendanceDb
|
||||
.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
|
||||
.map {
|
||||
it.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
|
||||
}
|
||||
},
|
||||
fetch = {
|
||||
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
|
||||
|
||||
val lessons = timetableDb.load(
|
||||
semester.diaryId, semester.studentId, start.monday, end.sunday
|
||||
)
|
||||
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getAttendance(start.monday, end.sunday)
|
||||
.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
|
||||
.mapToEntities(semester, lessons)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
@ -79,7 +151,13 @@ class AttendanceRepository @Inject constructor(
|
||||
start: LocalDate,
|
||||
end: LocalDate
|
||||
): Flow<List<Attendance>> {
|
||||
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
|
||||
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
|
||||
|
||||
return attendanceDb
|
||||
.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
|
||||
.map {
|
||||
it.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateTimetable(timetable: List<Attendance>) {
|
||||
|
@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -30,10 +31,18 @@ class GradeRepository @Inject constructor(
|
||||
private val gradeDescriptiveDb: GradeDescriptiveDao,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private fun loadGrades(semesterId: Int, studentId: Int): Flow<List<Grade>> {
|
||||
val hiddenGrades = preferencesRepository.hiddenGrades
|
||||
|
||||
Timber.i("Load grades for semester $semesterId student $studentId")
|
||||
return gradeDb.loadAllCensored(semesterId, studentId, hiddenGrades.toTypedArray())
|
||||
}
|
||||
|
||||
fun getGrades(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
@ -61,11 +70,15 @@ class GradeRepository @Inject constructor(
|
||||
}
|
||||
},
|
||||
fetch = {
|
||||
val hiddenGrades = preferencesRepository.hiddenGrades
|
||||
|
||||
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
|
||||
.getGrades(semester.semesterId)
|
||||
|
||||
val censoredDetails = details.filterNot { it.entry in hiddenGrades }
|
||||
|
||||
Triple(
|
||||
details.mapToEntities(semester),
|
||||
censoredDetails.mapToEntities(semester),
|
||||
summary.mapToEntities(semester),
|
||||
descriptive.mapToEntities(semester)
|
||||
)
|
||||
@ -157,13 +170,13 @@ class GradeRepository @Inject constructor(
|
||||
}
|
||||
|
||||
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
|
||||
return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
|
||||
return loadGrades(semester.semesterId, semester.studentId).map {
|
||||
it.filter { grade -> !grade.isRead }
|
||||
}
|
||||
}
|
||||
|
||||
fun getGradesFromDatabase(semester: Semester): Flow<List<Grade>> {
|
||||
return gradeDb.loadAll(semester.semesterId, semester.studentId)
|
||||
return loadGrades(semester.semesterId, semester.studentId)
|
||||
}
|
||||
|
||||
fun getGradesPredictedFromDatabase(semester: Semester): Flow<List<GradeSummary>> {
|
||||
|
@ -6,6 +6,8 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
|
||||
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -18,6 +20,7 @@ import javax.inject.Singleton
|
||||
class LuckyNumberRepository @Inject constructor(
|
||||
private val luckyNumberDb: LuckyNumberDao,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -26,6 +29,7 @@ class LuckyNumberRepository @Inject constructor(
|
||||
student: Student,
|
||||
forceRefresh: Boolean,
|
||||
notify: Boolean = false,
|
||||
isFromAppWidget: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { it == null },
|
||||
@ -44,6 +48,9 @@ class LuckyNumberRepository @Inject constructor(
|
||||
oldItems = listOfNotNull(oldLuckyNumber),
|
||||
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
|
||||
)
|
||||
if (!isFromAppWidget) {
|
||||
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -122,7 +122,7 @@ class MessageRepository @Inject constructor(
|
||||
fetch = {
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMessageDetails(
|
||||
messageKey = it!!.message.messageGlobalKey,
|
||||
messageKey = message.messageGlobalKey,
|
||||
markAsRead = message.unread && markAsRead,
|
||||
)
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ class NoteRepository @Inject constructor(
|
||||
private val noteDb: NoteDao,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -39,12 +40,16 @@ class NoteRepository @Inject constructor(
|
||||
val isExpired = refreshHelper.shouldBeRefreshed(
|
||||
getRefreshKey(cacheKey, semester)
|
||||
)
|
||||
|
||||
it.isEmpty() || forceRefresh || isExpired
|
||||
},
|
||||
query = { noteDb.loadAll(student.studentId) },
|
||||
fetch = {
|
||||
val showNotes = preferencesRepository.showNotes
|
||||
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getNotes()
|
||||
.filter { showNotes }
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
|
@ -7,12 +7,16 @@ import androidx.core.content.edit
|
||||
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
|
||||
import com.fredporciuncula.flow.preferences.Preference
|
||||
import com.fredporciuncula.flow.preferences.Serializer
|
||||
import com.fredporciuncula.flow.preferences.map
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.api.models.Mapping
|
||||
import io.github.wulkanowy.data.enums.AppTheme
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||
import io.github.wulkanowy.data.enums.GradeColorTheme
|
||||
import io.github.wulkanowy.data.enums.GradeExpandMode
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode
|
||||
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
|
||||
import io.github.wulkanowy.data.enums.TimetableGapsMode
|
||||
import io.github.wulkanowy.data.enums.TimetableMode
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
@ -22,6 +26,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
@ -34,6 +39,7 @@ class PreferencesRepository @Inject constructor(
|
||||
private val flowSharedPref: FlowSharedPreferences,
|
||||
private val json: Json,
|
||||
) {
|
||||
private val NO_ATTENDANCE_VALUE = -1.0
|
||||
|
||||
val isShowPresent: Boolean
|
||||
get() = getBoolean(
|
||||
@ -41,6 +47,27 @@ class PreferencesRepository @Inject constructor(
|
||||
R.bool.pref_default_attendance_present
|
||||
)
|
||||
|
||||
val targetAttendanceFlow: Flow<Int>
|
||||
get() = flowSharedPref.getInt(
|
||||
context.getString(R.string.pref_key_attendance_target),
|
||||
context.resources.getInteger(R.integer.pref_default_attendance_target)
|
||||
).asFlow()
|
||||
|
||||
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
|
||||
get() = flowSharedPref.getString(
|
||||
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
|
||||
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
|
||||
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
|
||||
|
||||
/**
|
||||
* Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0).
|
||||
*/
|
||||
val attendanceCalculatorShowEmptySubjects: Flow<Boolean>
|
||||
get() = flowSharedPref.getBoolean(
|
||||
context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects),
|
||||
context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects)
|
||||
).asFlow()
|
||||
|
||||
private val gradeAverageModePref: Preference<GradeAverageMode>
|
||||
get() = getObjectFlow(
|
||||
R.string.pref_key_grade_average_mode,
|
||||
@ -191,6 +218,12 @@ class PreferencesRepository @Inject constructor(
|
||||
)
|
||||
)
|
||||
|
||||
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
|
||||
get() = getString(
|
||||
R.string.pref_key_timetable_show_additional_lessons,
|
||||
R.string.pref_default_timetable_show_additional_lessons
|
||||
).let { ShowAdditionalLessonsMode.getByValue(it) }
|
||||
|
||||
val gradeSortingMode: GradeSortingMode
|
||||
get() = GradeSortingMode.getByValue(
|
||||
getString(
|
||||
@ -272,6 +305,60 @@ class PreferencesRepository @Inject constructor(
|
||||
selectedDashboardTilesPreference.set(filteredValue)
|
||||
}
|
||||
|
||||
var attendancePercentage: Double?
|
||||
get() = attendancePercentagePreference.get().takeIf { it != NO_ATTENDANCE_VALUE }
|
||||
set(value) = attendancePercentagePreference.set(value ?: NO_ATTENDANCE_VALUE)
|
||||
|
||||
var hiddenAttendanceItems: List<DashboardItem.HiddenAttendanceTile>
|
||||
get() = hiddenAttendanceItemsPreference.get().toList()
|
||||
set(value) = hiddenAttendanceItemsPreference.set(value.toSet())
|
||||
|
||||
var hiddenGrades: List<String>
|
||||
get() = hiddenGradesPreference.get().toList()
|
||||
set(value) = hiddenGradesPreference.set(value.toSet())
|
||||
|
||||
var showNotes: Boolean
|
||||
get() = showNotesPreference.get()
|
||||
set(value) = showNotesPreference.set(value)
|
||||
|
||||
var developerMode: Boolean
|
||||
get() = developerModePreference.get()
|
||||
set(value) = developerModePreference.set(value)
|
||||
|
||||
private val developerModePreference: Preference<Boolean>
|
||||
get() = flowSharedPref.getBoolean(
|
||||
context.getString(R.string.pref_key_developer_mode),
|
||||
context.resources.getBoolean(R.bool.pref_default_developer_mode)
|
||||
)
|
||||
|
||||
private val hiddenGradesPreference: Preference<Set<String>>
|
||||
get() {
|
||||
val defaultSet = context.resources.getStringArray(R.array.pref_default_hidden_grades).toSet()
|
||||
val prefKey = "hidden_grades"
|
||||
|
||||
return flowSharedPref.getStringSet(prefKey, defaultSet)
|
||||
}
|
||||
|
||||
private val showNotesPreference: Preference<Boolean>
|
||||
get() = flowSharedPref.getBoolean(
|
||||
context.getString(R.string.pref_key_show_notes),
|
||||
context.resources.getBoolean(R.bool.pref_default_show_notes)
|
||||
)
|
||||
|
||||
private val hiddenAttendanceItemsPreference: Preference<Set<DashboardItem.HiddenAttendanceTile>>
|
||||
get() {
|
||||
val defaultSet =
|
||||
context.resources.getStringArray(R.array.pref_default_hidden_attendance_items).toSet()
|
||||
val prefKey = "attendance_items"
|
||||
|
||||
return flowSharedPref
|
||||
.getStringSet(prefKey, defaultSet)
|
||||
.map(
|
||||
mapper = { it -> it.map { DashboardItem.HiddenAttendanceTile.valueOf(it) }.toSet() },
|
||||
reverse = { it -> it.map { it.name }.toSet() }
|
||||
)
|
||||
}
|
||||
|
||||
private val selectedDashboardTilesPreference: Preference<Set<String>>
|
||||
get() {
|
||||
val defaultSet =
|
||||
@ -281,6 +368,19 @@ class PreferencesRepository @Inject constructor(
|
||||
return flowSharedPref.getStringSet(prefKey, defaultSet)
|
||||
}
|
||||
|
||||
private val attendancePercentagePreference: Preference<Double>
|
||||
get() {
|
||||
val prefKey = context.getString(R.string.pref_key_attendance_percentage)
|
||||
val defaultValue = context.resources.getString(R.string.pref_default_attendance_percentage)
|
||||
|
||||
return flowSharedPref
|
||||
.getString(prefKey, defaultValue)
|
||||
.map(
|
||||
mapper = { it.toDoubleOrNull() ?: NO_ATTENDANCE_VALUE },
|
||||
reverse = { it.toString() }
|
||||
)
|
||||
}
|
||||
|
||||
var dismissedAdminMessageIds: List<Int>
|
||||
get() = sharedPref.getStringSet(PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS, emptySet())
|
||||
.orEmpty()
|
||||
@ -346,6 +446,15 @@ class PreferencesRepository @Inject constructor(
|
||||
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
|
||||
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
|
||||
|
||||
var mapping: Mapping?
|
||||
get() {
|
||||
val value = sharedPref.getString("mapping", null)
|
||||
return value?.let { json.decodeFromString(it) }
|
||||
}
|
||||
set(value) = sharedPref.edit(commit = true) {
|
||||
putString("mapping", value?.let { json.encodeToString(it) })
|
||||
}
|
||||
|
||||
init {
|
||||
if (installationId.isEmpty()) {
|
||||
installationId = UUID.randomUUID().toString()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.api.SchoolsService
|
||||
import io.github.wulkanowy.data.api.services.SchoolsService
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
|
@ -199,7 +199,7 @@ class StudentRepository @Inject constructor(
|
||||
|
||||
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
|
||||
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
|
||||
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
|
||||
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
|
||||
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
|
||||
.getOrNull() ?: return
|
||||
|
||||
|
@ -13,6 +13,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.pojos.TimetableFull
|
||||
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
|
||||
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
|
||||
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.monday
|
||||
@ -26,6 +28,7 @@ import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
class TimetableRepository @Inject constructor(
|
||||
private val timetableDb: TimetableDao,
|
||||
@ -34,6 +37,7 @@ class TimetableRepository @Inject constructor(
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val schedulerHelper: TimetableNotificationSchedulerHelper,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -52,7 +56,8 @@ class TimetableRepository @Inject constructor(
|
||||
forceRefresh: Boolean,
|
||||
refreshAdditional: Boolean = false,
|
||||
notify: Boolean = false,
|
||||
timetableType: TimetableType = TimetableType.NORMAL
|
||||
timetableType: TimetableType = TimetableType.NORMAL,
|
||||
isFromAppWidget: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = {
|
||||
@ -83,6 +88,9 @@ class TimetableRepository @Inject constructor(
|
||||
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
if (!isFromAppWidget) {
|
||||
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
|
||||
}
|
||||
},
|
||||
filterResult = { (timetable, additional, headers) ->
|
||||
TimetableFull(
|
||||
|
@ -0,0 +1,66 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.api.models.Mapping
|
||||
import io.github.wulkanowy.data.api.services.WulkanowyService
|
||||
import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class WulkanowyRepository @Inject constructor(
|
||||
private val wulkanowyService: WulkanowyService,
|
||||
private val adminMessageDao: AdminMessageDao,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val cacheKey = "mapping_refresh_key"
|
||||
|
||||
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
|
||||
networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { false },
|
||||
query = { adminMessageDao.loadAll() },
|
||||
fetch = { wulkanowyService.getAdminMessages() },
|
||||
shouldFetch = { true },
|
||||
saveFetchResult = { oldItems, newItems ->
|
||||
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
||||
},
|
||||
)
|
||||
.filterNot { it is Resource.Intermediate }
|
||||
|
||||
suspend fun getMapping(): Mapping? {
|
||||
var savedMapping = preferencesRepository.mapping
|
||||
|
||||
val isExpired = refreshHelper.shouldBeRefreshed(
|
||||
key = getRefreshKey(cacheKey)
|
||||
)
|
||||
|
||||
if (savedMapping == null || isExpired) {
|
||||
fetchMapping()
|
||||
savedMapping = preferencesRepository.mapping
|
||||
}
|
||||
|
||||
return savedMapping
|
||||
}
|
||||
|
||||
suspend fun fetchMapping() {
|
||||
runCatching { wulkanowyService.getMapping() }
|
||||
.onFailure { Timber.e(it) }
|
||||
.onSuccess {
|
||||
preferencesRepository.mapping = it
|
||||
refreshHelper.updateLastRefreshTimestamp(cacheKey)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package io.github.wulkanowy.data.serializers
|
||||
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object SafeMessageTypeEnumListSerializer : KSerializer<List<MessageType>> {
|
||||
|
||||
private val serializer = ListSerializer(String.serializer())
|
||||
|
||||
override val descriptor = serializer.descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: List<MessageType>) {
|
||||
encoder.encodeNotNullMark()
|
||||
serializer.serialize(encoder, value.map { it.name })
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): List<MessageType> =
|
||||
serializer.deserialize(decoder).mapNotNull { enumName ->
|
||||
MessageType.entries.find { it.name == enumName }
|
||||
}
|
||||
}
|
@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.repositories.AdminMessageRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.WulkanowyRepository
|
||||
import io.github.wulkanowy.utils.AppInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetAppropriateAdminMessageUseCase @Inject constructor(
|
||||
private val adminMessageRepository: AdminMessageRepository,
|
||||
private val wulkanowyRepository: WulkanowyRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val appInfo: AppInfo
|
||||
) {
|
||||
@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
|
||||
}
|
||||
|
||||
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
|
||||
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
|
||||
return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
|
||||
adminMessages
|
||||
.asSequence()
|
||||
.filter { it.isNotDismissed() }
|
||||
|
@ -0,0 +1,106 @@
|
||||
package io.github.wulkanowy.domain.attendance
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.Subject
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.*
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SubjectRepository
|
||||
import io.github.wulkanowy.utils.allAbsences
|
||||
import io.github.wulkanowy.utils.allPresences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
class GetAttendanceCalculatorDataUseCase @Inject constructor(
|
||||
private val subjectRepository: SubjectRepository,
|
||||
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
forceRefresh: Boolean,
|
||||
): Flow<Resource<List<AttendanceData>>> =
|
||||
subjectRepository.getSubjects(student, semester, forceRefresh)
|
||||
.mapResourceData { subjects -> subjects.sortedBy(Subject::name) }
|
||||
.combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair)
|
||||
.flatMapResourceData { (subjects, targetFreq) ->
|
||||
combineResourceFlows(subjects.map { subject ->
|
||||
attendanceSummaryRepository.getAttendanceSummary(
|
||||
student = student,
|
||||
semester = semester,
|
||||
subjectId = subject.realId,
|
||||
forceRefresh = forceRefresh
|
||||
).mapResourceData { summaries ->
|
||||
summaries.toAttendanceData(subject.name, targetFreq)
|
||||
}
|
||||
})
|
||||
// Every individual combined flow causes separate network requests to update data.
|
||||
// When there is N child flows, they can cause up to N-1 items to be emitted. Since all
|
||||
// requests are usually completed in less than 5s, there is no need to emit multiple
|
||||
// intermediates that will be visible for barely any time.
|
||||
.debounceIntermediates()
|
||||
}
|
||||
.combineWithResourceData(preferencesRepository.attendanceCalculatorShowEmptySubjects) { attendanceDataList, showEmptySubjects ->
|
||||
attendanceDataList.filter { it.total != 0 || showEmptySubjects }
|
||||
}
|
||||
.combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List<AttendanceData>::sortedBy)
|
||||
}
|
||||
|
||||
private fun List<AttendanceSummary>.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData {
|
||||
val presences = sumOf { it.allPresences }
|
||||
val absences = sumOf { it.allAbsences }
|
||||
return AttendanceData(
|
||||
subjectName = subjectName,
|
||||
lessonBalance = calcLessonBalance(
|
||||
targetFreq.toDouble() / 100, presences, absences
|
||||
),
|
||||
presences = presences,
|
||||
absences = absences,
|
||||
)
|
||||
}
|
||||
|
||||
private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int {
|
||||
val total = presences + absences
|
||||
// The `+ 1` is to avoid false positives in close cases. Eg.:
|
||||
// target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as
|
||||
// a positive balance of +1, however that is not actually true as skipping one class
|
||||
// would make it so that the balance would actually be negative (-98). The `+ 1`
|
||||
// fixes this and makes sure that in situations like these, it's not reporting incorrect
|
||||
// balances
|
||||
return when {
|
||||
presences / (total + 1f) >= targetFreq -> calcMissingAbsences(
|
||||
targetFreq, absences, presences
|
||||
)
|
||||
presences / (total + 0f) < targetFreq -> -calcMissingPresences(
|
||||
targetFreq, absences, presences
|
||||
)
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) =
|
||||
calcMinRequiredPresencesFor(targetFreq, absences) - presences
|
||||
|
||||
private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) =
|
||||
ceil((targetFreq / (1 - targetFreq)) * absences).toInt()
|
||||
|
||||
private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) =
|
||||
calcMinRequiredAbsencesFor(targetFreq, presences) - absences
|
||||
|
||||
private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) =
|
||||
floor((presences * (1 - targetFreq)) / targetFreq).toInt()
|
||||
|
||||
private fun List<AttendanceData>.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
|
||||
ALPHABETIC -> sortedBy(AttendanceData::subjectName)
|
||||
ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
|
||||
LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
|
||||
}
|
@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
|
||||
private fun String.getUnauthorizedVersion(): String {
|
||||
return normalizeStudentName().split(" ")
|
||||
.joinToString(" ") {
|
||||
it.first() + "*".repeat(it.length - 1)
|
||||
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,16 +19,18 @@ class NewGradeNotification @Inject constructor(
|
||||
) {
|
||||
|
||||
suspend fun notifyDetails(items: List<Grade>, student: Student) {
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.grade_new_items, 1),
|
||||
content = buildString {
|
||||
append("${it.subject}: ${it.entry}")
|
||||
if (it.comment.isNotBlank()) append(" (${it.comment})")
|
||||
},
|
||||
destination = Destination.Grade,
|
||||
)
|
||||
}
|
||||
val notificationDataList = items
|
||||
.filter { !it.isNotified }
|
||||
.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.grade_new_items, 1),
|
||||
content = buildString {
|
||||
append("${it.subject}: ${it.entry}")
|
||||
if (it.comment.isNotBlank()) append(" (${it.comment})")
|
||||
},
|
||||
destination = Destination.Grade,
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
|
@ -3,14 +3,19 @@ package io.github.wulkanowy.ui.modules.about
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.ItemAboutBinding
|
||||
import io.github.wulkanowy.databinding.ScrollableHeaderAboutBinding
|
||||
import javax.inject.Inject
|
||||
|
||||
class AboutAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class AboutAdapter @Inject constructor(
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var developerModeClicks = 0
|
||||
private enum class ViewType(val id: Int) {
|
||||
ITEM_HEADER(1),
|
||||
ITEM_ELEMENT(2)
|
||||
@ -46,6 +51,19 @@ class AboutAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.Vie
|
||||
|
||||
private fun bindHeaderViewHolder(binding: ScrollableHeaderAboutBinding) {
|
||||
with(binding.aboutScrollableHeaderIcon) {
|
||||
setOnClickListener {
|
||||
if (++developerModeClicks == 5 && !preferencesRepository.developerMode) {
|
||||
preferencesRepository.developerMode = true
|
||||
developerModeClicks = 0
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
"done!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
setImageDrawable(ResourcesCompat.getDrawableForDensity(
|
||||
context.resources, context.applicationInfo.icon, 640, null)
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
||||
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
|
||||
else false
|
||||
}
|
||||
|
||||
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun openCalculatorView() {
|
||||
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun startActionMode() {
|
||||
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
||||
}
|
||||
|
@ -1,16 +1,36 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceLoading
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||
import io.github.wulkanowy.utils.capitalise
|
||||
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||
import io.github.wulkanowy.utils.isHolidays
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.nextSchoolDay
|
||||
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.previousSchoolDay
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
fun onCalculatorSwitchSelected(): Boolean {
|
||||
view?.openCalculatorView()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
Timber.i("Loading attendance data started")
|
||||
|
||||
|
@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
|
||||
|
||||
fun openSummaryView()
|
||||
|
||||
fun openCalculatorView()
|
||||
|
||||
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
||||
|
||||
fun startActionMode()
|
||||
|
@ -0,0 +1,67 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AttendanceCalculatorAdapter @Inject constructor() :
|
||||
RecyclerView.Adapter<AttendanceCalculatorAdapter.ViewHolder>() {
|
||||
|
||||
var items = emptyList<AttendanceData>()
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemAttendanceCalculatorHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(parent: ViewHolder, position: Int) {
|
||||
val context = parent.binding.root.context
|
||||
val item = items[position]
|
||||
|
||||
with(parent.binding) {
|
||||
attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}"
|
||||
|
||||
attendanceCalculatorSummaryBalance.text = when {
|
||||
item.lessonBalance > 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_positive,
|
||||
item.lessonBalance
|
||||
)
|
||||
}
|
||||
|
||||
item.lessonBalance < 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_negative,
|
||||
abs(item.lessonBalance)
|
||||
)
|
||||
}
|
||||
|
||||
else -> context.getString(R.string.attendance_calculator_summary_balance_neutral)
|
||||
}
|
||||
attendanceCalculatorWarning.isVisible = item.lessonBalance < 0
|
||||
attendanceCalculatorTitle.text = item.subjectName
|
||||
attendanceCalculatorSummaryValues.text = if (item.total == 0) {
|
||||
context.getString(R.string.attendance_calculator_summary_values_empty)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_values,
|
||||
item.presences,
|
||||
item.total
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment
|
||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AttendanceCalculatorFragment :
|
||||
BaseFragment<FragmentAttendanceCalculatorBinding>(R.layout.fragment_attendance_calculator),
|
||||
AttendanceCalculatorView, MainView.TitledView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: AttendanceCalculatorPresenter
|
||||
|
||||
@Inject
|
||||
lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter
|
||||
|
||||
override val titleStringId get() = R.string.attendance_title
|
||||
|
||||
companion object {
|
||||
fun newInstance() = AttendanceCalculatorFragment()
|
||||
}
|
||||
|
||||
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentAttendanceCalculatorBinding.bind(view)
|
||||
messageContainer = binding.attendanceCalculatorRecycler
|
||||
presenter.onAttachView(this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.action_menu_attendance_calculator, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected()
|
||||
else false
|
||||
}
|
||||
|
||||
override fun openSettingsView() {
|
||||
(activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target)))
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
with(binding.attendanceCalculatorRecycler) {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = attendanceCalculatorAdapter
|
||||
addItemDecoration(DividerItemDecoration(context))
|
||||
}
|
||||
|
||||
with(binding) {
|
||||
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
|
||||
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
|
||||
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(
|
||||
requireContext().getThemeAttrColor(
|
||||
R.attr.colorSwipeRefresh
|
||||
)
|
||||
)
|
||||
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateData(data: List<AttendanceData>) {
|
||||
with(attendanceCalculatorAdapter) {
|
||||
items = data
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView() {
|
||||
with(attendanceCalculatorAdapter) {
|
||||
items = emptyList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showEmpty(show: Boolean) {
|
||||
binding.attendanceCalculatorEmpty.isVisible = show
|
||||
}
|
||||
|
||||
override fun showErrorView(show: Boolean) {
|
||||
binding.attendanceCalculatorError.isVisible = show
|
||||
}
|
||||
|
||||
override fun setErrorDetails(message: String) {
|
||||
binding.attendanceCalculatorErrorMessage.text = message
|
||||
}
|
||||
|
||||
override fun showProgress(show: Boolean) {
|
||||
binding.attendanceCalculatorProgress.isVisible = show
|
||||
}
|
||||
|
||||
override fun enableSwipe(enable: Boolean) {
|
||||
binding.attendanceCalculatorSwipe.isEnabled = enable
|
||||
}
|
||||
|
||||
override fun showContent(show: Boolean) {
|
||||
binding.attendanceCalculatorRecycler.isVisible = show
|
||||
}
|
||||
|
||||
override fun showRefresh(show: Boolean) {
|
||||
binding.attendanceCalculatorSwipe.isRefreshing = show
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class AttendanceCalculatorPresenter @Inject constructor(
|
||||
errorHandler: ErrorHandler,
|
||||
studentRepository: StudentRepository,
|
||||
private val semesterRepository: SemesterRepository,
|
||||
private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase,
|
||||
) : BasePresenter<AttendanceCalculatorView>(errorHandler, studentRepository) {
|
||||
|
||||
private lateinit var lastError: Throwable
|
||||
|
||||
override fun onAttachView(view: AttendanceCalculatorView) {
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
Timber.i("Attendance calculator view was initialized")
|
||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun onSwipeRefresh() {
|
||||
Timber.i("Force refreshing the attendance calculator")
|
||||
loadData(forceRefresh = true)
|
||||
}
|
||||
|
||||
fun onRetry() {
|
||||
view?.run {
|
||||
showErrorView(false)
|
||||
showProgress(true)
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun onDetailsClick() {
|
||||
view?.showErrorDetailsDialog(lastError)
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
flatResourceFlow {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
getAttendanceCalculatorData(student, semester, forceRefresh)
|
||||
}
|
||||
.logResourceStatus("load attendance calculator")
|
||||
.onResourceData {
|
||||
view?.run {
|
||||
showProgress(false)
|
||||
showErrorView(false)
|
||||
showContent(it.isNotEmpty())
|
||||
showEmpty(it.isEmpty())
|
||||
updateData(it)
|
||||
}
|
||||
}
|
||||
.onResourceIntermediate { view?.showRefresh(true) }
|
||||
.onResourceNotLoading {
|
||||
view?.run {
|
||||
enableSwipe(true)
|
||||
showRefresh(false)
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
.onResourceError(errorHandler::dispatch)
|
||||
.launch()
|
||||
}
|
||||
|
||||
private fun showErrorViewOnError(message: String, error: Throwable) {
|
||||
view?.run {
|
||||
if (isViewEmpty) {
|
||||
lastError = error
|
||||
setErrorDetails(message)
|
||||
showErrorView(true)
|
||||
showEmpty(false)
|
||||
} else showError(message, error)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSettingsSelected(): Boolean {
|
||||
view?.openSettingsView()
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
|
||||
interface AttendanceCalculatorView : BaseView {
|
||||
|
||||
val isViewEmpty: Boolean
|
||||
|
||||
fun initView()
|
||||
|
||||
fun showRefresh(show: Boolean)
|
||||
|
||||
fun showContent(show: Boolean)
|
||||
|
||||
fun showProgress(show: Boolean)
|
||||
|
||||
fun enableSwipe(enable: Boolean)
|
||||
|
||||
fun showEmpty(show: Boolean)
|
||||
|
||||
fun showErrorView(show: Boolean)
|
||||
|
||||
fun setErrorDetails(message: String)
|
||||
|
||||
fun updateData(data: List<AttendanceData>)
|
||||
|
||||
fun clearView()
|
||||
|
||||
fun openSettingsView()
|
||||
}
|
@ -5,6 +5,7 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceSummaryBinding
|
||||
import io.github.wulkanowy.databinding.ScrollableHeaderAttendanceSummaryBinding
|
||||
import io.github.wulkanowy.utils.calculatePercentage
|
||||
@ -13,9 +14,13 @@ import java.time.Month
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class AttendanceSummaryAdapter @Inject constructor() :
|
||||
class AttendanceSummaryAdapter @Inject constructor(
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val attendancePercentage = preferencesRepository.attendancePercentage
|
||||
|
||||
private enum class ViewType(val id: Int) {
|
||||
HEADER(1),
|
||||
ITEM(2)
|
||||
@ -48,7 +53,10 @@ class AttendanceSummaryAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindHeaderViewHolder(binding: ScrollableHeaderAttendanceSummaryBinding) {
|
||||
binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(items.calculatePercentage())
|
||||
binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(
|
||||
attendancePercentage ?:
|
||||
items.calculatePercentage()
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindItemViewHolder(binding: ItemAttendanceSummaryBinding, position: Int) {
|
||||
@ -60,8 +68,8 @@ class AttendanceSummaryAdapter @Inject constructor() :
|
||||
else -> item.month.getFormattedName()
|
||||
}
|
||||
attendanceSummaryPercentage.text = when (position) {
|
||||
-1 -> formatPercentage(items.calculatePercentage())
|
||||
else -> formatPercentage(item.calculatePercentage())
|
||||
-1 -> formatPercentage(attendancePercentage ?: item.calculatePercentage())
|
||||
else -> formatPercentage(attendancePercentage ?: item.calculatePercentage())
|
||||
}
|
||||
|
||||
attendanceSummaryPresent.text = item.presence.toString()
|
||||
|
@ -27,8 +27,12 @@ class AuthPresenter @Inject constructor(
|
||||
|
||||
private fun loadName() {
|
||||
presenterScope.launch {
|
||||
runCatching { studentRepository.getCurrentStudent(false) }
|
||||
.onSuccess { view?.showDescriptionWithName(it.studentName) }
|
||||
runCatching {
|
||||
studentRepository.getCurrentStudent(false)
|
||||
.studentName
|
||||
.replace(" ", "\u00A0")
|
||||
}
|
||||
.onSuccess { view?.showDescriptionWithName(it) }
|
||||
.onFailure { errorHandler.dispatch(it) }
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
webView = this
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = wulkanowySdkFactory.create().userAgent
|
||||
userAgentString = wulkanowySdkFactory.createBase().userAgent
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
|
@ -30,6 +30,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.message.MessageFragment
|
||||
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
|
||||
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
|
||||
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
|
||||
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
|
||||
import io.github.wulkanowy.utils.capitalise
|
||||
@ -125,6 +126,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
mainActivity.pushView(ConferenceFragment.newInstance())
|
||||
}
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected
|
||||
onPanicButtonClickListener = presenter::onPanicButtonClicked
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed
|
||||
|
||||
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
@ -208,7 +210,11 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
binding = binding.dashboardErrorAdminMessage,
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||
).bind(adminMessageItem.adminMessage)
|
||||
onPanicButtonClickListener = presenter::onPanicButtonClicked,
|
||||
).bind(
|
||||
item = adminMessageItem.adminMessage,
|
||||
showPanicButton = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,6 +242,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
requireContext().openInternetBrowser(url)
|
||||
}
|
||||
|
||||
override fun openPanicWebView(url: String) {
|
||||
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
dashboardAdapter.clearTimers()
|
||||
presenter.onDetachView()
|
||||
|
@ -147,6 +147,17 @@ sealed class DashboardItem(val type: Type) {
|
||||
EXAMS,
|
||||
CONFERENCES,
|
||||
}
|
||||
|
||||
enum class HiddenAttendanceTile {
|
||||
UNEXCUSED_ABSENCE,
|
||||
EXEMPTION,
|
||||
EXCUSED_LATENESS,
|
||||
UNEXCUSED_LATENESS,
|
||||
PRESENT,
|
||||
DELETED,
|
||||
EXCUSED_ABSENCE,
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
fun DashboardItem.Tile.toDashboardItemType() = when (this) {
|
||||
|
@ -11,6 +11,7 @@ import io.github.wulkanowy.data.errorOrNull
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
|
||||
import io.github.wulkanowy.data.repositories.ConferenceRepository
|
||||
import io.github.wulkanowy.data.repositories.ExamRepository
|
||||
@ -23,6 +24,7 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
@ -282,8 +285,26 @@ class DashboardPresenter @Inject constructor(
|
||||
url?.let { view?.openInternetBrowser(it) }
|
||||
}
|
||||
|
||||
fun onPanicButtonClicked() {
|
||||
resourceFlow { studentRepository.getCurrentStudent() }
|
||||
.onResourceError { errorHandler.dispatch(it) }
|
||||
.onResourceSuccess {
|
||||
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
|
||||
val urlToOpen = baseUrl.newBuilder()
|
||||
.host("uonetplus${it.scrapperDomainSuffix}.${baseUrl.host}")
|
||||
.addPathSegment(it.symbol)
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
view?.openPanicWebView(urlToOpen)
|
||||
}
|
||||
.launch("panic_button")
|
||||
}
|
||||
|
||||
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
|
||||
flow {
|
||||
val attendancePercentage = preferencesRepository.attendancePercentage
|
||||
|
||||
val selectedTiles = selectedDashboardTiles
|
||||
val flowSuccess = flowOf(Resource.Success(null))
|
||||
|
||||
@ -336,7 +357,7 @@ class DashboardPresenter @Inject constructor(
|
||||
} else null
|
||||
},
|
||||
attendancePercentage = DashboardItem.HorizontalGroup.Cell(
|
||||
data = attendanceResource.dataOrNull?.calculatePercentage(),
|
||||
data = attendancePercentage ?: attendanceResource.dataOrNull?.calculatePercentage(),
|
||||
error = attendanceResource.errorOrNull != null,
|
||||
isLoading = attendanceResource is Resource.Loading,
|
||||
),
|
||||
|
@ -31,4 +31,6 @@ interface DashboardView : BaseView {
|
||||
fun openNotificationsCenterView()
|
||||
|
||||
fun openInternetBrowser(url: String)
|
||||
|
||||
fun openPanicWebView(url: String)
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
|
||||
var onAdminMessageClickListener: (String?) -> Unit = {}
|
||||
|
||||
var onPanicButtonClickListener: () -> Unit = {}
|
||||
|
||||
var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {}
|
||||
|
||||
val items = mutableListOf<DashboardItem>()
|
||||
@ -86,35 +88,46 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder(
|
||||
ItemDashboardAccountBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder(
|
||||
ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.GRADES.ordinal -> GradesViewHolder(
|
||||
ItemDashboardGradesBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder(
|
||||
ItemDashboardLessonsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder(
|
||||
ItemDashboardHomeworkBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder(
|
||||
ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder(
|
||||
ItemDashboardExamsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
|
||||
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
|
||||
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
|
||||
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
|
||||
onAdminMessageClickListener = onAdminMessageClickListener,
|
||||
onPanicButtonClickListener = onPanicButtonClickListener,
|
||||
)
|
||||
|
||||
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
|
||||
ItemDashboardAdsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@ -129,7 +142,11 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
|
||||
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
|
||||
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
|
||||
is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
|
||||
is AdminMessageViewHolder -> holder.bind(
|
||||
(items[position] as DashboardItem.AdminMessages).adminMessage,
|
||||
showPanicButton = true
|
||||
)
|
||||
|
||||
is AdsViewHolder -> bindAdsViewHolder(holder, position)
|
||||
}
|
||||
}
|
||||
@ -240,12 +257,15 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
attendancePercentage == null || attendancePercentage == .0 -> {
|
||||
root.context.getThemeAttrColor(R.attr.colorOnSurface)
|
||||
}
|
||||
|
||||
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
|
||||
root.context.getThemeAttrColor(R.attr.colorPrimary)
|
||||
}
|
||||
|
||||
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
|
||||
root.context.getThemeAttrColor(R.attr.colorTimetableChange)
|
||||
}
|
||||
|
||||
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
|
||||
}
|
||||
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
|
||||
@ -336,24 +356,28 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
|
||||
tomorrowTimetable.isNotEmpty() -> {
|
||||
dateToNavigate = tomorrowDate
|
||||
updateLessonView(item, tomorrowTimetable, binding)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
|
||||
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
|
||||
dateToNavigate = currentDate
|
||||
updateLessonView(item, emptyList(), binding, currentDayHeader)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
|
||||
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
|
||||
dateToNavigate = tomorrowDate
|
||||
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
dateToNavigate = currentDate
|
||||
updateLessonView(item, emptyList(), binding)
|
||||
@ -461,6 +485,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
firstTitleText =
|
||||
context.getString(R.string.dashboard_timetable_first_lesson_title_moment)
|
||||
}
|
||||
|
||||
minutesToStartLesson < 240 -> {
|
||||
firstTitleAndValueTextColor =
|
||||
context.getThemeAttrColor(R.attr.colorOnSurface)
|
||||
@ -468,6 +493,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
firstTitleText =
|
||||
context.getString(R.string.dashboard_timetable_first_lesson_title_soon)
|
||||
}
|
||||
|
||||
else -> {
|
||||
firstTitleAndValueTextColor =
|
||||
context.getThemeAttrColor(R.attr.colorOnSurface)
|
||||
|
@ -13,9 +13,10 @@ class AdminMessageViewHolder(
|
||||
private val binding: ItemDashboardAdminMessageBinding,
|
||||
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
|
||||
private val onAdminMessageClickListener: (String?) -> Unit,
|
||||
private val onPanicButtonClickListener: () -> Unit,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: AdminMessage?) {
|
||||
fun bind(item: AdminMessage?, showPanicButton: Boolean = false) {
|
||||
item ?: return
|
||||
|
||||
val context = binding.root.context
|
||||
@ -48,10 +49,14 @@ class AdminMessageViewHolder(
|
||||
dashboardAdminMessageItemClose.setOnClickListener {
|
||||
onAdminMessageDismissClickListener(item)
|
||||
}
|
||||
dashboardPanicSection.root.isVisible = showPanicButton
|
||||
dashboardPanicSection.dashboardPanicButton.setOnClickListener {
|
||||
onPanicButtonClickListener()
|
||||
}
|
||||
|
||||
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
|
||||
dashboardAdminMessage.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
|
||||
item.destinationUrl?.let { url ->
|
||||
root.setOnClickListener { onAdminMessageClickListener(url) }
|
||||
dashboardAdminMessage.setOnClickListener { onAdminMessageClickListener(url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,6 @@ enum class GradeAverageMode(val value: String) {
|
||||
BOTH_SEMESTERS("both_semesters");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE_SEMESTER
|
||||
fun getByValue(value: String) = entries.firstOrNull { it.value == value } ?: ONE_SEMESTER
|
||||
}
|
||||
}
|
||||
|
@ -118,5 +118,6 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
inAppUpdateHelper.onResume()
|
||||
presenter.updateSdkMappings()
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
package io.github.wulkanowy.ui.modules.login
|
||||
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.repositories.WulkanowyRepository
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginPresenter @Inject constructor(
|
||||
private val wulkanowyRepository: WulkanowyRepository,
|
||||
errorHandler: ErrorHandler,
|
||||
studentRepository: StudentRepository
|
||||
) : BasePresenter<LoginView>(errorHandler, studentRepository) {
|
||||
@ -16,4 +19,11 @@ class LoginPresenter @Inject constructor(
|
||||
view.initView()
|
||||
Timber.i("Login view was initialized")
|
||||
}
|
||||
|
||||
fun updateSdkMappings() {
|
||||
presenterScope.launch {
|
||||
runCatching { wulkanowyRepository.fetchMapping() }
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,6 +238,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
|
||||
binding = binding.loginFormMessage,
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||
onPanicButtonClickListener = {},
|
||||
).bind(message)
|
||||
binding.loginFormMessage.root.isVisible = message != null
|
||||
}
|
||||
|
@ -6,10 +6,12 @@ import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
|
||||
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
||||
@ -111,6 +113,20 @@ class LoginStudentSelectFragment :
|
||||
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
||||
}
|
||||
|
||||
override fun showAdminMessage(adminMessage: AdminMessage?) {
|
||||
AdminMessageViewHolder(
|
||||
binding = binding.loginStudentSelectAdminMessage,
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||
onPanicButtonClickListener = {},
|
||||
).bind(adminMessage)
|
||||
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
|
||||
}
|
||||
|
||||
override fun openInternetBrowser(url: String) {
|
||||
requireContext().openInternetBrowser(url)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
|
@ -2,16 +2,23 @@ package io.github.wulkanowy.ui.modules.login.studentselect
|
||||
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.dataOrNull
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.pojos.RegisterStudent
|
||||
import io.github.wulkanowy.data.pojos.RegisterSymbol
|
||||
import io.github.wulkanowy.data.pojos.RegisterUnit
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SchoolsRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||
import io.github.wulkanowy.services.sync.SyncManager
|
||||
@ -33,6 +40,8 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
private val syncManager: SyncManager,
|
||||
private val analytics: AnalyticsHelper,
|
||||
private val appInfo: AppInfo,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
|
||||
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
|
||||
|
||||
private var lastError: Throwable? = null
|
||||
@ -65,6 +74,7 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
this.loginData = loginData
|
||||
this.registerUser = registerUser
|
||||
loadData()
|
||||
loadAdminMessage()
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
@ -88,7 +98,20 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
refreshItems()
|
||||
}
|
||||
}
|
||||
}.launch()
|
||||
}.launch("load_data")
|
||||
}
|
||||
|
||||
private fun loadAdminMessage() {
|
||||
flatResourceFlow {
|
||||
getAppropriateAdminMessageUseCase(
|
||||
scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(),
|
||||
type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE,
|
||||
)
|
||||
}
|
||||
.logResourceStatus("load login admin message")
|
||||
.onResourceData { view?.showAdminMessage(it) }
|
||||
.onResourceError { view?.showAdminMessage(null) }
|
||||
.launch("load_admin_message")
|
||||
}
|
||||
|
||||
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
|
||||
@ -341,4 +364,14 @@ class LoginStudentSelectPresenter @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAdminMessageSelected(url: String?) {
|
||||
url?.let { view?.openInternetBrowser(it) }
|
||||
}
|
||||
|
||||
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
|
||||
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
|
||||
|
||||
view?.showAdminMessage(null)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.ui.modules.login.studentselect
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
|
||||
@ -25,4 +26,8 @@ interface LoginStudentSelectView : BaseView {
|
||||
fun openDiscordInvite()
|
||||
|
||||
fun openEmail(supportInfo: LoginSupportInfo)
|
||||
|
||||
fun showAdminMessage(adminMessage: AdminMessage?)
|
||||
|
||||
fun openInternetBrowser(url: String)
|
||||
}
|
||||
|
@ -9,13 +9,16 @@ import android.view.inputmethod.EditorInfo.IME_NULL
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
|
||||
import io.github.wulkanowy.ui.modules.login.LoginActivity
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
|
||||
@ -179,4 +182,18 @@ class LoginSymbolFragment :
|
||||
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
|
||||
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
|
||||
}
|
||||
|
||||
override fun showAdminMessage(adminMessage: AdminMessage?) {
|
||||
AdminMessageViewHolder(
|
||||
binding = binding.loginSymbolAdminMessage,
|
||||
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected,
|
||||
onPanicButtonClickListener = {},
|
||||
).bind(adminMessage)
|
||||
binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
|
||||
}
|
||||
|
||||
override fun openInternetBrowser(url: String) {
|
||||
requireContext().openInternetBrowser(url)
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,18 @@ package io.github.wulkanowy.ui.modules.login.symbol
|
||||
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.dataOrNull
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.enums.MessageType
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
|
||||
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
|
||||
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -21,7 +29,9 @@ import javax.inject.Inject
|
||||
class LoginSymbolPresenter @Inject constructor(
|
||||
studentRepository: StudentRepository,
|
||||
private val loginErrorHandler: LoginErrorHandler,
|
||||
private val analytics: AnalyticsHelper
|
||||
private val analytics: AnalyticsHelper,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
|
||||
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository) {
|
||||
|
||||
private var lastError: Throwable? = null
|
||||
@ -43,6 +53,21 @@ class LoginSymbolPresenter @Inject constructor(
|
||||
clearAndFocusSymbol()
|
||||
showSoftKeyboard()
|
||||
}
|
||||
|
||||
loadAdminMessage()
|
||||
}
|
||||
|
||||
private fun loadAdminMessage() {
|
||||
flatResourceFlow {
|
||||
getAppropriateAdminMessageUseCase(
|
||||
scrapperBaseUrl = loginData.baseUrl,
|
||||
type = MessageType.LOGIN_SYMBOL_MESSAGE,
|
||||
)
|
||||
}
|
||||
.logResourceStatus("load login admin message")
|
||||
.onResourceData { view?.showAdminMessage(it) }
|
||||
.onResourceError { view?.showAdminMessage(null) }
|
||||
.launch("load_admin_message")
|
||||
}
|
||||
|
||||
fun onSymbolTextChanged() {
|
||||
@ -166,4 +191,14 @@ class LoginSymbolPresenter @Inject constructor(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun onAdminMessageSelected(url: String?) {
|
||||
url?.let { view?.openInternetBrowser(it) }
|
||||
}
|
||||
|
||||
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
|
||||
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
|
||||
|
||||
view?.showAdminMessage(null)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.ui.modules.login.symbol
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
@ -44,4 +45,8 @@ interface LoginSymbolView : BaseView {
|
||||
fun openFaqPage()
|
||||
|
||||
fun openSupportDialog(supportInfo: LoginSupportInfo)
|
||||
|
||||
fun showAdminMessage(adminMessage: AdminMessage?)
|
||||
|
||||
fun openInternetBrowser(url: String)
|
||||
}
|
||||
|
@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
|
||||
}
|
||||
|
||||
if (currentStudent != null) {
|
||||
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
|
||||
luckyNumberRepository.getLuckyNumber(
|
||||
student = currentStudent,
|
||||
forceRefresh = false,
|
||||
isFromAppWidget = true
|
||||
)
|
||||
.toFirstResult()
|
||||
.dataOrThrow
|
||||
} else null
|
||||
|
@ -138,6 +138,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
inAppUpdateHelper.onResume()
|
||||
presenter.updateSdkMappings()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
|
@ -6,6 +6,7 @@ import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.repositories.WulkanowyRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.services.sync.SyncManager
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -29,6 +30,7 @@ class MainPresenter @Inject constructor(
|
||||
errorHandler: ErrorHandler,
|
||||
studentRepository: StudentRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val wulkanowyRepository: WulkanowyRepository,
|
||||
private val syncManager: SyncManager,
|
||||
private val analytics: AnalyticsHelper,
|
||||
private val json: Json,
|
||||
@ -199,4 +201,11 @@ class MainPresenter @Inject constructor(
|
||||
.onFailure { errorHandler.dispatch(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSdkMappings() {
|
||||
presenterScope.launch {
|
||||
runCatching { wulkanowyRepository.fetchMapping() }
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.message.MessageFragment
|
||||
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
|
||||
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
|
||||
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
|
||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||
import io.github.wulkanowy.utils.dpToPx
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
@ -132,6 +133,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
|
||||
)
|
||||
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
messageTabPanicSection.dashboardPanicButton.setOnClickListener { presenter.onPanicButtonClicked() }
|
||||
}
|
||||
|
||||
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
|
||||
@ -283,6 +285,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
|
||||
)
|
||||
}
|
||||
|
||||
override fun openPanicWebView(url: String) {
|
||||
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
|
||||
}
|
||||
|
||||
override fun hideKeyboard() {
|
||||
activity?.hideSoftInput()
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.pow
|
||||
@ -429,4 +430,20 @@ class MessageTabPresenter @Inject constructor(
|
||||
+ dateRatio.toDouble().pow(2) * 2
|
||||
).toInt()
|
||||
}
|
||||
|
||||
fun onPanicButtonClicked() {
|
||||
resourceFlow { studentRepository.getCurrentStudent() }
|
||||
.onResourceError { errorHandler.dispatch(it) }
|
||||
.onResourceSuccess {
|
||||
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
|
||||
val urlToOpen = baseUrl.newBuilder()
|
||||
.host("uonetplus${it.scrapperDomainSuffix}-wiadomosciplus.${baseUrl.host}")
|
||||
.addPathSegment(it.symbol)
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
view?.openPanicWebView(urlToOpen)
|
||||
}
|
||||
.launch("panic_button")
|
||||
}
|
||||
}
|
||||
|
@ -50,4 +50,6 @@ interface MessageTabView : BaseView {
|
||||
fun showRecyclerBottomPadding(show: Boolean)
|
||||
|
||||
fun showMailboxChooser(mailboxes: List<Mailbox>)
|
||||
|
||||
fun openPanicWebView(url: String)
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
|
||||
|
||||
var onClickListener: (moreItem: MoreItem) -> Unit = {}
|
||||
|
||||
var onLongClickListener: (moreItem: MoreItem) -> Unit = {}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
|
||||
@ -27,6 +29,10 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
|
||||
moreItemImage.setImageResource(item.icon)
|
||||
|
||||
root.setOnClickListener { onClickListener(item) }
|
||||
root.setOnLongClickListener {
|
||||
onLongClickListener(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.databinding.FragmentMoreBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
@ -23,6 +24,9 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
|
||||
@Inject
|
||||
lateinit var moreAdapter: MoreAdapter
|
||||
|
||||
@Inject
|
||||
lateinit var preferencesRepository: PreferencesRepository
|
||||
|
||||
companion object {
|
||||
fun newInstance() = MoreFragment()
|
||||
}
|
||||
@ -73,4 +77,9 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun restartApp() {
|
||||
startActivity(MainActivity.getStartIntent(requireContext()))
|
||||
requireActivity().finishAffinity()
|
||||
}
|
||||
}
|
||||
|
@ -12,4 +12,6 @@ interface MoreView : BaseView {
|
||||
fun popView(depth: Int)
|
||||
|
||||
fun openView(destination: Destination)
|
||||
|
||||
fun restartApp()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.note
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.Note
|
||||
import io.github.wulkanowy.data.repositories.NoteRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
@ -17,6 +18,7 @@ class NotePresenter @Inject constructor(
|
||||
studentRepository: StudentRepository,
|
||||
private val noteRepository: NoteRepository,
|
||||
private val semesterRepository: SemesterRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val analytics: AnalyticsHelper
|
||||
) : BasePresenter<NoteView>(errorHandler, studentRepository) {
|
||||
|
||||
@ -48,6 +50,19 @@ class NotePresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
if (!preferencesRepository.showNotes) {
|
||||
view?.run {
|
||||
enableSwipe(false)
|
||||
showEmpty(false)
|
||||
showContent(false)
|
||||
showErrorView(false)
|
||||
showProgress(false)
|
||||
showEmpty(true)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
flatResourceFlow {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
|
@ -0,0 +1,99 @@
|
||||
package io.github.wulkanowy.ui.modules.panicmode
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.os.bundleOf
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.databinding.FragmentPanicModeBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import io.github.wulkanowy.utils.openInternetBrowser
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PanicModeFragment : BaseFragment<FragmentPanicModeBinding>(R.layout.fragment_panic_mode),
|
||||
MainView.TitledView {
|
||||
|
||||
@Inject
|
||||
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
|
||||
|
||||
@Inject
|
||||
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
|
||||
|
||||
private var webView: WebView? = null
|
||||
|
||||
override val titleStringId: Int get() = R.string.panic_mode_title
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PANIC_URL = "panic_mode_url"
|
||||
fun newInstance(url: String?): PanicModeFragment {
|
||||
return PanicModeFragment().apply {
|
||||
arguments = bundleOf(PANIC_URL to url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentPanicModeBinding.bind(view)
|
||||
|
||||
binding.panicModeRefresh.setOnClickListener {
|
||||
binding.panicModeWebview.loadUrl(
|
||||
binding.panicModeWebview.url ?: arguments?.getString(PANIC_URL).orEmpty()
|
||||
)
|
||||
}
|
||||
binding.panicModeBack.setOnClickListener { binding.panicModeWebview.goBack() }
|
||||
binding.panicModeHome.setOnClickListener {
|
||||
binding.panicModeWebview.loadUrl(
|
||||
arguments?.getString(PANIC_URL).orEmpty()
|
||||
)
|
||||
}
|
||||
binding.panicModeForward.setOnClickListener { binding.panicModeWebview.goForward() }
|
||||
binding.panicModeShare.setOnClickListener {
|
||||
requireContext().openInternetBrowser(
|
||||
binding.panicModeWebview.url.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
val onBackPressedCallback = requireActivity().onBackPressedDispatcher
|
||||
.addCallback(viewLifecycleOwner) {
|
||||
binding.panicModeWebview.goBack()
|
||||
}
|
||||
|
||||
with(binding.panicModeWebview) {
|
||||
webView = this
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = wulkanowySdkFactory.createBase().userAgent
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun doUpdateVisitedHistory(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
isReload: Boolean
|
||||
) {
|
||||
binding.panicModeBack.isEnabled = binding.panicModeWebview.canGoBack()
|
||||
binding.panicModeForward.isEnabled = binding.panicModeWebview.canGoForward()
|
||||
onBackPressedCallback.isEnabled = binding.panicModeWebview.canGoBack()
|
||||
}
|
||||
}
|
||||
loadUrl(arguments?.getString(PANIC_URL).orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
webkitCookieManagerProxy.webkitCookieManager?.flush()
|
||||
webView?.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
@ -1,13 +1,22 @@
|
||||
package io.github.wulkanowy.ui.modules.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceScreen
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.ui.base.BaseActivity
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView {
|
||||
@AndroidEntryPoint
|
||||
class SettingsFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
MainView.TitledView, SettingsView {
|
||||
|
||||
companion object {
|
||||
|
||||
@ -16,11 +25,26 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin
|
||||
|
||||
override val titleStringId get() = R.string.settings_title
|
||||
|
||||
@Inject
|
||||
lateinit var preferencesRepository: PreferencesRepository
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
|
||||
|
||||
val prefScreen: PreferenceScreen? = findPreference("settings_preferences")
|
||||
val prefDeveloper: Preference? = findPreference("mod_settings")
|
||||
|
||||
if (!preferencesRepository.developerMode && prefScreen != null && prefDeveloper != null) {
|
||||
prefScreen.removePreference(prefDeveloper)
|
||||
}
|
||||
|
||||
Timber.i("Settings view was initialized")
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
|
||||
}
|
||||
|
||||
override fun showError(text: String, error: Throwable) {}
|
||||
|
||||
override fun showMessage(text: String) {}
|
||||
|
@ -3,7 +3,9 @@ package io.github.wulkanowy.ui.modules.settings.appearance
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SeekBarPreference
|
||||
import com.yariksoffice.lingver.Lingver
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
@ -29,13 +31,31 @@ class AppearanceFragment : PreferenceFragmentCompat(),
|
||||
|
||||
override val titleStringId get() = R.string.pref_settings_appearance_title
|
||||
|
||||
companion object {
|
||||
fun withFocusedPreference(key: String) = AppearanceFragment().apply {
|
||||
arguments = bundleOf(FOCUSED_KEY to key)
|
||||
}
|
||||
|
||||
private const val FOCUSED_KEY = "focusedKey"
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
presenter.onAttachView(this)
|
||||
arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) }
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
||||
val attendanceTargetPref =
|
||||
findPreference<SeekBarPreference>(requireContext().getString(R.string.pref_key_attendance_target))!!
|
||||
attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj ->
|
||||
val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5
|
||||
attendanceTargetPref.value =
|
||||
newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max)
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
|
@ -0,0 +1,147 @@
|
||||
package io.github.wulkanowy.ui.modules.settings.mod_settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.ui.base.BaseActivity
|
||||
import io.github.wulkanowy.ui.base.ErrorDialog
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.utils.AppInfo
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ModSettingsFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
MainView.TitledView, ModSettingsView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: ModSettingsPresenter
|
||||
|
||||
@Inject
|
||||
lateinit var appInfo: AppInfo
|
||||
|
||||
@Inject
|
||||
lateinit var preferencesRepository: PreferencesRepository
|
||||
|
||||
override val titleStringId get() = R.string.pref_mod_settings_title
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
presenter.onAttachView(this)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.scheme_preferences_mod_settings, rootKey)
|
||||
|
||||
val attendancePercentagePreference: EditTextPreference? = findPreference("attendance_percentage")
|
||||
attendancePercentagePreference?.setOnBindEditTextListener { editText ->
|
||||
editText.inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||
editText.filters = arrayOf(
|
||||
android.text.InputFilter { source, _, _, dest, _, _ ->
|
||||
if (source == "." && dest.isEmpty()) {
|
||||
return@InputFilter "0."
|
||||
}
|
||||
|
||||
val input = dest.toString() + source.toString()
|
||||
if (input == "100.00") {
|
||||
return@InputFilter null
|
||||
}
|
||||
|
||||
val inputVal = input.toFloatOrNull()
|
||||
if (inputVal != null && inputVal >= 0 && inputVal <= 100) {
|
||||
null
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
presenter.onSharedPreferenceChanged(key)
|
||||
}
|
||||
|
||||
override fun showError(text: String, error: Throwable) {
|
||||
(activity as? BaseActivity<*, *>)?.showError(text, error)
|
||||
}
|
||||
|
||||
override fun showMessage(text: String) {
|
||||
(activity as? BaseActivity<*, *>)?.showMessage(text)
|
||||
}
|
||||
|
||||
override fun showExpiredCredentialsDialog() {
|
||||
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
|
||||
}
|
||||
|
||||
override fun onCaptchaVerificationRequired(url: String?) {
|
||||
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
|
||||
}
|
||||
|
||||
override fun showDecryptionFailedDialog() {
|
||||
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
|
||||
}
|
||||
|
||||
override fun showChangePasswordSnackbar(redirectUrl: String) {
|
||||
(activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl)
|
||||
}
|
||||
|
||||
override fun openClearLoginView() {
|
||||
(activity as? BaseActivity<*, *>)?.openClearLoginView()
|
||||
}
|
||||
|
||||
override fun showErrorDetailsDialog(error: Throwable) {
|
||||
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
|
||||
}
|
||||
|
||||
override fun showAuthDialog() {
|
||||
(activity as? BaseActivity<*, *>)?.showAuthDialog()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun showAttendanceSettings(items: List<DashboardItem.HiddenAttendanceTile>) {
|
||||
val entries = requireContext().resources.getStringArray(R.array.mod_settings_attendance_entries)
|
||||
val values = requireContext().resources.getStringArray(R.array.mod_settings_attendance_values)
|
||||
val selectedItemsState = values.map { value -> items.any { it.name == value } }
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.attendance_title)
|
||||
.setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> }
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
val selectedState = (dialog as AlertDialog).listView.checkedItemPositions
|
||||
val selectedValues = values
|
||||
.filterIndexed { index, _ -> selectedState[index] }
|
||||
.map { DashboardItem.HiddenAttendanceTile.valueOf(it) }
|
||||
|
||||
Timber.i("Selected attendance to hide: $selectedValues")
|
||||
presenter.onAttendanceSettingsSelected(selectedValues)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun restartApp() {
|
||||
startActivity(MainActivity.getStartIntent(requireContext()))
|
||||
requireActivity().finishAffinity()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user